/* * 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.engine; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.FilterLeafReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexFileNames; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.SegmentCommitInfo; import org.apache.lucene.index.SegmentInfos; import org.apache.lucene.index.SegmentReader; import org.apache.lucene.index.SnapshotDeletionPolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.store.Directory; import org.apache.lucene.store.IOContext; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.Accountables; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.lease.Releasables; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndVersion; import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.merge.MergeStats; import org.elasticsearch.index.seqno.SequenceNumbersService; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.translog.Translog; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.NoSuchFileException; import java.util.Arrays; import java.util.Base64; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; public abstract class Engine implements Closeable { public static final String SYNC_COMMIT_ID = "sync_id"; protected final ShardId shardId; protected final Logger logger; protected final EngineConfig engineConfig; protected final Store store; protected final AtomicBoolean isClosed = new AtomicBoolean(false); protected final EventListener eventListener; protected final SnapshotDeletionPolicy deletionPolicy; protected final ReentrantLock failEngineLock = new ReentrantLock(); protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); protected final ReleasableLock readLock = new ReleasableLock(rwl.readLock()); protected final ReleasableLock writeLock = new ReleasableLock(rwl.writeLock()); protected final SetOnce<Exception> failedEngine = new SetOnce<>(); /* * on <tt>lastWriteNanos</tt> we use System.nanoTime() to initialize this since: * - we use the value for figuring out if the shard / engine is active so if we startup and no write has happened yet we still consider it active * for the duration of the configured active to inactive period. If we initialize to 0 or Long.MAX_VALUE we either immediately or never mark it * inactive if no writes at all happen to the shard. * - we also use this to flush big-ass merges on an inactive engine / shard but if we we initialize 0 or Long.MAX_VALUE we either immediately or never * commit merges even though we shouldn't from a user perspective (this can also have funky sideeffects in tests when we open indices with lots of segments * and suddenly merges kick in. * NOTE: don't use this value for anything accurate it's a best effort for freeing up diskspace after merges and on a shard level to reduce index buffer sizes on * inactive shards. */ protected volatile long lastWriteNanos = System.nanoTime(); protected Engine(EngineConfig engineConfig) { Objects.requireNonNull(engineConfig.getStore(), "Store must be provided to the engine"); Objects.requireNonNull(engineConfig.getDeletionPolicy(), "Snapshot deletion policy must be provided to the engine"); this.engineConfig = engineConfig; this.shardId = engineConfig.getShardId(); this.store = engineConfig.getStore(); this.logger = Loggers.getLogger(Engine.class, // we use the engine class directly here to make sure all subclasses have the same logger name engineConfig.getIndexSettings().getSettings(), engineConfig.getShardId()); this.eventListener = engineConfig.getEventListener(); this.deletionPolicy = engineConfig.getDeletionPolicy(); } /** Returns 0 in the case where accountable is null, otherwise returns {@code ramBytesUsed()} */ protected static long guardedRamBytesUsed(Accountable a) { if (a == null) { return 0; } return a.ramBytesUsed(); } /** * Tries to extract a segment reader from the given index reader. * If no SegmentReader can be extracted an {@link IllegalStateException} is thrown. */ protected static SegmentReader segmentReader(LeafReader reader) { if (reader instanceof SegmentReader) { return (SegmentReader) reader; } else if (reader instanceof FilterLeafReader) { final FilterLeafReader fReader = (FilterLeafReader) reader; return segmentReader(FilterLeafReader.unwrap(fReader)); } // hard fail - we can't get a SegmentReader throw new IllegalStateException("Can not extract segment reader from given index reader [" + reader + "]"); } /** * Returns whether a leaf reader comes from a merge (versus flush or addIndexes). */ protected static boolean isMergedSegment(LeafReader reader) { // We expect leaves to be segment readers final Map<String, String> diagnostics = segmentReader(reader).getSegmentInfo().info.getDiagnostics(); final String source = diagnostics.get(IndexWriter.SOURCE); assert Arrays.asList(IndexWriter.SOURCE_ADDINDEXES_READERS, IndexWriter.SOURCE_FLUSH, IndexWriter.SOURCE_MERGE).contains(source) : "Unknown source " + source; return IndexWriter.SOURCE_MERGE.equals(source); } protected Searcher newSearcher(String source, IndexSearcher searcher, SearcherManager manager) { return new EngineSearcher(source, searcher, manager, store, logger); } public final EngineConfig config() { return engineConfig; } protected abstract SegmentInfos getLastCommittedSegmentInfos(); public MergeStats getMergeStats() { return new MergeStats(); } /** * A throttling class that can be activated, causing the * {@code acquireThrottle} method to block on a lock when throttling * is enabled */ protected static final class IndexThrottle { private final CounterMetric throttleTimeMillisMetric = new CounterMetric(); private volatile long startOfThrottleNS; private static final ReleasableLock NOOP_LOCK = new ReleasableLock(new NoOpLock()); private final ReleasableLock lockReference = new ReleasableLock(new ReentrantLock()); private volatile ReleasableLock lock = NOOP_LOCK; public Releasable acquireThrottle() { return lock.acquire(); } /** Activate throttling, which switches the lock to be a real lock */ public void activate() { assert lock == NOOP_LOCK : "throttling activated while already active"; startOfThrottleNS = System.nanoTime(); lock = lockReference; } /** Deactivate throttling, which switches the lock to be an always-acquirable NoOpLock */ public void deactivate() { assert lock != NOOP_LOCK : "throttling deactivated but not active"; lock = NOOP_LOCK; assert startOfThrottleNS > 0 : "Bad state of startOfThrottleNS"; long throttleTimeNS = System.nanoTime() - startOfThrottleNS; if (throttleTimeNS >= 0) { // Paranoia (System.nanoTime() is supposed to be monotonic): time slip may have occurred but never want to add a negative number throttleTimeMillisMetric.inc(TimeValue.nsecToMSec(throttleTimeNS)); } } long getThrottleTimeInMillis() { long currentThrottleNS = 0; if (isThrottled() && startOfThrottleNS != 0) { currentThrottleNS += System.nanoTime() - startOfThrottleNS; if (currentThrottleNS < 0) { // Paranoia (System.nanoTime() is supposed to be monotonic): time slip must have happened, have to ignore this value currentThrottleNS = 0; } } return throttleTimeMillisMetric.count() + TimeValue.nsecToMSec(currentThrottleNS); } boolean isThrottled() { return lock != NOOP_LOCK; } } /** * Returns the number of milliseconds this engine was under index throttling. */ public abstract long getIndexThrottleTimeInMillis(); /** * Returns the <code>true</code> iff this engine is currently under index throttling. * @see #getIndexThrottleTimeInMillis() */ public abstract boolean isThrottled(); /** A Lock implementation that always allows the lock to be acquired */ protected static final class NoOpLock implements Lock { @Override public void lock() { } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return true; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return true; } @Override public void unlock() { } @Override public Condition newCondition() { throw new UnsupportedOperationException("NoOpLock can't provide a condition"); } } /** * Perform document index operation on the engine * @param index operation to perform * @return {@link IndexResult} containing updated translog location, version and * document specific failures * * Note: engine level failures (i.e. persistent engine failures) are thrown */ public abstract IndexResult index(Index index) throws IOException; /** * Perform document delete operation on the engine * @param delete operation to perform * @return {@link DeleteResult} containing updated translog location, version and * document specific failures * * Note: engine level failures (i.e. persistent engine failures) are thrown */ public abstract DeleteResult delete(Delete delete) throws IOException; public abstract NoOpResult noOp(NoOp noOp); /** * Base class for index and delete operation results * Holds result meta data (e.g. translog location, updated version) * for an executed write {@link Operation} **/ public abstract static class Result { private final Operation.TYPE operationType; private final long version; private final long seqNo; private final Exception failure; private final SetOnce<Boolean> freeze = new SetOnce<>(); private Translog.Location translogLocation; private long took; protected Result(Operation.TYPE operationType, Exception failure, long version, long seqNo) { this.operationType = operationType; this.failure = failure; this.version = version; this.seqNo = seqNo; } protected Result(Operation.TYPE operationType, long version, long seqNo) { this(operationType, null, version, seqNo); } /** whether the operation had failure */ public boolean hasFailure() { return failure != null; } /** get the updated document version */ public long getVersion() { return version; } /** * Get the sequence number on the primary. * * @return the sequence number */ public long getSeqNo() { return seqNo; } /** get the translog location after executing the operation */ public Translog.Location getTranslogLocation() { return translogLocation; } /** get document failure while executing the operation {@code null} in case of no failure */ public Exception getFailure() { return failure; } /** get total time in nanoseconds */ public long getTook() { return took; } public Operation.TYPE getOperationType() { return operationType; } void setTranslogLocation(Translog.Location translogLocation) { if (freeze.get() == null) { this.translogLocation = translogLocation; } else { throw new IllegalStateException("result is already frozen"); } } void setTook(long took) { if (freeze.get() == null) { this.took = took; } else { throw new IllegalStateException("result is already frozen"); } } void freeze() { freeze.set(true); } } public static class IndexResult extends Result { private final boolean created; public IndexResult(long version, long seqNo, boolean created) { super(Operation.TYPE.INDEX, version, seqNo); this.created = created; } /** * use in case of index operation failed before getting to internal engine * (e.g while preparing operation or updating mappings) * */ public IndexResult(Exception failure, long version) { this(failure, version, SequenceNumbersService.UNASSIGNED_SEQ_NO); } public IndexResult(Exception failure, long version, long seqNo) { super(Operation.TYPE.INDEX, failure, version, seqNo); this.created = false; } public boolean isCreated() { return created; } } public static class DeleteResult extends Result { private final boolean found; public DeleteResult(long version, long seqNo, boolean found) { super(Operation.TYPE.DELETE, version, seqNo); this.found = found; } public DeleteResult(Exception failure, long version, long seqNo, boolean found) { super(Operation.TYPE.DELETE, failure, version, seqNo); this.found = found; } public boolean isFound() { return found; } } public static class NoOpResult extends Result { NoOpResult(long seqNo) { super(Operation.TYPE.NO_OP, 0, seqNo); } NoOpResult(long seqNo, Exception failure) { super(Operation.TYPE.NO_OP, failure, 0, seqNo); } } /** * Attempts to do a special commit where the given syncID is put into the commit data. The attempt * succeeds if there are not pending writes in lucene and the current point is equal to the expected one. * * @param syncId id of this sync * @param expectedCommitId the expected value of * @return true if the sync commit was made, false o.w. */ public abstract SyncedFlushResult syncFlush(String syncId, CommitId expectedCommitId) throws EngineException; public enum SyncedFlushResult { SUCCESS, COMMIT_MISMATCH, PENDING_OPERATIONS } protected final GetResult getFromSearcher(Get get, Function<String, Searcher> searcherFactory) throws EngineException { final Searcher searcher = searcherFactory.apply("get"); final DocIdAndVersion docIdAndVersion; try { docIdAndVersion = VersionsAndSeqNoResolver.loadDocIdAndVersion(searcher.reader(), get.uid()); } catch (Exception e) { Releasables.closeWhileHandlingException(searcher); //TODO: A better exception goes here throw new EngineException(shardId, "Couldn't resolve version", e); } if (docIdAndVersion != null) { if (get.versionType().isVersionConflictForReads(docIdAndVersion.version, get.version())) { Releasables.close(searcher); throw new VersionConflictEngineException(shardId, get.type(), get.id(), get.versionType().explainConflictForReads(docIdAndVersion.version, get.version())); } } if (docIdAndVersion != null) { // don't release the searcher on this path, it is the // responsibility of the caller to call GetResult.release return new GetResult(searcher, docIdAndVersion); } else { Releasables.close(searcher); return GetResult.NOT_EXISTS; } } public final GetResult get(Get get) throws EngineException { return get(get, this::acquireSearcher); } public abstract GetResult get(Get get, Function<String, Searcher> searcherFactory) throws EngineException; /** * Returns a new searcher instance. The consumer of this * API is responsible for releasing the returned searcher in a * safe manner, preferably in a try/finally block. * * @see Searcher#close() */ public final Searcher acquireSearcher(String source) throws EngineException { boolean success = false; /* Acquire order here is store -> manager since we need * to make sure that the store is not closed before * the searcher is acquired. */ store.incRef(); try { final SearcherManager manager = getSearcherManager(); // can never be null /* This might throw NPE but that's fine we will run ensureOpen() * in the catch block and throw the right exception */ final IndexSearcher searcher = manager.acquire(); try { final Searcher retVal = newSearcher(source, searcher, manager); success = true; return retVal; } finally { if (!success) { manager.release(searcher); } } } catch (AlreadyClosedException ex) { throw ex; } catch (Exception ex) { ensureOpen(); // throw EngineCloseException here if we are already closed logger.error((Supplier<?>) () -> new ParameterizedMessage("failed to acquire searcher, source {}", source), ex); throw new EngineException(shardId, "failed to acquire searcher, source " + source, ex); } finally { if (!success) { // release the ref in the case of an error... store.decRef(); } } } /** returns the translog for this engine */ public abstract Translog getTranslog(); protected void ensureOpen() { if (isClosed.get()) { throw new AlreadyClosedException(shardId + " engine is closed", failedEngine.get()); } } /** get commits stats for the last commit */ public CommitStats commitStats() { return new CommitStats(getLastCommittedSegmentInfos()); } /** get the sequence number service */ public abstract SequenceNumbersService seqNoService(); /** * Read the last segments info from the commit pointed to by the searcher manager */ protected static SegmentInfos readLastCommittedSegmentInfos(final SearcherManager sm, final Store store) throws IOException { IndexSearcher searcher = sm.acquire(); try { IndexCommit latestCommit = ((DirectoryReader) searcher.getIndexReader()).getIndexCommit(); return Lucene.readSegmentInfos(latestCommit); } catch (IOException e) { // Fall back to reading from the store if reading from the commit fails try { return store.readLastCommittedSegmentsInfo(); } catch (IOException e2) { e2.addSuppressed(e); throw e2; } } finally { sm.release(searcher); } } /** * Global stats on segments. */ public final SegmentsStats segmentsStats(boolean includeSegmentFileSizes) { ensureOpen(); try (Searcher searcher = acquireSearcher("segments_stats")) { SegmentsStats stats = new SegmentsStats(); for (LeafReaderContext reader : searcher.reader().leaves()) { final SegmentReader segmentReader = segmentReader(reader.reader()); stats.add(1, segmentReader.ramBytesUsed()); stats.addTermsMemoryInBytes(guardedRamBytesUsed(segmentReader.getPostingsReader())); stats.addStoredFieldsMemoryInBytes(guardedRamBytesUsed(segmentReader.getFieldsReader())); stats.addTermVectorsMemoryInBytes(guardedRamBytesUsed(segmentReader.getTermVectorsReader())); stats.addNormsMemoryInBytes(guardedRamBytesUsed(segmentReader.getNormsReader())); stats.addPointsMemoryInBytes(guardedRamBytesUsed(segmentReader.getPointsReader())); stats.addDocValuesMemoryInBytes(guardedRamBytesUsed(segmentReader.getDocValuesReader())); if (includeSegmentFileSizes) { // TODO: consider moving this to StoreStats stats.addFileSizes(getSegmentFileSizes(segmentReader)); } } writerSegmentStats(stats); return stats; } } private ImmutableOpenMap<String, Long> getSegmentFileSizes(SegmentReader segmentReader) { Directory directory = null; SegmentCommitInfo segmentCommitInfo = segmentReader.getSegmentInfo(); boolean useCompoundFile = segmentCommitInfo.info.getUseCompoundFile(); if (useCompoundFile) { try { directory = engineConfig.getCodec().compoundFormat().getCompoundReader(segmentReader.directory(), segmentCommitInfo.info, IOContext.READ); } catch (IOException e) { logger.warn((Supplier<?>) () -> new ParameterizedMessage("Error when opening compound reader for Directory [{}] and SegmentCommitInfo [{}]", segmentReader.directory(), segmentCommitInfo), e); return ImmutableOpenMap.of(); } } else { directory = segmentReader.directory(); } assert directory != null; String[] files; if (useCompoundFile) { try { files = directory.listAll(); } catch (IOException e) { final Directory finalDirectory = directory; logger.warn( (Supplier<?>) () -> new ParameterizedMessage("Couldn't list Compound Reader Directory [{}]", finalDirectory), e); return ImmutableOpenMap.of(); } } else { try { files = segmentReader.getSegmentInfo().files().toArray(new String[]{}); } catch (IOException e) { logger.warn((Supplier<?>) () -> new ParameterizedMessage("Couldn't list Directory from SegmentReader [{}] and SegmentInfo [{}]", segmentReader, segmentReader.getSegmentInfo()), e); return ImmutableOpenMap.of(); } } ImmutableOpenMap.Builder<String, Long> map = ImmutableOpenMap.builder(); for (String file : files) { String extension = IndexFileNames.getExtension(file); long length = 0L; try { length = directory.fileLength(file); } catch (NoSuchFileException | FileNotFoundException e) { final Directory finalDirectory = directory; logger.warn((Supplier<?>) () -> new ParameterizedMessage("Tried to query fileLength but file is gone [{}] [{}]", finalDirectory, file), e); } catch (IOException e) { final Directory finalDirectory = directory; logger.warn( (Supplier<?>) () -> new ParameterizedMessage("Error when trying to query fileLength [{}] [{}]", finalDirectory, file), e); } if (length == 0L) { continue; } map.put(extension, length); } if (useCompoundFile && directory != null) { try { directory.close(); } catch (IOException e) { final Directory finalDirectory = directory; logger.warn( (Supplier<?>) () -> new ParameterizedMessage("Error when closing compound reader on Directory [{}]", finalDirectory), e); } } return map.build(); } protected void writerSegmentStats(SegmentsStats stats) { // by default we don't have a writer here... subclasses can override this stats.addVersionMapMemoryInBytes(0); stats.addIndexWriterMemoryInBytes(0); } /** How much heap is used that would be freed by a refresh. Note that this may throw {@link AlreadyClosedException}. */ public abstract long getIndexBufferRAMBytesUsed(); protected Segment[] getSegmentInfo(SegmentInfos lastCommittedSegmentInfos, boolean verbose) { ensureOpen(); Map<String, Segment> segments = new HashMap<>(); // first, go over and compute the search ones... Searcher searcher = acquireSearcher("segments"); try { for (LeafReaderContext reader : searcher.reader().leaves()) { SegmentCommitInfo info = segmentReader(reader.reader()).getSegmentInfo(); assert !segments.containsKey(info.info.name); Segment segment = new Segment(info.info.name); segment.search = true; segment.docCount = reader.reader().numDocs(); segment.delDocCount = reader.reader().numDeletedDocs(); segment.version = info.info.getVersion(); segment.compound = info.info.getUseCompoundFile(); try { segment.sizeInBytes = info.sizeInBytes(); } catch (IOException e) { logger.trace((Supplier<?>) () -> new ParameterizedMessage("failed to get size for [{}]", info.info.name), e); } final SegmentReader segmentReader = segmentReader(reader.reader()); segment.memoryInBytes = segmentReader.ramBytesUsed(); segment.segmentSort = info.info.getIndexSort(); if (verbose) { segment.ramTree = Accountables.namedAccountable("root", segmentReader); } // TODO: add more fine grained mem stats values to per segment info here segments.put(info.info.name, segment); } } finally { searcher.close(); } // now, correlate or add the committed ones... if (lastCommittedSegmentInfos != null) { SegmentInfos infos = lastCommittedSegmentInfos; for (SegmentCommitInfo info : infos) { Segment segment = segments.get(info.info.name); if (segment == null) { segment = new Segment(info.info.name); segment.search = false; segment.committed = true; segment.docCount = info.info.maxDoc(); segment.delDocCount = info.getDelCount(); segment.version = info.info.getVersion(); segment.compound = info.info.getUseCompoundFile(); try { segment.sizeInBytes = info.sizeInBytes(); } catch (IOException e) { logger.trace((Supplier<?>) () -> new ParameterizedMessage("failed to get size for [{}]", info.info.name), e); } segments.put(info.info.name, segment); } else { segment.committed = true; } } } Segment[] segmentsArr = segments.values().toArray(new Segment[segments.values().size()]); Arrays.sort(segmentsArr, new Comparator<Segment>() { @Override public int compare(Segment o1, Segment o2) { return (int) (o1.getGeneration() - o2.getGeneration()); } }); return segmentsArr; } /** * The list of segments in the engine. */ public abstract List<Segment> segments(boolean verbose); public final boolean refreshNeeded() { if (store.tryIncRef()) { /* we need to inc the store here since searcherManager.isSearcherCurrent() acquires a searcher internally and that might keep a file open on the store. this violates the assumption that all files are closed when the store is closed so we need to make sure we increment it here */ try { return getSearcherManager().isSearcherCurrent() == false; } catch (IOException e) { logger.error("failed to access searcher manager", e); failEngine("failed to access searcher manager", e); throw new EngineException(shardId, "failed to access searcher manager", e); } finally { store.decRef(); } } return false; } /** * Synchronously refreshes the engine for new search operations to reflect the latest * changes. */ @Nullable public abstract void refresh(String source) throws EngineException; /** * Called when our engine is using too much heap and should move buffered indexed/deleted documents to disk. */ // NOTE: do NOT rename this to something containing flush or refresh! public abstract void writeIndexingBuffer() throws EngineException; /** * Flushes the state of the engine including the transaction log, clearing memory. * * @param force if <code>true</code> a lucene commit is executed even if no changes need to be committed. * @param waitIfOngoing if <code>true</code> this call will block until all currently running flushes have finished. * Otherwise this call will return without blocking. * @return the commit Id for the resulting commit */ public abstract CommitId flush(boolean force, boolean waitIfOngoing) throws EngineException; /** * Flushes the state of the engine including the transaction log, clearing memory and persisting * documents in the lucene index to disk including a potentially heavy and durable fsync operation. * This operation is not going to block if another flush operation is currently running and won't write * a lucene commit if nothing needs to be committed. * * @return the commit Id for the resulting commit */ public abstract CommitId flush() throws EngineException; /** * Force merges to 1 segment */ public void forceMerge(boolean flush) throws IOException { forceMerge(flush, 1, false, false, false); } /** * Triggers a forced merge on this engine */ public abstract void forceMerge(boolean flush, int maxNumSegments, boolean onlyExpungeDeletes, boolean upgrade, boolean upgradeOnlyAncientSegments) throws EngineException, IOException; /** * Snapshots the index and returns a handle to it. If needed will try and "commit" the * lucene index to make sure we have a "fresh" copy of the files to snapshot. * * @param flushFirst indicates whether the engine should flush before returning the snapshot */ public abstract IndexCommit acquireIndexCommit(boolean flushFirst) throws EngineException; /** * fail engine due to some error. the engine will also be closed. * The underlying store is marked corrupted iff failure is caused by index corruption */ public void failEngine(String reason, @Nullable Exception failure) { if (failEngineLock.tryLock()) { store.incRef(); try { if (failedEngine.get() != null) { logger.warn((Supplier<?>) () -> new ParameterizedMessage("tried to fail engine but engine is already failed. ignoring. [{}]", reason), failure); return; } // this must happen before we close IW or Translog such that we can check this state to opt out of failing the engine // again on any caught AlreadyClosedException failedEngine.set((failure != null) ? failure : new IllegalStateException(reason)); try { // we just go and close this engine - no way to recover closeNoLock("engine failed on: [" + reason + "]"); } finally { logger.warn((Supplier<?>) () -> new ParameterizedMessage("failed engine [{}]", reason), failure); // we must set a failure exception, generate one if not supplied // we first mark the store as corrupted before we notify any listeners // this must happen first otherwise we might try to reallocate so quickly // on the same node that we don't see the corrupted marker file when // the shard is initializing if (Lucene.isCorruptionException(failure)) { try { store.markStoreCorrupted(new IOException("failed engine (reason: [" + reason + "])", ExceptionsHelper.unwrapCorruption(failure))); } catch (IOException e) { logger.warn("Couldn't mark store corrupted", e); } } eventListener.onFailedEngine(reason, failure); } } catch (Exception inner) { if (failure != null) inner.addSuppressed(failure); // don't bubble up these exceptions up logger.warn("failEngine threw exception", inner); } finally { store.decRef(); } } else { logger.debug((Supplier<?>) () -> new ParameterizedMessage("tried to fail engine but could not acquire lock - engine should be failed by now [{}]", reason), failure); } } /** Check whether the engine should be failed */ protected boolean maybeFailEngine(String source, Exception e) { if (Lucene.isCorruptionException(e)) { failEngine("corrupt file (source: [" + source + "])", e); return true; } return false; } public interface EventListener { /** * Called when a fatal exception occurred */ default void onFailedEngine(String reason, @Nullable Exception e) { } } public static class Searcher implements Releasable { private final String source; private final IndexSearcher searcher; public Searcher(String source, IndexSearcher searcher) { this.source = source; this.searcher = searcher; } /** * The source that caused this searcher to be acquired. */ public String source() { return source; } public IndexReader reader() { return searcher.getIndexReader(); } public DirectoryReader getDirectoryReader() { if (reader() instanceof DirectoryReader) { return (DirectoryReader) reader(); } throw new IllegalStateException("Can't use " + reader().getClass() + " as a directory reader"); } public IndexSearcher searcher() { return searcher; } @Override public void close() { // Nothing to close here } } public abstract static class Operation { /** type of operation (index, delete), subclasses use static types */ public enum TYPE { INDEX, DELETE, NO_OP; private final String lowercase; TYPE() { this.lowercase = this.toString().toLowerCase(Locale.ROOT); } public String getLowercase() { return lowercase; } } private final Term uid; private final long version; private final long seqNo; private final long primaryTerm; private final VersionType versionType; private final Origin origin; private final long startTime; public Operation(Term uid, long seqNo, long primaryTerm, long version, VersionType versionType, Origin origin, long startTime) { this.uid = uid; this.seqNo = seqNo; this.primaryTerm = primaryTerm; this.version = version; this.versionType = versionType; this.origin = origin; this.startTime = startTime; } public enum Origin { PRIMARY, REPLICA, PEER_RECOVERY, LOCAL_TRANSLOG_RECOVERY; public boolean isRecovery() { return this == PEER_RECOVERY || this == LOCAL_TRANSLOG_RECOVERY; } } public Origin origin() { return this.origin; } public Term uid() { return this.uid; } public long version() { return this.version; } public long seqNo() { return seqNo; } public long primaryTerm() { return primaryTerm; } public abstract int estimatedSizeInBytes(); public VersionType versionType() { return this.versionType; } /** * Returns operation start time in nanoseconds. */ public long startTime() { return this.startTime; } public abstract String type(); abstract String id(); abstract TYPE operationType(); } public static class Index extends Operation { private final ParsedDocument doc; private final long autoGeneratedIdTimestamp; private final boolean isRetry; public Index(Term uid, ParsedDocument doc, long seqNo, long primaryTerm, long version, VersionType versionType, Origin origin, long startTime, long autoGeneratedIdTimestamp, boolean isRetry) { super(uid, seqNo, primaryTerm, version, versionType, origin, startTime); this.doc = doc; this.isRetry = isRetry; this.autoGeneratedIdTimestamp = autoGeneratedIdTimestamp; } public Index(Term uid, ParsedDocument doc) { this(uid, doc, Versions.MATCH_ANY); } // TEST ONLY Index(Term uid, ParsedDocument doc, long version) { // use a primary term of 2 to allow tests to reduce it to a valid >0 term this(uid, doc, SequenceNumbersService.UNASSIGNED_SEQ_NO, 2, version, VersionType.INTERNAL, Origin.PRIMARY, System.nanoTime(), -1, false); } // TEST ONLY public ParsedDocument parsedDoc() { return this.doc; } @Override public String type() { return this.doc.type(); } @Override public String id() { return this.doc.id(); } @Override TYPE operationType() { return TYPE.INDEX; } public String routing() { return this.doc.routing(); } public String parent() { return this.doc.parent(); } public List<Document> docs() { return this.doc.docs(); } public BytesReference source() { return this.doc.source(); } @Override public int estimatedSizeInBytes() { return (id().length() + type().length()) * 2 + source().length() + 12; } /** * Returns a positive timestamp if the ID of this document is auto-generated by elasticsearch. * if this property is non-negative indexing code might optimize the addition of this document * due to it's append only nature. */ public long getAutoGeneratedIdTimestamp() { return autoGeneratedIdTimestamp; } /** * Returns <code>true</code> if this index requests has been retried on the coordinating node and can therefor be delivered * multiple times. Note: this might also be set to true if an equivalent event occurred like the replay of the transaction log */ public boolean isRetry() { return isRetry; } } public static class Delete extends Operation { private final String type; private final String id; public Delete(String type, String id, Term uid, long seqNo, long primaryTerm, long version, VersionType versionType, Origin origin, long startTime) { super(uid, seqNo, primaryTerm, version, versionType, origin, startTime); this.type = type; this.id = id; } public Delete(String type, String id, Term uid) { this(type, id, uid, SequenceNumbersService.UNASSIGNED_SEQ_NO, 0, Versions.MATCH_ANY, VersionType.INTERNAL, Origin.PRIMARY, System.nanoTime()); } public Delete(Delete template, VersionType versionType) { this(template.type(), template.id(), template.uid(), template.seqNo(), template.primaryTerm(), template.version(), versionType, template.origin(), template.startTime()); } @Override public String type() { return this.type; } @Override public String id() { return this.id; } @Override TYPE operationType() { return TYPE.DELETE; } @Override public int estimatedSizeInBytes() { return (uid().field().length() + uid().text().length()) * 2 + 20; } } public static class NoOp extends Operation { private final String reason; public String reason() { return reason; } public NoOp(final long seqNo, final long primaryTerm, final Origin origin, final long startTime, final String reason) { super(null, seqNo, primaryTerm, Versions.NOT_FOUND, null, origin, startTime); this.reason = reason; } @Override public Term uid() { throw new UnsupportedOperationException(); } @Override public String type() { throw new UnsupportedOperationException(); } @Override public long version() { throw new UnsupportedOperationException(); } @Override public VersionType versionType() { throw new UnsupportedOperationException(); } @Override String id() { throw new UnsupportedOperationException(); } @Override TYPE operationType() { return TYPE.NO_OP; } @Override public int estimatedSizeInBytes() { return 2 * reason.length() + 2 * Long.BYTES; } } public static class Get { private final boolean realtime; private final Term uid; private final String type, id; private long version = Versions.MATCH_ANY; private VersionType versionType = VersionType.INTERNAL; public Get(boolean realtime, String type, String id, Term uid) { this.realtime = realtime; this.type = type; this.id = id; this.uid = uid; } public boolean realtime() { return this.realtime; } public String type() { return type; } public String id() { return id; } public Term uid() { return uid; } public long version() { return version; } public Get version(long version) { this.version = version; return this; } public VersionType versionType() { return versionType; } public Get versionType(VersionType versionType) { this.versionType = versionType; return this; } } public static class GetResult implements Releasable { private final boolean exists; private final long version; private final DocIdAndVersion docIdAndVersion; private final Searcher searcher; public static final GetResult NOT_EXISTS = new GetResult(false, Versions.NOT_FOUND, null, null); private GetResult(boolean exists, long version, DocIdAndVersion docIdAndVersion, Searcher searcher) { this.exists = exists; this.version = version; this.docIdAndVersion = docIdAndVersion; this.searcher = searcher; } /** * Build a non-realtime get result from the searcher. */ public GetResult(Searcher searcher, DocIdAndVersion docIdAndVersion) { this(true, docIdAndVersion.version, docIdAndVersion, searcher); } public boolean exists() { return exists; } public long version() { return this.version; } public Searcher searcher() { return this.searcher; } public DocIdAndVersion docIdAndVersion() { return docIdAndVersion; } @Override public void close() { release(); } public void release() { Releasables.close(searcher); } } protected abstract SearcherManager getSearcherManager(); /** * Method to close the engine while the write lock is held. */ protected abstract void closeNoLock(String reason); /** * Flush the engine (committing segments to disk and truncating the * translog) and close it. */ public void flushAndClose() throws IOException { if (isClosed.get() == false) { logger.trace("flushAndClose now acquire writeLock"); try (ReleasableLock lock = writeLock.acquire()) { logger.trace("flushAndClose now acquired writeLock"); try { logger.debug("flushing shard on close - this might take some time to sync files to disk"); try { flush(); // TODO we might force a flush in the future since we have the write lock already even though recoveries are running. } catch (AlreadyClosedException ex) { logger.debug("engine already closed - skipping flushAndClose"); } } finally { close(); // double close is not a problem } } } } @Override public void close() throws IOException { if (isClosed.get() == false) { // don't acquire the write lock if we are already closed logger.debug("close now acquiring writeLock"); try (ReleasableLock lock = writeLock.acquire()) { logger.debug("close acquired writeLock"); closeNoLock("api"); } } } public static class CommitId implements Writeable { private final byte[] id; public CommitId(byte[] id) { assert id != null; this.id = Arrays.copyOf(id, id.length); } /** * Read from a stream. */ public CommitId(StreamInput in) throws IOException { assert in != null; this.id = in.readByteArray(); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeByteArray(id); } @Override public String toString() { return Base64.getEncoder().encodeToString(id); } public boolean idsEqual(byte[] id) { return Arrays.equals(id, this.id); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } CommitId commitId = (CommitId) o; if (!Arrays.equals(id, commitId.id)) { return false; } return true; } @Override public int hashCode() { return Arrays.hashCode(id); } } public void onSettingsChanged() { } /** * Returns the timestamp of the last write in nanoseconds. * Note: this time might not be absolutely accurate since the {@link Operation#startTime()} is used which might be * slightly inaccurate. * * @see System#nanoTime() * @see Operation#startTime() */ public long getLastWriteNanos() { return this.lastWriteNanos; } /** * Called for each new opened engine searcher to warm new segments * * @see EngineConfig#getWarmer() */ public interface Warmer { /** * Called once a new Searcher is opened on the top-level searcher. */ void warm(Engine.Searcher searcher); } /** * Request that this engine throttle incoming indexing requests to one thread. Must be matched by a later call to {@link #deactivateThrottling()}. */ public abstract void activateThrottling(); /** * Reverses a previous {@link #activateThrottling} call. */ public abstract void deactivateThrottling(); /** * Fills up the local checkpoints history with no-ops until the local checkpoint * and the max seen sequence ID are identical. * @param primaryTerm the shards primary term this engine was created for * @return the number of no-ops added */ public abstract int fillSeqNoGaps(long primaryTerm) throws IOException; /** * Performs recovery from the transaction log. * This operation will close the engine if the recovery fails. */ public abstract Engine recoverFromTranslog() throws IOException; /** * Returns <code>true</code> iff this engine is currently recovering from translog. */ public boolean isRecovering() { return false; } }