/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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.apache.jackrabbit.core.query.lucene; import java.io.IOException; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.jcr.RepositoryException; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; import org.apache.jackrabbit.core.state.ChildNodeEntry; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.core.state.ItemStateManager; import org.apache.jackrabbit.core.state.NoSuchItemStateException; import org.apache.jackrabbit.core.state.NodeState; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.PathFactory; import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; import org.apache.jackrabbit.spi.commons.conversion.PathResolver; import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.store.Directory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A <code>MultiIndex</code> consists of a {@link VolatileIndex} and multiple * {@link PersistentIndex}es. The goal is to keep most parts of the index open * with index readers and write new index data to the volatile index. When * the volatile index reaches a certain size (see {@link SearchIndex#setMinMergeDocs(int)}) * a new persistent index is created with the index data from the volatile index, * the same happens when the volatile index has been idle for some time (see * {@link SearchIndex#setVolatileIdleTime(int)}). * The new persistent index is then added to the list of already existing * persistent indexes. Further operations on the new persistent index will * however only require an <code>IndexReader</code> which serves for queries * but also for delete operations on the index. * <p> * The persistent indexes are merged from time to time. The merge behaviour * is configurable using the methods: {@link SearchIndex#setMaxMergeDocs(int)}, * {@link SearchIndex#setMergeFactor(int)} and {@link SearchIndex#setMinMergeDocs(int)}. * For detailed description of the configuration parameters see also the lucene * <code>IndexWriter</code> class. * <p> * This class is thread-safe. * <p> * Note on implementation: Multiple modifying threads are synchronized on a * <code>MultiIndex</code> instance itself. Synchronization between a modifying * thread and reader threads is done using {@link #updateMonitor} and * {@link #updateInProgress}. */ public class MultiIndex { /** * The logger instance for this class */ private static final Logger log = LoggerFactory.getLogger(MultiIndex.class); /** * A path factory. */ private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); /** * Names of active persistent index directories. */ private final IndexInfos indexNames; /** * The history of the multi index. */ private final IndexHistory indexHistory; /** * Names of index directories that can be deleted. * Key = index name (String), Value = time when last in use (Long) */ private final Map<String, Long> deletable = new HashMap<String, Long>(); /** * List of open persistent indexes. This list may also contain an open * PersistentIndex owned by the IndexMerger daemon. Such an index is not * registered with indexNames and <b>must not</b> be used in regular index * operations (delete node, etc.)! */ private final List<PersistentIndex> indexes = new ArrayList<PersistentIndex>(); /** * The internal namespace mappings of the query manager. */ private final NamespaceMappings nsMappings; /** * The directory manager. */ private final DirectoryManager directoryManager; /** * The redo log factory */ private final RedoLogFactory redoLogFactory; /** * The base directory to store the index. */ private final Directory indexDir; /** * The query handler */ private final SearchIndex handler; /** * The volatile index. */ private VolatileIndex volatileIndex; /** * Flag indicating whether an update operation is in progress. */ private boolean updateInProgress = false; /** * If not <code>null</code> points to a valid <code>IndexReader</code> that * reads from all indexes, including volatile and persistent indexes. */ private CachingMultiIndexReader multiReader; /** * Shared document number cache across all persistent indexes. */ private final DocNumberCache cache; /** * Monitor to use to synchronize access to {@link #multiReader} and * {@link #updateInProgress}. */ private final Object updateMonitor = new Object(); /** * <code>true</code> if the redo log contained entries on startup. */ private boolean redoLogApplied = false; /** * The time this index was last flushed or a transaction was committed. */ private long lastFlushTime = 0; /** * The <code>IndexMerger</code> for this <code>MultiIndex</code>. */ private final IndexMerger merger; /** * Task that is periodically called by the repository timer for checking * if index should be flushed. */ private ScheduledFuture<?> flushTask = null; /** * The RedoLog of this <code>MultiIndex</code>. */ private RedoLog redoLog; /** * The indexing queue with pending text extraction jobs. */ private IndexingQueue indexingQueue; /** * Only used for testing purpose. Set to <code>true</code> after finished * extraction jobs have been removed from the queue and set to * <code>false</code> again after the affected nodes have been updated in * the index. */ private boolean indexingQueueCommitPending; /** * Identifiers of nodes that should not be indexed. */ private final Set<NodeId> excludedIDs; /** * The next transaction id. */ private long nextTransactionId = 0; /** * The current transaction id. */ private long currentTransactionId = -1; /** * Flag indicating whether re-indexing is running. */ private boolean reindexing = false; /** * The index format version of this multi index. */ private final IndexFormatVersion version; /** * Creates a new MultiIndex. * * @param handler the search handler * @param excludedIDs identifiers of nodes that should * neither be indexed nor further traversed * @throws IOException if an error occurs */ MultiIndex(SearchIndex handler, Set<NodeId> excludedIDs) throws IOException { this.directoryManager = handler.getDirectoryManager(); this.redoLogFactory = handler.getRedoLogFactory(); this.indexDir = directoryManager.getDirectory("."); this.handler = handler; this.cache = new DocNumberCache(handler.getCacheSize()); this.excludedIDs = new HashSet<NodeId>(excludedIDs); this.nsMappings = handler.getNamespaceMappings(); indexNames = new IndexInfos(indexDir, "indexes"); this.indexHistory = new IndexHistory(indexDir, handler.getMaxHistoryAge() * 1000); // as of 1.5 deletable file is not used anymore removeDeletable(); this.redoLog = redoLogFactory.createRedoLog(this); // initialize IndexMerger merger = new IndexMerger(this, handler.getContext().getExecutor()); merger.setMaxMergeDocs(handler.getMaxMergeDocs()); merger.setMergeFactor(handler.getMergeFactor()); merger.setMinMergeDocs(handler.getMinMergeDocs()); // initialize indexing queue this.indexingQueue = new IndexingQueue(new IndexingQueueStore(indexDir)); // open persistent indexes Iterator<IndexInfo> iterator = indexNames.iterator(); while (iterator.hasNext()) { IndexInfo info = iterator.next(); String name = info.getName(); // only open if it still exists // it is possible that indexNames still contains a name for // an index that has been deleted, but indexNames has not been // written to disk. if (!directoryManager.hasDirectory(name)) { log.debug("index does not exist anymore: " + name); // move on to next index continue; } PersistentIndex index = new PersistentIndex(name, handler.getTextAnalyzer(), handler.getSimilarity(), cache, indexingQueue, directoryManager, handler.getMaxHistoryAge()); index.setUseCompoundFile(handler.getUseCompoundFile()); index.setTermInfosIndexDivisor(handler.getTermInfosIndexDivisor()); indexes.add(index); merger.indexAdded(index.getName(), index.getNumDocuments()); } // init volatile index resetVolatileIndex(); // set index format version and at the same time // initialize hierarchy cache if requested. CachingMultiIndexReader reader = getIndexReader(handler.isInitializeHierarchyCache()); try { version = IndexFormatVersion.getVersion(reader); } finally { reader.release(); } indexingQueue.initialize(this); redoLogApplied = redoLog.hasEntries(); // run recovery Recovery.run(this, redoLog); // enqueue unused segments for deletion enqueueUnusedSegments(); attemptDelete(); // now that we are ready, start index merger merger.start(); if (redoLogApplied) { // wait for the index merge to finish pending jobs try { merger.waitUntilIdle(); } catch (InterruptedException e) { // move on } flush(); } if (indexNames.size() > 0) { scheduleFlushTask(); } } /** * Returns the number of documents in this index. * * @return the number of documents in this index. * @throws IOException if an error occurs while reading from the index. */ int numDocs() throws IOException { if (indexNames.size() == 0) { return volatileIndex.getNumDocuments(); } else { CachingMultiIndexReader reader = getIndexReader(); try { return reader.numDocs(); } finally { reader.release(); } } } /** * @return the index format version for this multi index. */ IndexFormatVersion getIndexFormatVersion() { return version; } /** * Creates an initial index by traversing the node hierarchy starting at the * node with <code>rootId</code>. * * @param stateMgr the item state manager. * @param rootId the id of the node from where to start. * @param rootPath the path of the node from where to start. * @throws IOException if an error occurs while indexing the * workspace. * @throws IllegalStateException if this index is not empty. */ void createInitialIndex(ItemStateManager stateMgr, NodeId rootId, Path rootPath) throws IOException { // only do an initial index if there are no indexes at all if (indexNames.size() == 0) { reindexing = true; try { long count = 0; // traverse and index workspace executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); NodeState rootState = (NodeState) stateMgr.getItemState(rootId); count = createIndex(rootState, rootPath, stateMgr, count); checkIndexingQueue(true); executeAndLog(new Commit(getTransactionId())); log.debug("Created initial index for {} nodes", count); releaseMultiReader(); safeFlush(); } catch (Exception e) { String msg = "Error indexing workspace"; IOException ex = new IOException(msg); ex.initCause(e); throw ex; } finally { reindexing = false; scheduleFlushTask(); } } else { throw new IllegalStateException("Index already present"); } } /** * Atomically updates the index by removing some documents and adding * others. * * @param remove collection of <code>id</code>s that identify documents to * remove * @param add collection of <code>Document</code>s to add. Some of the * elements in this collection may be <code>null</code>, to * indicate that a node could not be indexed successfully. * @throws IOException if an error occurs while updating the index. */ synchronized void update( Collection<NodeId> remove, Collection<Document> add) throws IOException { // make sure a reader is available during long updates if (add.size() > handler.getBufferSize()) { try { getIndexReader().release(); } catch (IOException e) { // do not fail if an exception is thrown here log.warn("unable to prepare index reader for queries during update", e); } } synchronized (updateMonitor) { updateInProgress = true; } try { long transactionId = nextTransactionId++; executeAndLog(new Start(transactionId)); for (NodeId id : remove) { executeAndLog(new DeleteNode(transactionId, id)); } for (Document document : add) { if (document != null) { executeAndLog(new AddNode(transactionId, document)); // commit volatile index if needed checkVolatileCommit(); } } executeAndLog(new Commit(transactionId)); } finally { synchronized (updateMonitor) { updateInProgress = false; updateMonitor.notifyAll(); releaseMultiReader(); } } } /** * Adds a document to the index. * * @param doc the document to add. * @throws IOException if an error occurs while adding the document to the * index. */ void addDocument(Document doc) throws IOException { Collection<NodeId> empty = Collections.emptyList(); update(empty, Collections.singleton(doc)); } /** * Deletes the first document that matches the <code>id</code>. * * @param id document that match this <code>id</code> will be deleted. * @throws IOException if an error occurs while deleting the document. */ void removeDocument(NodeId id) throws IOException { Collection<Document> empty = Collections.emptyList(); update(Collections.singleton(id), empty); } /** * Deletes all documents that match the <code>id</code>. * * @param id documents that match this <code>id</code> will be deleted. * @return the number of deleted documents. * @throws IOException if an error occurs while deleting documents. */ synchronized int removeAllDocuments(NodeId id) throws IOException { synchronized (updateMonitor) { updateInProgress = true; } int num; try { Term idTerm = TermFactory.createUUIDTerm(id.toString()); executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); num = volatileIndex.removeDocument(idTerm); if (num > 0) { redoLog.append(new DeleteNode(getTransactionId(), id)); } for (PersistentIndex index : indexes) { // only remove documents from registered indexes if (indexNames.contains(index.getName())) { int removed = index.removeDocument(idTerm); if (removed > 0) { redoLog.append(new DeleteNode(getTransactionId(), id)); } num += removed; } } executeAndLog(new Commit(getTransactionId())); } finally { synchronized (updateMonitor) { updateInProgress = false; updateMonitor.notifyAll(); releaseMultiReader(); } } return num; } /** * Returns <code>IndexReader</code>s for the indexes named * <code>indexNames</code>. An <code>IndexListener</code> is registered and * notified when documents are deleted from one of the indexes in * <code>indexNames</code>. * <p> * Note: the number of <code>IndexReaders</code> returned by this method is * not necessarily the same as the number of index names passed. An index * might have been deleted and is not reachable anymore. * * @param indexNames the names of the indexes for which to obtain readers. * @param listener the listener to notify when documents are deleted. * @return the <code>IndexReaders</code>. * @throws IOException if an error occurs acquiring the index readers. */ synchronized IndexReader[] getIndexReaders( String[] indexNames, IndexListener listener) throws IOException { Set<String> names = new HashSet<String>(Arrays.asList(indexNames)); Map<ReadOnlyIndexReader, PersistentIndex> indexReaders = new HashMap<ReadOnlyIndexReader, PersistentIndex>(); try { for (PersistentIndex index : indexes) { if (names.contains(index.getName())) { indexReaders.put(index.getReadOnlyIndexReader(listener), index); } } } catch (IOException e) { // release readers obtained so far for (Map.Entry<ReadOnlyIndexReader, PersistentIndex> entry : indexReaders.entrySet()) { try { entry.getKey().release(); } catch (IOException ex) { log.warn("Exception releasing index reader", ex); } entry.getValue().resetListener(); } throw e; } return indexReaders.keySet().toArray(new IndexReader[indexReaders.size()]); } /** * Creates a new Persistent index. The new index is not registered with this * <code>MultiIndex</code>. * * @param indexName the name of the index to open, or <code>null</code> if * an index with a new name should be created. * @return a new <code>PersistentIndex</code>. * @throws IOException if a new index cannot be created. */ synchronized PersistentIndex getOrCreateIndex(String indexName) throws IOException { // check existing for (PersistentIndex idx : indexes) { if (idx.getName().equals(indexName)) { return idx; } } // otherwise open / create it if (indexName == null) { do { indexName = indexNames.newName(); } while (directoryManager.hasDirectory(indexName)); } PersistentIndex index; try { index = new PersistentIndex(indexName, handler.getTextAnalyzer(), handler.getSimilarity(), cache, indexingQueue, directoryManager, handler.getMaxHistoryAge()); } catch (IOException e) { // do some clean up if (!directoryManager.delete(indexName)) { deletable.put(indexName, Long.MIN_VALUE); } throw e; } index.setUseCompoundFile(handler.getUseCompoundFile()); index.setTermInfosIndexDivisor(handler.getTermInfosIndexDivisor()); // add to list of open indexes and return it indexes.add(index); return index; } /** * Returns <code>true</code> if this multi index has an index segment with * the given name. This method even returns <code>true</code> if an index * segments has not yet been loaded / initialized but exists on disk. * * @param indexName the name of the index segment. * @return <code>true</code> if it exists; otherwise <code>false</code>. * @throws IOException if an error occurs while checking existence of * directory. */ synchronized boolean hasIndex(String indexName) throws IOException { // check existing for (PersistentIndex idx : indexes) { if (idx.getName().equals(indexName)) { return true; } } // check if it exists on disk return directoryManager.hasDirectory(indexName); } /** * Replaces the indexes with names <code>obsoleteIndexes</code> with * <code>index</code>. Documents that must be deleted in <code>index</code> * can be identified with <code>Term</code>s in <code>deleted</code>. * * @param obsoleteIndexes the names of the indexes to replace. * @param index the new index that is the result of a merge of the * indexes to replace. * @param deleted <code>Term</code>s that identify documents that must be * deleted in <code>index</code>. * @throws IOException if an exception occurs while replacing the indexes. */ void replaceIndexes(String[] obsoleteIndexes, PersistentIndex index, Collection<Term> deleted) throws IOException { if (handler.isInitializeHierarchyCache()) { // force initializing of caches long time = System.currentTimeMillis(); index.getReadOnlyIndexReader(true).release(); time = System.currentTimeMillis() - time; log.debug("hierarchy cache initialized in {} ms", time); } synchronized (this) { synchronized (updateMonitor) { updateInProgress = true; } try { // if we are reindexing there is already an active transaction if (!reindexing) { executeAndLog(new Start(Action.INTERNAL_TRANS_REPL_INDEXES)); } // delete obsolete indexes Set<String> names = new HashSet<String>(Arrays.asList(obsoleteIndexes)); for (String indexName : names) { // do not try to delete indexes that are already gone if (indexNames.contains(indexName)) { executeAndLog(new DeleteIndex(getTransactionId(), indexName)); } } // Index merger does not log an action when it creates the target // index of the merge. We have to do this here. executeAndLog(new CreateIndex(getTransactionId(), index.getName())); executeAndLog(new AddIndex(getTransactionId(), index.getName())); // delete documents in index for (Term id : deleted) { index.removeDocument(id); } index.commit(); if (!reindexing) { // only commit if we are not reindexing // when reindexing the final commit is done at the very end executeAndLog(new Commit(getTransactionId())); } } finally { synchronized (updateMonitor) { updateInProgress = false; updateMonitor.notifyAll(); releaseMultiReader(); } } } if (reindexing) { // do some cleanup right away when reindexing attemptDelete(); } } /** * Returns an read-only <code>IndexReader</code> that spans alls indexes of this * <code>MultiIndex</code>. * * @return an <code>IndexReader</code>. * @throws IOException if an error occurs constructing the <code>IndexReader</code>. */ public CachingMultiIndexReader getIndexReader() throws IOException { return getIndexReader(false); } /** * Returns an read-only <code>IndexReader</code> that spans alls indexes of this * <code>MultiIndex</code>. * * @param initCache when set <code>true</code> the hierarchy cache is * completely initialized before this call returns. * @return an <code>IndexReader</code>. * @throws IOException if an error occurs constructing the <code>IndexReader</code>. */ public synchronized CachingMultiIndexReader getIndexReader(boolean initCache) throws IOException { synchronized (updateMonitor) { if (multiReader != null) { multiReader.acquire(); return multiReader; } // no reader available // wait until no update is in progress while (updateInProgress) { try { updateMonitor.wait(); } catch (InterruptedException e) { throw new IOException("Interrupted while waiting to aquire reader"); } } // some other read thread might have created the reader in the // meantime -> check again if (multiReader == null) { List<ReadOnlyIndexReader> readerList = new ArrayList<ReadOnlyIndexReader>(); for (PersistentIndex pIdx : indexes) { if (indexNames.contains(pIdx.getName())) { readerList.add(pIdx.getReadOnlyIndexReader(initCache)); } } readerList.add(volatileIndex.getReadOnlyIndexReader()); ReadOnlyIndexReader[] readers = readerList.toArray(new ReadOnlyIndexReader[readerList.size()]); multiReader = new CachingMultiIndexReader(readers, cache); } multiReader.acquire(); return multiReader; } } /** * Returns the volatile index. * * @return the volatile index. */ VolatileIndex getVolatileIndex() { return volatileIndex; } /** * Runs a consistency check on this multi index. * * @return the consistency check. * @throws IOException if an error occurs while running the check. */ ConsistencyCheck runConsistencyCheck() throws IOException { return ConsistencyCheck.run(this, handler, excludedIDs); } /** * Closes this <code>MultiIndex</code>. */ void close() { // stop index merger // when calling this method we must not lock this MultiIndex, otherwise // a deadlock might occur merger.dispose(); synchronized (this) { // stop timer unscheduleFlushTask(); // commit / close indexes try { releaseMultiReader(); } catch (IOException e) { log.error("Exception while closing search index.", e); } try { flush(); } catch (IOException e) { log.error("Exception while closing search index.", e); } volatileIndex.close(); for (PersistentIndex index : indexes) { index.close(); } // close indexing queue indexingQueue.close(); // finally close directory try { indexDir.close(); } catch (IOException e) { log.error("Exception while closing directory.", e); } } } /** * Returns the namespace mappings of this search index. * @return the namespace mappings of this search index. */ NamespaceMappings getNamespaceMappings() { return nsMappings; } /** * Returns the indexing queue for this multi index. * @return the indexing queue for this multi index. */ IndexingQueue getIndexingQueue() { return indexingQueue; } /** * @return the base directory of the index. */ Directory getDirectory() { return indexDir; } /** * @return the current generation of the index names. */ long getIndexGeneration() { return indexNames.getGeneration(); } /** * Returns a lucene Document for the <code>node</code>. * * @param node the node to index. * @return the index document. * @throws RepositoryException if an error occurs while reading from the * workspace. */ Document createDocument(NodeState node) throws RepositoryException { return handler.createDocument(node, nsMappings, version); } /** * Returns a lucene Document for the Node with <code>id</code>. * * @param id the id of the node to index. * @return the index document. * @throws RepositoryException if an error occurs while reading from the * workspace or if there is no node with * <code>id</code>. */ Document createDocument(NodeId id) throws RepositoryException { try { NodeState state = (NodeState) handler.getContext().getItemStateManager().getItemState(id); return createDocument(state); } catch (NoSuchItemStateException e) { throw new RepositoryException("Node " + id + " does not exist", e); } catch (ItemStateException e) { throw new RepositoryException("Error retrieving node: " + id, e); } } /** * Returns <code>true</code> if the redo log contained entries while * this index was instantiated; <code>false</code> otherwise. * @return <code>true</code> if the redo log contained entries. */ boolean getRedoLogApplied() { return redoLogApplied; } /** * Removes the <code>index</code> from the list of active sub indexes. * Depending on the {@link SearchIndex#getMaxHistoryAge()}, the * Index is not deleted right away. * <p> * This method does not close the index, but rather expects that the index * has already been closed. * * @param index the index to delete. */ synchronized void deleteIndex(PersistentIndex index) { // remove it from the lists if index is registered indexes.remove(index); indexNames.removeName(index.getName()); synchronized (deletable) { log.debug("Moved " + index.getName() + " to deletable"); deletable.put(index.getName(), System.currentTimeMillis()); } } /** * Flushes this <code>MultiIndex</code>. Persists all pending changes and * resets the redo log. * * @throws IOException if the flush fails. */ private void flush() throws IOException { synchronized (this) { // only start transaction when there is something to commit boolean transactionStarted = false; if (volatileIndex.getNumDocuments() > 0) { // commit volatile index executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); transactionStarted = true; commitVolatileIndex(); } boolean indexesModified = false; // commit persistent indexes for (int i = indexes.size() - 1; i >= 0; i--) { PersistentIndex index = indexes.get(i); // only commit indexes we own // index merger also places PersistentIndex instances in indexes, // but does not make them public by registering the name in indexNames if (indexNames.contains(index.getName())) { long gen = index.getCurrentGeneration(); index.commit(); if (gen != index.getCurrentGeneration()) { indexesModified = true; log.debug("Committed revision {} of index {}", Long.toString(index.getCurrentGeneration(), Character.MAX_RADIX), index.getName()); } // check if index still contains documents if (index.getNumDocuments() == 0) { if (!transactionStarted) { executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); transactionStarted = true; } executeAndLog(new DeleteIndex(getTransactionId(), index.getName())); } } } if (transactionStarted) { executeAndLog(new Commit(getTransactionId())); } if (transactionStarted || indexesModified || redoLog.hasEntries()) { indexNames.write(); indexHistory.addIndexInfos(indexNames); // close redo.log and create a new one based // on the new indexNames generation redoLog.close(); redoLog = redoLogFactory.createRedoLog(this); } lastFlushTime = System.currentTimeMillis(); } indexHistory.pruneOutdated(); // delete obsolete indexes attemptDelete(); } /** * Releases the {@link #multiReader} and sets it <code>null</code>. If the * reader is already <code>null</code> this method does nothing. When this * method returns {@link #multiReader} is guaranteed to be <code>null</code> * even if an exception is thrown. * <p> * Please note that this method does not take care of any synchronization. * A caller must ensure that it is the only thread operating on this multi * index, or that it holds the {@link #updateMonitor}. * * @throws IOException if an error occurs while releasing the reader. */ void releaseMultiReader() throws IOException { if (multiReader != null) { try { multiReader.release(); } finally { multiReader = null; } } } //-------------------------< testing only >--------------------------------- void waitUntilIndexingQueueIsEmpty() { IndexingQueue iq = getIndexingQueue(); synchronized (iq) { while (iq.getNumPendingDocuments() > 0 || indexingQueueCommitPending) { try { log.debug( "waiting for indexing queue to become empty. {} pending docs.", iq.getNumPendingDocuments()); iq.wait(); log.debug("notified"); } catch (InterruptedException e) { // interrupted, check again if queue is empty } } } } void notifyIfIndexingQueueIsEmpty() { IndexingQueue iq = getIndexingQueue(); synchronized (iq) { indexingQueueCommitPending = false; if (iq.getNumPendingDocuments() == 0) { iq.notifyAll(); } } } //-------------------------< internal >------------------------------------- /** * Enqueues unused segments for deletion in {@link #deletable}. This method * does not synchronize on {@link #deletable}! A caller must ensure that it * is the only one acting on the {@link #deletable} map. * * @throws IOException if an error occurs while reading directories. */ private void enqueueUnusedSegments() throws IOException { // walk through index segments for (String name : directoryManager.getDirectoryNames()) { if (!name.startsWith("_")) { continue; } long lastUse = indexHistory.getLastUseOf(name); if (lastUse != Long.MAX_VALUE) { if (log.isDebugEnabled()) { String msg = "Segment " + name + " not is use anymore. "; if (lastUse != Long.MIN_VALUE) { Calendar cal = Calendar.getInstance(); DateFormat df = DateFormat.getInstance(); cal.setTimeInMillis(lastUse); msg += "Unused since: " + df.format(cal.getTime()); } else { msg += "(orphaned)"; } log.debug(msg); } deletable.put(name, lastUse); } } // now prune outdated index infos indexHistory.pruneOutdated(); } /** * Schedules a background task for flushing the index once per second. */ private void scheduleFlushTask() { ScheduledExecutorService executor = handler.getContext().getExecutor(); flushTask = executor.scheduleWithFixedDelay(new Runnable() { public void run() { // check if there are any indexing jobs finished checkIndexingQueue(false); // check if volatile index should be flushed checkFlush(); } }, 1, 1, TimeUnit.SECONDS); } /** * Cancels the scheduled background index flush task. */ private void unscheduleFlushTask() { if (flushTask != null) { flushTask.cancel(false); flushTask = null; } } /** * Resets the volatile index to a new instance. * * @throws IOException if the volatile index cannot be reset. */ private void resetVolatileIndex() throws IOException { // JCR-3227 close VolatileIndex properly if (volatileIndex != null) { volatileIndex.close(); } volatileIndex = new VolatileIndex(handler.getTextAnalyzer(), handler.getSimilarity(), indexingQueue); volatileIndex.setUseCompoundFile(handler.getUseCompoundFile()); volatileIndex.setBufferSize(handler.getBufferSize()); } /** * Returns the current transaction id. * * @return the current transaction id. */ private long getTransactionId() { return currentTransactionId; } /** * Executes action <code>a</code> and appends the action to the redo log if * successful. * * @param a the <code>Action</code> to execute. * @return the executed action. * @throws IOException if an error occurs while executing the action * or appending the action to the redo log. */ private Action executeAndLog(Action a) throws IOException { a.execute(this); redoLog.append(a); // please note that flushing the redo log is only required on // commit, but we also want to keep track of new indexes for sure. // otherwise it might happen that unused index folders are orphaned // after a crash. if (a.getType() == Action.TYPE_COMMIT || a.getType() == Action.TYPE_ADD_INDEX) { redoLog.flush(); } return a; } /** * Checks if it is needed to commit the volatile index according to {@link * SearchIndex#getMaxVolatileIndexSize()}. * * @return <code>true</code> if the volatile index has been committed, * <code>false</code> otherwise. * @throws IOException if an error occurs while committing the volatile * index. */ private boolean checkVolatileCommit() throws IOException { if (volatileIndex.getRamSizeInBytes() >= handler.getMaxVolatileIndexSize()) { commitVolatileIndex(); return true; } return false; } /** * Commits the volatile index to a persistent index. The new persistent * index is added to the list of indexes but not written to disk. When this * method returns a new volatile index has been created. * * @throws IOException if an error occurs while writing the volatile index * to disk. */ private void commitVolatileIndex() throws IOException { // check if volatile index contains documents at all int volatileIndexDocuments = volatileIndex.getNumDocuments(); if (volatileIndexDocuments > 0) { long time = System.currentTimeMillis(); // create index CreateIndex create = new CreateIndex(getTransactionId(), null); executeAndLog(create); // commit volatile index executeAndLog(new VolatileCommit(getTransactionId(), create.getIndexName())); // add new index AddIndex add = new AddIndex(getTransactionId(), create.getIndexName()); executeAndLog(add); // create new volatile index resetVolatileIndex(); time = System.currentTimeMillis() - time; log.debug("Committed in-memory index containing {} documents in {}ms.", volatileIndexDocuments, time); } } /** * Recursively creates an index starting with the NodeState * <code>node</code>. * * @param node the current NodeState. * @param path the path of the current <code>node</code> state. * @param stateMgr the shared item state manager. * @param count the number of nodes already indexed. * @return the number of nodes indexed so far. * @throws IOException if an error occurs while writing to the * index. * @throws ItemStateException if an node state cannot be found. * @throws RepositoryException if any other error occurs */ private long createIndex(NodeState node, Path path, ItemStateManager stateMgr, long count) throws IOException, ItemStateException, RepositoryException { NodeId id = node.getNodeId(); if (excludedIDs.contains(id)) { return count; } executeAndLog(new AddNode(getTransactionId(), id)); if (++count % 100 == 0) { PathResolver resolver = new DefaultNamePathResolver( handler.getContext().getNamespaceRegistry()); log.info("indexing... {} ({})", resolver.getJCRPath(path), count); } if (count % 10 == 0) { checkIndexingQueue(true); } checkVolatileCommit(); for (ChildNodeEntry child : node.getChildNodeEntries()) { Path childPath = PATH_FACTORY.create(path, child.getName(), child.getIndex(), false); NodeState childState = null; try { childState = (NodeState) stateMgr.getItemState(child.getId()); } catch (NoSuchItemStateException e) { handler.getOnWorkspaceInconsistencyHandler().handleMissingChildNode( e, handler, path, node, child); } catch (ItemStateException e) { // JCR-3268 log bundle corruption and continue handler.getOnWorkspaceInconsistencyHandler().logError(e, handler, childPath, node, child); } if (childState != null) { count = createIndex(childState, childPath, stateMgr, count); } } return count; } /** * Attempts to delete all files that are older than *{@link SearchIndex#getMaxHistoryAge()}. */ private void attemptDelete() { synchronized (deletable) { for (Iterator<Map.Entry<String, Long>> it = deletable.entrySet().iterator(); it.hasNext(); ) { Map.Entry<String, Long> entry = it.next(); String indexName = entry.getKey(); long lastUse = entry.getValue(); if (System.currentTimeMillis() - handler.getMaxHistoryAge() * 1000 > lastUse) { if (directoryManager.delete(indexName)) { it.remove(); } else { // JCR-2705: We will retry later, so only a debug log log.debug("Unable to delete obsolete index: {}", indexName); } } } } } /** * Removes the deletable file if it exists. The file is not used anymore * in Jackrabbit versions >= 1.5. */ private void removeDeletable() { String fileName = "deletable"; try { if (indexDir.fileExists(fileName)) { indexDir.deleteFile(fileName); } } catch (IOException e) { log.warn("Unable to remove file 'deletable'.", e); } } /** * Checks the duration between the last commit to this index and the * current time and flushes the index (if there are changes at all) * if the duration (idle time) is more than {@link SearchIndex#getVolatileIdleTime()} * seconds. */ private synchronized void checkFlush() { long idleTime = System.currentTimeMillis() - lastFlushTime; // do not flush if volatileIdleTime is zero or negative if (handler.getVolatileIdleTime() > 0 && idleTime > handler.getVolatileIdleTime() * 1000) { try { if (redoLog.hasEntries()) { log.debug("Flushing index after being idle for " + idleTime + " ms."); safeFlush(); } } catch (IOException e) { log.error("Unable to commit volatile index", e); } } } void safeFlush() throws IOException{ synchronized (updateMonitor) { updateInProgress = true; } try { flush(); } finally { synchronized (updateMonitor) { updateInProgress = false; updateMonitor.notifyAll(); releaseMultiReader(); } } } /** * Checks the indexing queue for finished text extrator jobs and updates the * index accordingly if there are any new ones. * * @param transactionPresent whether a transaction is in progress and the * current {@link #getTransactionId()} should be * used. If <code>false</code> a new transaction * is created when documents are transfered from * the indexing queue to the index. */ private void checkIndexingQueue(boolean transactionPresent) { Map<NodeId, Document> finished = new HashMap<NodeId, Document>(); for (Document document : indexingQueue.getFinishedDocuments()) { NodeId id = new NodeId(document.get(FieldNames.UUID)); finished.put(id, document); } // now update index with the remaining ones if there are any if (!finished.isEmpty()) { log.debug("updating index with {} nodes from indexing queue.", finished.size()); // Only useful for testing synchronized (getIndexingQueue()) { indexingQueueCommitPending = true; } try { // remove documents from the queue for (NodeId id : finished.keySet()) { indexingQueue.removeDocument(id.toString()); } try { if (transactionPresent) { synchronized (this) { for (NodeId id : finished.keySet()) { executeAndLog(new DeleteNode(getTransactionId(), id)); } for (Document document : finished.values()) { executeAndLog(new AddNode(getTransactionId(), document)); } } } else { update(finished.keySet(), finished.values()); } } catch (IOException e) { // update failed log.warn("Failed to update index with deferred text extraction", e); } } finally { // the following method also resets // indexingQueueCommitPending back to false notifyIfIndexingQueueIsEmpty(); } } } //------------------------< Actions >--------------------------------------- /** * Defines an action on an <code>MultiIndex</code>. */ public abstract static class Action { /** * Action identifier in redo log for transaction start action. */ static final String START = "STR"; /** * Action type for start action. */ public static final int TYPE_START = 0; /** * Action identifier in redo log for add node action. */ static final String ADD_NODE = "ADD"; /** * Action type for add node action. */ public static final int TYPE_ADD_NODE = 1; /** * Action identifier in redo log for node delete action. */ static final String DELETE_NODE = "DEL"; /** * Action type for delete node action. */ public static final int TYPE_DELETE_NODE = 2; /** * Action identifier in redo log for transaction commit action. */ static final String COMMIT = "COM"; /** * Action type for commit action. */ public static final int TYPE_COMMIT = 3; /** * Action identifier in redo log for volatile index commit action. */ static final String VOLATILE_COMMIT = "VOL_COM"; /** * Action type for volatile index commit action. */ public static final int TYPE_VOLATILE_COMMIT = 4; /** * Action identifier in redo log for index create action. */ static final String CREATE_INDEX = "CRE_IDX"; /** * Action type for create index action. */ public static final int TYPE_CREATE_INDEX = 5; /** * Action identifier in redo log for index add action. */ static final String ADD_INDEX = "ADD_IDX"; /** * Action type for add index action. */ public static final int TYPE_ADD_INDEX = 6; /** * Action identifier in redo log for delete index action. */ static final String DELETE_INDEX = "DEL_IDX"; /** * Action type for delete index action. */ public static final int TYPE_DELETE_INDEX = 7; /** * Transaction identifier for internal actions like volatile index * commit triggered by timer thread. */ static final long INTERNAL_TRANSACTION = -1; /** * Transaction identifier for internal action that replaces indexs. */ static final long INTERNAL_TRANS_REPL_INDEXES = -2; /** * The id of the transaction that executed this action. */ private final long transactionId; /** * The action type. */ private final int type; /** * Creates a new <code>Action</code>. * * @param transactionId the id of the transaction that executed this * action. * @param type the action type. */ Action(long transactionId, int type) { this.transactionId = transactionId; this.type = type; } /** * Returns the transaction id for this <code>Action</code>. * * @return the transaction id for this <code>Action</code>. */ long getTransactionId() { return transactionId; } /** * Returns the action type. * * @return the action type. */ int getType() { return type; } /** * Executes this action on the <code>index</code>. * * @param index the index where to execute the action. * @throws IOException if the action fails due to some I/O error in * the index or some other error. */ public abstract void execute(MultiIndex index) throws IOException; /** * Executes the inverse operation of this action. That is, does an undo * of this action. This default implementation does nothing, but returns * silently. * * @param index the index where to undo the action. * @throws IOException if the action cannot be undone. */ public void undo(MultiIndex index) throws IOException { } /** * Returns a <code>String</code> representation of this action that can be * written to the {@link RedoLog}. * * @return a <code>String</code> representation of this action. */ public abstract String toString(); /** * Parses an line in the redo log and created an {@link Action}. * * @param line the line from the redo log. * @return an <code>Action</code>. * @throws IllegalArgumentException if the line is malformed. */ static Action fromString(String line) throws IllegalArgumentException { int endTransIdx = line.indexOf(' '); if (endTransIdx == -1) { throw new IllegalArgumentException(line); } long transactionId; try { transactionId = Long.parseLong(line.substring(0, endTransIdx)); } catch (NumberFormatException e) { throw new IllegalArgumentException(line); } int endActionIdx = line.indexOf(' ', endTransIdx + 1); if (endActionIdx == -1) { // action does not have arguments endActionIdx = line.length(); } String actionLabel = line.substring(endTransIdx + 1, endActionIdx); String arguments = ""; if (endActionIdx + 1 <= line.length()) { arguments = line.substring(endActionIdx + 1); } Action a; if (actionLabel.equals(Action.ADD_NODE)) { a = AddNode.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.ADD_INDEX)) { a = AddIndex.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.COMMIT)) { a = Commit.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.CREATE_INDEX)) { a = CreateIndex.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.DELETE_INDEX)) { a = DeleteIndex.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.DELETE_NODE)) { a = DeleteNode.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.START)) { a = Start.fromString(transactionId, arguments); } else if (actionLabel.equals(Action.VOLATILE_COMMIT)) { a = VolatileCommit.fromString(transactionId, arguments); } else { throw new IllegalArgumentException(line); } return a; } } /** * Adds an index to the MultiIndex's active persistent index list. */ private static class AddIndex extends Action { /** * The name of the index to add. */ private String indexName; /** * Creates a new AddIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param indexName the name of the index to add, or <code>null</code> * if an index with a new name should be created. */ AddIndex(long transactionId, String indexName) { super(transactionId, Action.TYPE_ADD_INDEX); this.indexName = indexName; } /** * Creates a new AddIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments the name of the index to add. * @return the AddIndex action. * @throws IllegalArgumentException if the arguments are malformed. */ static AddIndex fromString(long transactionId, String arguments) { return new AddIndex(transactionId, arguments); } /** * Adds a sub index to <code>index</code>. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { PersistentIndex idx = index.getOrCreateIndex(indexName); if (!index.indexNames.contains(indexName)) { index.indexNames.addName(indexName, idx.getCurrentGeneration()); // now that the index is in the active list let the merger know about it index.merger.indexAdded(indexName, idx.getNumDocuments()); } } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.ADD_INDEX); logLine.append(' '); logLine.append(indexName); return logLine.toString(); } } /** * Adds a node to the index. */ private static class AddNode extends Action { /** * The maximum length of a AddNode String. */ private static final int ENTRY_LENGTH = Long.toString(Long.MAX_VALUE).length() + Action.ADD_NODE.length() + new NodeId(0, 0).toString().length() + 2; /** * The id of the node to add. */ private final NodeId id; /** * The document to add to the index, or <code>null</code> if not available. */ private Document doc; /** * Creates a new AddNode action. * * @param transactionId the id of the transaction that executes this action. * @param id the id of the node to add. */ AddNode(long transactionId, NodeId id) { super(transactionId, Action.TYPE_ADD_NODE); this.id = id; } /** * Creates a new AddNode action. * * @param transactionId the id of the transaction that executes this action. * @param doc the document to add. */ AddNode(long transactionId, Document doc) { this(transactionId, new NodeId(doc.get(FieldNames.UUID))); this.doc = doc; } /** * Creates a new AddNode action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments The UUID of the node to add * @return the AddNode action. * @throws IllegalArgumentException if the arguments are malformed. Not a * UUID. */ static AddNode fromString(long transactionId, String arguments) throws IllegalArgumentException { return new AddNode(transactionId, new NodeId(arguments)); } /** * Adds a node to the index. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { if (doc == null) { try { doc = index.createDocument(id); } catch (RepositoryException e) { // node does not exist anymore log.debug(e.getMessage()); } } if (doc != null) { index.volatileIndex.addDocuments(new Document[]{doc}); } } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(ENTRY_LENGTH); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.ADD_NODE); logLine.append(' '); logLine.append(id); return logLine.toString(); } } /** * Commits a transaction. */ private static class Commit extends Action { /** * Creates a new Commit action. * * @param transactionId the id of the transaction that is committed. */ Commit(long transactionId) { super(transactionId, Action.TYPE_COMMIT); } /** * Creates a new Commit action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments ignored by this method. * @return the Commit action. */ static Commit fromString(long transactionId, String arguments) { return new Commit(transactionId); } /** * Touches the last flush time (sets it to the current time). * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { index.lastFlushTime = System.currentTimeMillis(); } /** * @inheritDoc */ public String toString() { return Long.toString(getTransactionId()) + ' ' + Action.COMMIT; } } /** * Creates an new sub index but does not add it to the active persistent index * list. */ private static class CreateIndex extends Action { /** * The name of the index to add. */ private String indexName; /** * Creates a new CreateIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param indexName the name of the index to add, or <code>null</code> * if an index with a new name should be created. */ CreateIndex(long transactionId, String indexName) { super(transactionId, Action.TYPE_CREATE_INDEX); this.indexName = indexName; } /** * Creates a new CreateIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments the name of the index to create. * @return the AddIndex action. * @throws IllegalArgumentException if the arguments are malformed. */ static CreateIndex fromString(long transactionId, String arguments) { // when created from String, this action is executed as redo action return new CreateIndex(transactionId, arguments); } /** * Creates a new index. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { PersistentIndex idx = index.getOrCreateIndex(indexName); indexName = idx.getName(); } /** * @inheritDoc */ public void undo(MultiIndex index) throws IOException { if (index.hasIndex(indexName)) { PersistentIndex idx = index.getOrCreateIndex(indexName); idx.close(); index.deleteIndex(idx); } } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.CREATE_INDEX); logLine.append(' '); logLine.append(indexName); return logLine.toString(); } /** * Returns the index name that has been created. If this method is called * before {@link #execute(MultiIndex)} it will return <code>null</code>. * * @return the name of the index that has been created. */ String getIndexName() { return indexName; } } /** * Closes and deletes an index that is no longer in use. */ private static class DeleteIndex extends Action { /** * The name of the index to add. */ private String indexName; /** * Creates a new DeleteIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param indexName the name of the index to delete. */ DeleteIndex(long transactionId, String indexName) { super(transactionId, Action.TYPE_DELETE_INDEX); this.indexName = indexName; } /** * Creates a new DeleteIndex action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments the name of the index to delete. * @return the DeleteIndex action. * @throws IllegalArgumentException if the arguments are malformed. */ static DeleteIndex fromString(long transactionId, String arguments) { return new DeleteIndex(transactionId, arguments); } /** * Removes a sub index from <code>index</code>. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { // get index if it exists for (PersistentIndex idx : index.indexes) { if (idx.getName().equals(indexName)) { idx.close(); index.deleteIndex(idx); break; } } } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.DELETE_INDEX); logLine.append(' '); logLine.append(indexName); return logLine.toString(); } } /** * Deletes a node from the index. */ private static class DeleteNode extends Action { /** * The maximum length of a DeleteNode String. */ private static final int ENTRY_LENGTH = Long.toString(Long.MAX_VALUE).length() + Action.DELETE_NODE.length() + new NodeId(0, 0).toString().length() + 2; /** * The id of the node to remove. */ private final NodeId id; /** * Creates a new DeleteNode action. * * @param transactionId the id of the transaction that executes this action. * @param id the id of the node to delete. */ DeleteNode(long transactionId, NodeId id) { super(transactionId, Action.TYPE_DELETE_NODE); this.id = id; } /** * Creates a new DeleteNode action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments the UUID of the node to delete. * @return the DeleteNode action. * @throws IllegalArgumentException if the arguments are malformed. Not a * UUID. */ static DeleteNode fromString(long transactionId, String arguments) { return new DeleteNode(transactionId, new NodeId(arguments)); } /** * Deletes a node from the index. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { String uuidString = id.toString(); // check if indexing queue is still working on // this node from a previous update Document doc = index.indexingQueue.removeDocument(uuidString); if (doc != null) { Util.disposeDocument(doc); index.notifyIfIndexingQueueIsEmpty(); } Term idTerm = TermFactory.createUUIDTerm(uuidString); // if the document cannot be deleted from the volatile index // delete it from one of the persistent indexes. int num = index.volatileIndex.removeDocument(idTerm); if (num == 0) { for (int i = index.indexes.size() - 1; i >= 0; i--) { // only look in registered indexes PersistentIndex idx = index.indexes.get(i); if (index.indexNames.contains(idx.getName())) { num = idx.removeDocument(idTerm); if (num > 0) { return; } } } } } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(ENTRY_LENGTH); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.DELETE_NODE); logLine.append(' '); logLine.append(id); return logLine.toString(); } } /** * Starts a transaction. */ private static class Start extends Action { /** * Creates a new Start transaction action. * * @param transactionId the id of the transaction that started. */ Start(long transactionId) { super(transactionId, Action.TYPE_START); } /** * Creates a new Start action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments ignored by this method. * @return the Start action. */ static Start fromString(long transactionId, String arguments) { return new Start(transactionId); } /** * Sets the current transaction id on <code>index</code>. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { index.currentTransactionId = getTransactionId(); } /** * @inheritDoc */ public String toString() { return Long.toString(getTransactionId()) + ' ' + Action.START; } } /** * Commits the volatile index to disk. */ private static class VolatileCommit extends Action { /** * The name of the target index to commit to. */ private final String targetIndex; /** * Creates a new VolatileCommit action. * * @param transactionId the id of the transaction that executes this action. * @param targetIndex the name of the index where the volatile index * will be committed. */ VolatileCommit(long transactionId, String targetIndex) { super(transactionId, Action.TYPE_VOLATILE_COMMIT); this.targetIndex = targetIndex; } /** * Creates a new VolatileCommit action. * * @param transactionId the id of the transaction that executes this * action. * @param arguments ignored by this implementation. * @return the VolatileCommit action. */ static VolatileCommit fromString(long transactionId, String arguments) { return new VolatileCommit(transactionId, arguments); } /** * Commits the volatile index to disk. * * @inheritDoc */ public void execute(MultiIndex index) throws IOException { VolatileIndex volatileIndex = index.getVolatileIndex(); PersistentIndex persistentIndex = index.getOrCreateIndex(targetIndex); persistentIndex.copyIndex(volatileIndex); index.resetVolatileIndex(); } /** * @inheritDoc */ public String toString() { StringBuffer logLine = new StringBuffer(); logLine.append(Long.toString(getTransactionId())); logLine.append(' '); logLine.append(Action.VOLATILE_COMMIT); logLine.append(' '); logLine.append(targetIndex); return logLine.toString(); } } }