/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.index; import static com.google.common.collect.Maps.newHashMap; import static org.elasticsearch.common.collect.MapBuilder.newMapBuilder; import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.IOUtils; import org.elassandra.cluster.InternalCassandraClusterService; import org.elassandra.index.search.TokenRangesBitsetFilterCache; import org.elassandra.search.SearchProcessorFactory; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.ConfigurationException; import org.elasticsearch.common.inject.CreationException; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Injector; import org.elasticsearch.common.inject.Injectors; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.ModulesBuilder; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.env.ShardLock; import org.elasticsearch.index.aliases.IndexAliasesService; import org.elasticsearch.index.analysis.AnalysisService; import org.elasticsearch.index.cache.IndexCache; import org.elasticsearch.index.cache.bitset.BitsetFilterCache; import org.elasticsearch.index.deletionpolicy.DeletionPolicyModule; import org.elasticsearch.index.fielddata.FieldDataType; import org.elasticsearch.index.fielddata.IndexFieldDataCache; import org.elasticsearch.index.fielddata.IndexFieldDataService; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.IndexQueryParserService; import org.elasticsearch.index.settings.IndexSettingsService; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardModule; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardNotFoundException; import org.elasticsearch.index.shard.ShardPath; import org.elasticsearch.index.shard.StoreRecoveryService; import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.index.store.IndexStore; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.store.StoreModule; import org.elasticsearch.index.translog.TranslogService; import org.elasticsearch.indices.IndicesLifecycle; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.InternalIndicesLifecycle; import org.elasticsearch.indices.cache.query.IndicesQueryCache; import org.elasticsearch.plugins.PluginsService; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; /** * */ public class IndexService extends AbstractIndexComponent implements IndexComponent, Iterable<IndexShard> { private final Injector injector; private final PluginsService pluginsService; private final InternalIndicesLifecycle indicesLifecycle; private final AnalysisService analysisService; private final MapperService mapperService; private final ClusterService clusterService; private final IndexQueryParserService queryParserService; private final SimilarityService similarityService; private final IndexAliasesService aliasesService; private final IndexCache indexCache; private final IndexFieldDataService indexFieldData; private final BitsetFilterCache bitsetFilterCache; private final IndexSettingsService settingsService; private final NodeEnvironment nodeEnv; private final IndicesService indicesServices; private volatile ImmutableMap<Integer, IndexShardInjectorPair> shards = ImmutableMap.of(); private SearchProcessorFactory searchProcessorFactory; private static class IndexShardInjectorPair { private final IndexShard indexShard; private final Injector injector; public IndexShardInjectorPair(IndexShard indexShard, Injector injector) { this.indexShard = indexShard; this.injector = injector; } public IndexShard getIndexShard() { return indexShard; } public Injector getInjector() { return injector; } } private final AtomicBoolean closed = new AtomicBoolean(false); private final AtomicBoolean deleted = new AtomicBoolean(false); @Inject public IndexService(Injector injector, Index index, NodeEnvironment nodeEnv, AnalysisService analysisService, MapperService mapperService, IndexQueryParserService queryParserService, SimilarityService similarityService, IndexAliasesService aliasesService, IndexCache indexCache, IndexSettingsService settingsService, IndexFieldDataService indexFieldData, BitsetFilterCache bitSetFilterCache, IndicesService indicesServices) { super(index, settingsService.getSettings()); this.injector = injector; this.analysisService = analysisService; this.mapperService = mapperService; this.queryParserService = queryParserService; this.similarityService = similarityService; this.aliasesService = aliasesService; this.indexCache = indexCache; this.indexFieldData = indexFieldData; this.settingsService = settingsService; this.bitsetFilterCache = bitSetFilterCache; this.clusterService = injector.getInstance(ClusterService.class); this.pluginsService = injector.getInstance(PluginsService.class); try { this.searchProcessorFactory = injector.getInstance(SearchProcessorFactory.class); logger.debug("SearchProcessorFactory={}", searchProcessorFactory.getClass().getName()); } catch(ConfigurationException e) { logger.debug("No SearchProcessorFactory " ); } this.indicesServices = indicesServices; this.indicesLifecycle = (InternalIndicesLifecycle) injector.getInstance(IndicesLifecycle.class); // inject workarounds for cyclic dep indexFieldData.setListener(new FieldDataCacheListener(this)); bitSetFilterCache.setListener(new BitsetCacheListener(this)); this.indexCache.tokenRangeBitsetFilterCache().setListener(new TokenRangeBitsetCacheListener(this)); this.nodeEnv = nodeEnv; } public int numberOfShards() { return shards.size(); } public InternalIndicesLifecycle indicesLifecycle() { return this.indicesLifecycle; } public SearchProcessorFactory searchProcessorFactory() { return this.searchProcessorFactory; } public boolean isTokenRangesBitsetCacheEnabled() { return this.settingsService.indexSettings().getAsBoolean(IndexMetaData.SETTING_TOKEN_RANGES_BITSET_CACHE, this.clusterService.settings().getAsBoolean(InternalCassandraClusterService.SETTING_CLUSTER_TOKEN_RANGES_BITSET_CACHE, Boolean.getBoolean(InternalCassandraClusterService.SETTING_SYSTEM_TOKEN_RANGES_BITSET_CACHE))); } @Override public Iterator<IndexShard> iterator() { return Iterators.transform(shards.values().iterator(), new Function<IndexShardInjectorPair, IndexShard>() { @Override public IndexShard apply(IndexShardInjectorPair input) { return input.getIndexShard(); } }); } public boolean hasShard(int shardId) { return shards.containsKey(shardId); } /** * Return the shard with the provided id, or null if there is no such shard. */ @Nullable public IndexShard shard(int shardId) { IndexShardInjectorPair indexShardInjectorPair = shards.get(shardId); if (indexShardInjectorPair != null) { return indexShardInjectorPair.getIndexShard(); } return null; } /** * Return the shard with the provided id, or throw an exception if it doesn't exist. */ public IndexShard shardSafe(int shardId) { IndexShard indexShard = shard(shardId); if (indexShard == null) { throw new ShardNotFoundException(new ShardId(index, shardId)); } return indexShard; } public Set<Integer> shardIds() { return shards.keySet(); } public Injector injector() { return injector; } public IndexSettingsService settingsService() { return this.settingsService; } public IndexCache cache() { return indexCache; } public IndexFieldDataService fieldData() { return indexFieldData; } public BitsetFilterCache bitsetFilterCache() { return bitsetFilterCache; } public AnalysisService analysisService() { return this.analysisService; } public ClusterService clusterService() { return this.clusterService; } public MapperService mapperService() { return mapperService; } public IndexQueryParserService queryParserService() { return queryParserService; } public SimilarityService similarityService() { return similarityService; } public IndexAliasesService aliasesService() { return aliasesService; } public synchronized void close(final String reason, boolean delete) { if (closed.compareAndSet(false, true)) { deleted.compareAndSet(false, delete); final Set<Integer> shardIds = shardIds(); for (final int shardId : shardIds) { try { removeShard(shardId, reason); } catch (Throwable t) { logger.warn("failed to close shard", t); } } } } /** * Return the shard injector for the provided id, or throw an exception if there is no such shard. */ public Injector shardInjectorSafe(int shardId) { IndexShardInjectorPair indexShardInjectorPair = shards.get(shardId); if (indexShardInjectorPair == null) { throw new ShardNotFoundException(new ShardId(index, shardId)); } return indexShardInjectorPair.getInjector(); } public String indexUUID() { return indexSettings().get(IndexMetaData.SETTING_INDEX_UUID, IndexMetaData.INDEX_UUID_NA_VALUE); } // NOTE: O(numShards) cost, but numShards should be smallish? private long getAvgShardSizeInBytes() throws IOException { long sum = 0; int count = 0; for(IndexShard indexShard : this) { sum += indexShard.store().stats().sizeInBytes(); count++; } if (count == 0) { return -1L; } else { return sum / count; } } public synchronized IndexShard createShard(ShardRouting routing) { return createShard(0, routing); } public synchronized IndexShard createShard(int sShardId, ShardRouting routing) { final boolean primary = routing.primary(); final Settings indexSettings = indexSettings(); /* * TODO: we execute this in parallel but it's a synced method. Yet, we might * be able to serialize the execution via the cluster state in the future. for now we just * keep it synced. */ if (closed.get()) { throw new IllegalStateException("Can't create shard [" + index.name() + "][" + sShardId + "], closed"); } final ShardId shardId = new ShardId(index, sShardId); ShardLock lock = null; boolean success = false; Injector shardInjector = null; try { lock = nodeEnv.shardLock(shardId, TimeUnit.SECONDS.toMillis(5)); indicesLifecycle.beforeIndexShardCreated(shardId, indexSettings); ShardPath path; try { path = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings); } catch (IllegalStateException ex) { logger.warn("{} failed to load shard path, trying to remove leftover", shardId); try { ShardPath.deleteLeftoverShardDirectory(logger, nodeEnv, lock, indexSettings); path = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings); } catch (Throwable t) { t.addSuppressed(ex); throw t; } } if (path == null) { // TODO: we should, instead, hold a "bytes reserved" of how large we anticipate this shard will be, e.g. for a shard // that's being relocated/replicated we know how large it will become once it's done copying: // Count up how many shards are currently on each data path: Map<Path,Integer> dataPathToShardCount = new HashMap<>(); for(IndexShard shard : this) { Path dataPath = shard.shardPath().getRootStatePath(); Integer curCount = dataPathToShardCount.get(dataPath); if (curCount == null) { curCount = 0; } dataPathToShardCount.put(dataPath, curCount+1); } path = ShardPath.selectNewPathForShard(nodeEnv, shardId, indexSettings, routing.getExpectedShardSize() == ShardRouting.UNAVAILABLE_EXPECTED_SHARD_SIZE ? getAvgShardSizeInBytes() : routing.getExpectedShardSize(), dataPathToShardCount); logger.debug("{} creating using a new path [{}]", shardId, path); } else { logger.debug("{} creating using an existing path [{}]", shardId, path); } if (shards.containsKey(shardId.id())) { throw new IndexShardAlreadyExistsException(shardId + " already exists"); } logger.debug("creating shard_id {}", shardId); // if we are on a shared FS we only own the shard (ie. we can safely delete it) if we are the primary. final boolean canDeleteShardContent = IndexMetaData.isOnSharedFilesystem(indexSettings) == false || (primary && IndexMetaData.isOnSharedFilesystem(indexSettings)); ModulesBuilder modules = new ModulesBuilder(); // plugin modules must be added here, before others or we can get crazy injection errors... for (Module pluginModule : pluginsService.shardModules(indexSettings)) { modules.add(pluginModule); } modules.add(new IndexShardModule(shardId, primary, indexSettings)); modules.add(new StoreModule(injector.getInstance(IndexStore.class).shardDirectory(), lock, new StoreCloseListener(shardId, canDeleteShardContent, new Closeable() { @Override public void close() throws IOException { injector.getInstance(IndicesQueryCache.class).onClose(shardId); } }), path)); modules.add(new DeletionPolicyModule()); pluginsService.processModules(modules); try { shardInjector = modules.createChildInjector(injector); } catch (CreationException e) { ElasticsearchException ex = new ElasticsearchException("failed to create shard", Injectors.getFirstErrorFailure(e)); ex.setShard(shardId); throw ex; } catch (Throwable e) { ElasticsearchException ex = new ElasticsearchException("failed to create shard", e); ex.setShard(shardId); throw ex; } IndexShard indexShard = shardInjector.getInstance(IndexShard.class); indicesLifecycle.indexShardStateChanged(indexShard, null, "shard created"); indicesLifecycle.afterIndexShardCreated(indexShard); shards = newMapBuilder(shards).put(shardId.id(), new IndexShardInjectorPair(indexShard, shardInjector)).immutableMap(); success = true; return indexShard; } catch (IOException e) { ElasticsearchException ex = new ElasticsearchException("failed to create shard", e); ex.setShard(shardId); throw ex; } finally { if (success == false) { IOUtils.closeWhileHandlingException(lock); if (shardInjector != null) { IndexShard indexShard = shardInjector.getInstance(IndexShard.class); closeShardInjector("initialization failed", shardId, shardInjector, indexShard); } } } } public synchronized void removeShard(int shardId, String reason) { final ShardId sId = new ShardId(index, shardId); final Injector shardInjector; final IndexShard indexShard; if (shards.containsKey(shardId) == false) { return; } logger.debug("[{}] closing... (reason: [{}])", shardId, reason); HashMap<Integer, IndexShardInjectorPair> tmpShardsMap = newHashMap(shards); IndexShardInjectorPair indexShardInjectorPair = tmpShardsMap.remove(shardId); indexShard = indexShardInjectorPair.getIndexShard(); shardInjector = indexShardInjectorPair.getInjector(); shards = ImmutableMap.copyOf(tmpShardsMap); closeShardInjector(reason, sId, shardInjector, indexShard); logger.debug("[{}] closed (reason: [{}])", shardId, reason); } private void closeShardInjector(String reason, ShardId sId, Injector shardInjector, IndexShard indexShard) { final int shardId = sId.id(); final Settings indexSettings = indexSettings(); try { try { indicesLifecycle.beforeIndexShardClosed(sId, indexShard, indexSettings); } finally { // close everything else even if the beforeIndexShardClosed threw an exception for (Class<? extends Closeable> closeable : pluginsService.shardServices()) { try { shardInjector.getInstance(closeable).close(); } catch (Throwable e) { logger.debug("[{}] failed to clean plugin shard service [{}]", e, shardId, closeable); } } // now we can close the translog service, we need to close it before the we close the shard // note the that the translog service is not there for shadow replicas closeInjectorOptionalResource(sId, shardInjector, TranslogService.class); // this logic is tricky, we want to close the engine so we rollback the changes done to it // and close the shard so no operations are allowed to it if (indexShard != null) { try { final boolean flushEngine = deleted.get() == false && closed.get(); // only flush we are we closed (closed index or shutdown) and if we are not deleted indexShard.close(reason, flushEngine); } catch (Throwable e) { logger.debug("[{}] failed to close index shard", e, shardId); // ignore } } closeInjectorResource(sId, shardInjector, StoreRecoveryService.class); // call this before we close the store, so we can release resources for it indicesLifecycle.afterIndexShardClosed(sId, indexShard, indexSettings); } } finally { try { shardInjector.getInstance(Store.class).close(); } catch (Throwable e) { logger.warn("[{}] failed to close store on shard removal (reason: [{}])", e, shardId, reason); } } } /** * This method gets an instance for each of the given classes passed and calls #close() on the returned instance. * NOTE: this method swallows all exceptions thrown from the close method of the injector and logs them as debug log */ private void closeInjectorResource(ShardId shardId, Injector shardInjector, Class<? extends Closeable>... toClose) { for (Class<? extends Closeable> closeable : toClose) { if (closeInjectorOptionalResource(shardId, shardInjector, closeable) == false) { logger.warn("[{}] no instance available for [{}], ignoring... ", shardId, closeable.getSimpleName()); } } } /** * Closes an optional resource. Returns true if the resource was found; * NOTE: this method swallows all exceptions thrown from the close method of the injector and logs them as debug log */ private boolean closeInjectorOptionalResource(ShardId shardId, Injector shardInjector, Class<? extends Closeable> toClose) { try { final Closeable instance = shardInjector.getInstance(toClose); if (instance == null) { return false; } IOUtils.close(instance); } catch (Throwable t) { logger.debug("{} failed to close {}", t, shardId, Strings.toUnderscoreCase(toClose.getSimpleName())); } return true; } private void onShardClose(ShardLock lock, boolean ownsShard) { if (deleted.get()) { // we remove that shards content if this index has been deleted final Settings indexSettings = indexSettings(); try { if (ownsShard) { try { indicesLifecycle.beforeIndexShardDeleted(lock.getShardId(), indexSettings); } finally { indicesServices.deleteShardStore("delete index", lock, indexSettings); indicesLifecycle.afterIndexShardDeleted(lock.getShardId(), indexSettings); } } } catch (IOException e) { indicesServices.addPendingDelete(lock.getShardId(), indexSettings); logger.debug("[{}] failed to delete shard content - scheduled a retry", e, lock.getShardId().id()); } } } private class StoreCloseListener implements Store.OnClose { private final ShardId shardId; private final boolean ownsShard; private final Closeable[] toClose; public StoreCloseListener(ShardId shardId, boolean ownsShard, Closeable... toClose) { this.shardId = shardId; this.ownsShard = ownsShard; this.toClose = toClose; } @Override public void handle(ShardLock lock) { try { assert lock.getShardId().equals(shardId) : "shard id mismatch, expected: " + shardId + " but got: " + lock.getShardId(); onShardClose(lock, ownsShard); } finally { try { IOUtils.close(toClose); } catch (IOException ex) { logger.debug("failed to close resource", ex); } } } } @Override public Settings indexSettings() { return settingsService.getSettings(); } public String keyspace() { return settingsService().getSettings().get(IndexMetaData.SETTING_KEYSPACE, this.index.name()); } private static final class BitsetCacheListener implements BitsetFilterCache.Listener { final IndexService indexService; private BitsetCacheListener(IndexService indexService) { this.indexService = indexService; } @Override public void onCache(ShardId shardId, Accountable accountable) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { long ramBytesUsed = accountable != null ? accountable.ramBytesUsed() : 0l; shard.shardBitsetFilterCache().onCached(ramBytesUsed); } } } @Override public void onRemoval(ShardId shardId, Accountable accountable) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { long ramBytesUsed = accountable != null ? accountable.ramBytesUsed() : 0l; shard.shardBitsetFilterCache().onRemoval(ramBytesUsed); } } } } private static final class TokenRangeBitsetCacheListener implements TokenRangesBitsetFilterCache.Listener { final IndexService indexService; private TokenRangeBitsetCacheListener(IndexService indexService) { this.indexService = indexService; } @Override public void onCache(ShardId shardId, Accountable accountable) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { long ramBytesUsed = accountable != null ? accountable.ramBytesUsed() : 0l; shard.tokenRangesBitsetFilterCache().onCached(ramBytesUsed); } } } @Override public void onRemoval(ShardId shardId, Accountable accountable) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { long ramBytesUsed = accountable != null ? accountable.ramBytesUsed() : 0l; shard.tokenRangesBitsetFilterCache().onRemoval(ramBytesUsed); } } } } private final class FieldDataCacheListener implements IndexFieldDataCache.Listener { final IndexService indexService; public FieldDataCacheListener(IndexService indexService) { this.indexService = indexService; } @Override public void onCache(ShardId shardId, MappedFieldType.Names fieldNames, FieldDataType fieldDataType, Accountable ramUsage) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { shard.fieldData().onCache(shardId, fieldNames, fieldDataType, ramUsage); } } } @Override public void onRemoval(ShardId shardId, MappedFieldType.Names fieldNames, FieldDataType fieldDataType, boolean wasEvicted, long sizeInBytes) { if (shardId != null) { final IndexShard shard = indexService.shard(shardId.id()); if (shard != null) { shard.fieldData().onRemoval(shardId, fieldNames, fieldDataType, wasEvicted, sizeInBytes); } } } } }