Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.search.impl.lucene.index; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.channels.FileLock; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.zip.CRC32; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.search.IndexerException; import org.alfresco.repo.search.impl.lucene.FilterIndexReaderByStringId; import org.alfresco.repo.search.impl.lucene.LuceneConfig; import org.alfresco.repo.search.impl.lucene.LuceneXPathHandler; import org.alfresco.repo.search.impl.lucene.analysis.AlfrescoStandardAnalyser; import org.alfresco.repo.search.impl.lucene.query.PathQuery; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.GUID; import org.alfresco.util.TraceableThreadFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexReader.FieldOption; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriter.MaxFieldLength; import org.apache.lucene.index.LogDocMergePolicy; import org.apache.lucene.index.MultiReader; import org.apache.lucene.index.SerialMergeScheduler; import org.apache.lucene.index.Term; import org.apache.lucene.index.TermEnum; import org.apache.lucene.search.Hits; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.Searcher; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.IndexOutput; import org.apache.lucene.store.RAMDirectory; import org.jaxen.saxpath.SAXPathException; import org.jaxen.saxpath.base.XPathReader; import org.safehaus.uuid.UUID; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; /** * The information that makes up an index. IndexInfoVersion Repeated information of the form * <ol> * <li> Index Type. * <li> sub-directory name. * <li> Status * <ol> * <li>Indexes, sub indexes, and overlays must be committed. Status is ACTIVE, MERGING, COMPLETING_INDEX * <li>Delta: Transaction status * <li>Overlay: Transaction status * </ol> * </ol> * Merges always take place to new indexes so we can detect merge failure or partial merges. Or we do not know what has * merged. Incomplete delete merging does not matter - the overlay would still exist and be treated as such. So a * document may be deleted in the index as well as in the applied overlay. It is still correctly deleted. NOTE: Public * methods lock as required, the private methods assume that the appropriate locks have been obtained. TODO: Write * element status into individual directories. This would be enough for recovery if both index files are lost or * corrupted. TODO: Tidy up index status at start up or after some time. How long would you leave a merge to run? * <p> * The index structure is duplicated to two files. If one is currupted the second is used. * <p> * TODO: * <p> * <ol> * <li> make the index sharing configurable * <li> use a thread pool for deletions, merging and index deletions * <li> something to control the maximum number of overlays to limit the number of things layered together for searching * <li> look at lucene locking again post 2.0, to see if it is improved * <li> clean up old data files (that are not old index entries) - should be a config option * </ol> * * @author Andy Hind */ public class IndexInfo implements IndexMonitor { public static synchronized void destroy() { timer.cancel(); timer = new Timer(true); for (IndexInfo indexInfo : indexInfos.values()) { indexInfo.destroyInstance(); } indexInfos.clear(); ReferenceCountingReadOnlyIndexReaderFactory.destroy(); } public void destroyInstance() { getWriteLock(); try { if (mainIndexReader != null) { try { ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); } catch (IOException e) { // OK filed to close } mainIndexReader = null; for (IndexReader reader : referenceCountingReadOnlyIndexReaders.values()) { ReferenceCounting referenceCounting = (ReferenceCounting) reader; try { referenceCounting.setInvalidForReuse(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } for (IndexReader reader : indexReaders.values()) { try { reader.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } indexReaders.clear(); for (IndexWriter writer : indexWriters.values()) { try { writer.close(); } catch (CorruptIndexException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } indexWriters.clear(); if (indexInfoRAF != null) { try { indexInfoRAF.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (indexInfoBackupRAF != null) { try { indexInfoBackupRAF.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // TODO: should set some running flag .... to abort ungoing stuff // at the moment it will die ungracefully .... } finally { releaseWriteLock(); } } public static final String MAIN_READER = "MainReader"; private static Timer timer = new Timer("IndexInfo Cleaner Deamon", true); /** * The logger. */ private static Log s_logger = LogFactory.getLog(IndexInfo.class); /** * Use NIO memory mapping to wite the index control file. */ private static boolean useNIOMemoryMapping = true; /** * The default name for the file that holds the index information */ private static String INDEX_INFO = "IndexInfo"; /** * The default name for the back up file that holds the index information */ private static String INDEX_INFO_BACKUP = "IndexInfoBackup"; /** * The default name for the index deletions file */ private static String INDEX_INFO_DELETIONS = "IndexInfoDeletions"; /** * The default name for the index container deletions file */ private static String INDEX_INFO_CONTAINER_DELETIONS = "IndexInfoContainerDeletions"; /** * What to look for to detect the previous index implementation. */ private static String OLD_INDEX = "index"; /** * Is this index shared by more than one repository? We can make many lock optimisations if the index is not shared. */ private boolean indexIsShared = false; /** * The directory that holds the index */ private File indexDirectory; /** * The directory relative to the root path */ private String relativePath; /** * The file holding the index information */ private RandomAccessFile indexInfoRAF; /** * And its file channel */ private FileChannel indexInfoChannel; /** * The file holding the backup index information. */ private RandomAccessFile indexInfoBackupRAF; /** * And its file channel */ private FileChannel indexInfoBackupChannel; /** * The file version. Negative is not yet written. */ private long version = -1; /** * The index entries that make up this index. Map entries are looked up by name. These are maintained in order so * document order is maintained. */ private LinkedHashMap<String, IndexEntry> indexEntries = new LinkedHashMap<String, IndexEntry>(); /** * Lock for the index entries */ private final ReentrantReadWriteLock readWriteLock; private ReentrantReadWriteLock readOnlyLock = new ReentrantReadWriteLock(); /** * Read only index readers that also do reference counting. */ private HashMap<String, IndexReader> referenceCountingReadOnlyIndexReaders = new HashMap<String, IndexReader>(); /** * Main index reader */ private IndexReader mainIndexReader; private Map<String, IndexReader> mainIndexReaders = new HashMap<String, IndexReader>(); /** * Index writers for deltas */ private Map<String, IndexWriter> indexWriters = new ConcurrentHashMap<String, IndexWriter>(51); /** * Index Readers for deltas */ private Map<String, IndexReader> indexReaders = new ConcurrentHashMap<String, IndexReader>(51); /** * Map of state transitions */ private EnumMap<TransactionStatus, Transition> transitions = new EnumMap<TransactionStatus, Transition>( TransactionStatus.class); /** * The queue of files and folders to delete */ private ConcurrentLinkedQueue<String> deleteQueue = new ConcurrentLinkedQueue<String>(); /** * A queue of reference counting index readers. We wait for these to become unused (ref count falls to zero) then * the data can be removed. */ private ConcurrentLinkedQueue<IndexReader> deletableReaders = new ConcurrentLinkedQueue<IndexReader>(); /** * The call that is responsible for deleting old index information from disk. */ private Cleaner cleaner = new Cleaner(); /** * The thread that deletes old index data */ // private Thread cleanerThread; /** * The class the supports index merging and applying deletions from deltas to indexes and deltas that go before it. */ private Merger merger = new Merger(); /** * The thread that carries out index merging and applying deletions from deltas to indexes and deltas that go before * it. */ // private Thread mergerThread; /** * A shared empty index to use if non exist. */ private Directory emptyIndex = new RAMDirectory(); /** * The index infor files that make up the index */ private static HashMap<File, IndexInfo> indexInfos = new HashMap<File, IndexInfo>(); // Properties that control lucene indexing // -------------------------------------- // Properties for indexes that are created by transactions ... private int maxDocsForInMemoryMerge = 10000; private int maxDocsForInMemoryIndex = 10000; private double maxRamInMbForInMemoryMerge = 16.0; private double maxRamInMbForInMemoryIndex = 16.0; private int writerMaxBufferedDocs = IndexWriter.DISABLE_AUTO_FLUSH; private double writerRamBufferSizeMb = 16.0; private int writerMergeFactor = 5; private int writerMaxMergeDocs = 1000000; private boolean writerUseCompoundFile = true; // Properties for indexes created by merging private int mergerMaxBufferedDocs = IndexWriter.DISABLE_AUTO_FLUSH; private double mergerRamBufferSizeMb = 16.0; private int mergerMergeFactor = 5; private int mergerMaxMergeDocs = 1000000; private boolean mergerUseCompoundFile = true; private int mergerTargetOverlays = 5; private int mergerTargetIndexes = 5; private int mergerTargetOverlaysBlockingFactor = 1; private Object mergerTargetLock = new Object(); // To avoid deadlock (a thread with multiple deltas never proceeding to commit) we track whether each thread is // already in the prepare phase. private static ThreadLocal<IndexInfo> thisThreadPreparing = new ThreadLocal<IndexInfo>(); // Common properties for indexers private long writeLockTimeout = IndexWriter.WRITE_LOCK_TIMEOUT; private int maxFieldLength = IndexWriter.DEFAULT_MAX_FIELD_LENGTH; private int termIndexInterval = IndexWriter.DEFAULT_TERM_INDEX_INTERVAL; /** * Control if the merger thread is active */ private ThreadPoolExecutor threadPoolExecutor; private LuceneConfig config; private List<ApplicationListener> applicationListeners = new LinkedList<ApplicationListener>(); static { // We do not require any of the lucene in-built locking. FSDirectory.setDisableLocks(true); } /** * */ public void delete(final String deltaId) { getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { setStatusFromFile(); // If the index is not shared we can do some easy clean // up if (!indexIsShared) { HashSet<String> deletable = new HashSet<String>(); // clean up for (IndexEntry entry : indexEntries.values()) { if (!entry.getName().equals(deltaId)) { entry.setStatus(TransactionStatus.DELETABLE); deletable.add(entry.getName()); } } // Delete entries that are not required invalidateMainReadersFromFirst(deletable); for (String id : deletable) { indexEntries.remove(id); } clearOldReaders(); cleaner.schedule(); merger.schedule(); // persist the new state writeStatus(); if (mainIndexReader != null) { if (s_logger.isDebugEnabled()) { s_logger.debug("... invalidating main index reader"); } ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); mainIndexReader = null; } } return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } if (s_logger.isDebugEnabled()) { s_logger.debug("Index " + indexDirectory + " deleted"); } } /** * Get the IndexInfo object based in the given directory. There is only one object per directory per JVM. * * @param file File * @param config LuceneConfig * @return IndexInfo * @throws IndexerException */ public static synchronized IndexInfo getIndexInfo(File file, LuceneConfig config) throws IndexerException { File canonicalFile; try { canonicalFile = file.getCanonicalFile(); IndexInfo indexInfo = indexInfos.get(canonicalFile); if (indexInfo == null) { indexInfo = new IndexInfo(canonicalFile, config); indexInfos.put(canonicalFile, indexInfo); if (s_logger.isDebugEnabled()) { s_logger.debug("Made " + indexInfo + " for " + file.getAbsolutePath()); } } if (s_logger.isDebugEnabled()) { s_logger.debug("Got " + indexInfo + " for " + file.getAbsolutePath()); } return indexInfo; } catch (IOException e) { throw new IndexerException("Failed to transform a file into is canonical form", e); } } /** * Construct an index in the given directory. * * @param indexDirectory File * @param config LuceneConfig */ private IndexInfo(File indexDirectory, LuceneConfig config) { super(); initialiseTransitions(); this.config = config; if (config != null) { this.readWriteLock = new ReentrantReadWriteLock(config.getFairLocking()); this.maxFieldLength = config.getIndexerMaxFieldLength(); this.threadPoolExecutor = config.getThreadPoolExecutor(); IndexInfo.useNIOMemoryMapping = config.getUseNioMemoryMapping(); this.maxDocsForInMemoryMerge = config.getMaxDocsForInMemoryMerge(); this.maxRamInMbForInMemoryMerge = config.getMaxRamInMbForInMemoryMerge(); this.maxDocsForInMemoryIndex = config.getMaxDocsForInMemoryIndex(); this.maxRamInMbForInMemoryIndex = config.getMaxRamInMbForInMemoryIndex(); this.writerMaxBufferedDocs = config.getWriterMaxBufferedDocs(); this.writerRamBufferSizeMb = config.getWriterRamBufferSizeMb(); this.writerMergeFactor = config.getWriterMergeFactor(); this.writerMaxMergeDocs = config.getWriterMaxMergeDocs(); this.mergerMaxBufferedDocs = config.getMergerMaxBufferedDocs(); this.mergerRamBufferSizeMb = config.getMergerRamBufferSizeMb(); this.mergerMergeFactor = config.getMergerMergeFactor(); this.mergerMaxMergeDocs = config.getMergerMaxMergeDocs(); this.termIndexInterval = config.getTermIndexInterval(); this.mergerTargetOverlays = config.getMergerTargetOverlayCount(); this.mergerTargetIndexes = config.getMergerTargetIndexCount(); this.mergerTargetOverlaysBlockingFactor = config.getMergerTargetOverlaysBlockingFactor(); // Work out the relative path of the index try { String indexRoot = new File(config.getIndexRootLocation()).getCanonicalPath(); this.relativePath = indexDirectory.getCanonicalPath().substring(indexRoot.length() + 1); } catch (IOException e) { throw new AlfrescoRuntimeException("Failed to determine index relative path", e); } } else { this.readWriteLock = new ReentrantReadWriteLock(false); // need a default thread pool .... TraceableThreadFactory threadFactory = new TraceableThreadFactory(); threadFactory.setThreadDaemon(true); threadFactory.setThreadPriority(5); threadPoolExecutor = new ThreadPoolExecutor(10, 10, 90, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); // Create a 'fake' relative path try { this.relativePath = indexDirectory.getCanonicalPath(); int sepIndex = this.relativePath.indexOf(File.separator); if (sepIndex != -1) { if (this.relativePath.length() > sepIndex + 1) { this.relativePath = this.relativePath.substring(sepIndex + 1); } else { this.relativePath = ""; } } } catch (IOException e) { throw new AlfrescoRuntimeException("Failed to determine index relative path", e); } } // Create an empty in memory index IndexWriter writer; try { writer = new IndexWriter(emptyIndex, new AlfrescoStandardAnalyser(), true, MaxFieldLength.LIMITED); writer.setUseCompoundFile(writerUseCompoundFile); writer.setMaxBufferedDocs(writerMaxBufferedDocs); writer.setRAMBufferSizeMB(writerRamBufferSizeMb); writer.setMergeFactor(writerMergeFactor); writer.setMaxMergeDocs(writerMaxMergeDocs); writer.setWriteLockTimeout(writeLockTimeout); writer.setMaxFieldLength(maxFieldLength); writer.setTermIndexInterval(termIndexInterval); writer.setMergeScheduler(new SerialMergeScheduler()); writer.setMergePolicy(new LogDocMergePolicy()); writer.close(); } catch (IOException e) { throw new IndexerException("Failed to create an empty in memory index!"); } this.indexDirectory = indexDirectory; // Make sure the directory exists if (!this.indexDirectory.exists()) { if (!this.indexDirectory.mkdirs()) { throw new AlfrescoRuntimeException("Failed to create index directory"); } } if (!this.indexDirectory.isDirectory()) { throw new AlfrescoRuntimeException("The index must be held in a directory"); } // Create the info files. File indexInfoFile = new File(this.indexDirectory, INDEX_INFO); File indexInfoBackupFile = new File(this.indexDirectory, INDEX_INFO_BACKUP); if (createFile(indexInfoFile) && createFile(indexInfoBackupFile)) { // If both files required creation this is a new index version = 0; } // Open the files and channels for the index info file and the backup this.indexInfoRAF = openFile(indexInfoFile); this.indexInfoChannel = this.indexInfoRAF.getChannel(); this.indexInfoBackupRAF = openFile(indexInfoBackupFile); this.indexInfoBackupChannel = this.indexInfoBackupRAF.getChannel(); // If the index found no info files (i.e. it is new), check if there is // an old style index and covert it. if (version == 0) { // Check if an old style index exists final File oldIndex = new File(this.indexDirectory, OLD_INDEX); if (IndexReader.indexExists(oldIndex)) { getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { IndexWriter writer; try { writer = new IndexWriter(oldIndex, new AlfrescoStandardAnalyser(), false, MaxFieldLength.LIMITED); writer.setUseCompoundFile(writerUseCompoundFile); writer.setMaxBufferedDocs(writerMaxBufferedDocs); writer.setRAMBufferSizeMB(writerRamBufferSizeMb); writer.setMergeFactor(writerMergeFactor); writer.setMaxMergeDocs(writerMaxMergeDocs); writer.setWriteLockTimeout(writeLockTimeout); writer.setMaxFieldLength(maxFieldLength); writer.setTermIndexInterval(termIndexInterval); writer.setMergeScheduler(new SerialMergeScheduler()); writer.setMergePolicy(new LogDocMergePolicy()); writer.optimize(); long docs = writer.numDocs(); writer.close(); IndexEntry entry = new IndexEntry(IndexType.INDEX, OLD_INDEX, "", TransactionStatus.COMMITTED, "", docs, 0, false); indexEntries.put(OLD_INDEX, entry); writeStatus(); // The index exists and we should initialise the single reader registerReferenceCountingIndexReader(entry.getName(), buildReferenceCountingIndexReader(entry.getName(), entry.getDocumentCount())); } catch (IOException e) { throw new IndexerException("Failed to optimise old index"); } return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } } } // The index exists else if (version == -1) { getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { setStatusFromFile(); // If the index is not shared we can do some easy clean // up if (!indexIsShared) { HashSet<String> deletable = new HashSet<String>(); // clean up for (IndexEntry entry : indexEntries.values()) { switch (entry.getStatus()) { // states which can be deleted // We could check prepared states can be // committed. case ACTIVE: case MARKED_ROLLBACK: case NO_TRANSACTION: case PREPARING: case ROLLEDBACK: case ROLLINGBACK: case MERGE_TARGET: case UNKNOWN: case PREPARED: case DELETABLE: if (s_logger.isInfoEnabled()) { s_logger.info("Deleting index entry " + entry); } entry.setStatus(TransactionStatus.DELETABLE); deletable.add(entry.getName()); break; // States which are in mid-transition which we // can roll back to the committed state case COMMITTED_DELETING: case MERGE: if (s_logger.isInfoEnabled()) { s_logger.info("Resetting merge to committed " + entry); } entry.setStatus(TransactionStatus.COMMITTED); registerReferenceCountingIndexReader(entry.getName(), buildReferenceCountingIndexReader(entry.getName(), entry.getDocumentCount())); break; // Complete committing (which is post database // commit) case COMMITTING: // do the commit if (s_logger.isInfoEnabled()) { s_logger.info("Committing " + entry); } entry.setStatus(TransactionStatus.COMMITTED); registerReferenceCountingIndexReader(entry.getName(), buildReferenceCountingIndexReader(entry.getName(), entry.getDocumentCount())); break; // States that require no action case COMMITTED: registerReferenceCountingIndexReader(entry.getName(), buildReferenceCountingIndexReader(entry.getName(), entry.getDocumentCount())); break; default: // nothing to do break; } } // Delete entries that are not required invalidateMainReadersFromFirst(deletable); for (String id : deletable) { indexEntries.remove(id); } clearOldReaders(); cleaner.schedule(); merger.schedule(); // persist the new state writeStatus(); } return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } } // Need to do with file lock - must share info about other readers to support this with shared indexer // implementation getWriteLock(); try { LockWork<Object> work = new DeleteUnknownGuidDirectories(); doWithFileLock(work); } finally { releaseWriteLock(); } // Run the cleaner around every 20 secods - this just makes the request to the thread pool timer.schedule(new TimerTask() { @Override public void run() { cleaner.schedule(); } }, 0, 20000); publishDiscoveryEvent(); } private class DeleteUnknownGuidDirectories implements LockWork<Object> { public boolean canRetry() { return true; } public Object doWork() throws Exception { setStatusFromFile(); // If the index is not shared we can do some easy clean // up if (!indexIsShared) { // Safe to tidy up all files that look like guids that we do not know about File[] files = indexDirectory.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { String id = file.getName(); if (!indexEntries.containsKey(id) && isGUID(id)) { if (s_logger.isDebugEnabled()) { s_logger.debug("Deleting unused index directory " + id); } deleteQueue.add(id); } } } } } return null; } } /** * This method should only be called from one thread as it is bound to a transaction. * * @param id String * @return IndexReader * @throws IOException */ public IndexReader getDeltaIndexReader(String id) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // No read lock required as the delta should be bound to one thread only // Index readers are simply thread safe IndexReader reader = indexReaders.get(id); if (reader == null) { // close index writer if required closeDeltaIndexWriter(id); // Check the index knows about the transaction reader = buildAndRegisterDeltaReader(id); indexReaders.put(id, reader); } return reader; } private IndexReader buildAndRegisterDeltaReader(String id) throws IOException { IndexReader reader; // only register on write to avoid any locking for transactions that only ever read File location = getDeltaLocation(id); // File location = ensureDeltaIsRegistered(id); // Create a dummy index reader to deal with empty indexes and not // persist these. if (IndexReader.indexExists(location)) { reader = IndexReader.open(location); } else { reader = IndexReader.open(emptyIndex); } return reader; } private File getDeltaLocation(String id) throws IOException { File file = new File(indexDirectory, id).getCanonicalFile(); return file; } /** * The delta information does not need to be saved to disk. * * @param id String * @return File * @throws IOException */ private File ensureDeltaIsRegistered(String id) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // A write lock is required if we have to update the local index // entries. // There should only be one thread trying to access this delta. File location = getDeltaLocation(id); getReadLock(); try { if (!indexEntries.containsKey(id)) { releaseReadLock(); // release to upgrade to write lock getWriteLock(); try { // Make sure the index exists if (!indexEntries.containsKey(id)) { indexEntries.put(id, new IndexEntry(IndexType.DELTA, id, "", TransactionStatus.ACTIVE, "", 0, 0, false)); } } finally { // Downgrade lock getReadLock(); releaseWriteLock(); } } } finally { // Release the lock releaseReadLock(); } return location; } /** * Make a lucene index writer * * @param location File * @param analyzer Analyzer * @return IndexWriter * @throws IOException */ private IndexWriter makeDeltaIndexWriter(File location, Analyzer analyzer) throws IOException { IndexWriter writer; if (!IndexReader.indexExists(location)) { writer = new IndexWriter(location, analyzer, true, MaxFieldLength.LIMITED); } else { writer = new IndexWriter(location, analyzer, false, MaxFieldLength.LIMITED); } writer.setUseCompoundFile(writerUseCompoundFile); writer.setMaxBufferedDocs(writerMaxBufferedDocs); writer.setRAMBufferSizeMB(writerRamBufferSizeMb); writer.setMergeFactor(writerMergeFactor); writer.setMaxMergeDocs(writerMaxMergeDocs); writer.setWriteLockTimeout(writeLockTimeout); writer.setMaxFieldLength(maxFieldLength); writer.setTermIndexInterval(termIndexInterval); writer.setMergeScheduler(new SerialMergeScheduler()); writer.setMergePolicy(new LogDocMergePolicy()); return writer; } /** * Manage getting a lucene index writer for transactional data - looks after registration and checking there is no * active reader. * * @param id String * @param analyzer Analyzer * @return IndexWriter * @throws IOException */ public IndexWriter getDeltaIndexWriter(String id, Analyzer analyzer) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // No read lock required as the delta should be bound to one thread only IndexWriter writer = indexWriters.get(id); if (writer == null) { // close index writer if required closeDeltaIndexReader(id); File location = ensureDeltaIsRegistered(id); writer = makeDeltaIndexWriter(location, analyzer); indexWriters.put(id, writer); } return writer; } /** * Manage closing and unregistering an index reader. * * @param id String * @throws IOException */ public void closeDeltaIndexReader(String id) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // No lock required as the delta applied to one thread. The delta is // still active. IndexReader reader = indexReaders.remove(id); if (reader != null) { reader.close(); } } /** * Manage closing and unregistering an index writer . * * @param id String * @throws IOException */ public void closeDeltaIndexWriter(String id) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // No lock required as the delta applied to one thread. The delta is // still active. IndexWriter writer = indexWriters.remove(id); if (writer != null) { writer.close(); } } /** * Make sure the writer and reader for TX data are closed. * * @param id String * @throws IOException */ public void closeDelta(String id) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } closeDeltaIndexReader(id); closeDeltaIndexWriter(id); } /** * Get the deletions for a given index (there is no check if they should be applied that is up to the calling layer) * * @param id String * @throws IOException */ public Set<String> getDeletions(String id) throws IOException { return getDeletions(id, INDEX_INFO_DELETIONS); } /** * Get the deletions for a given index (there is no check if they should be applied that is up to the calling layer) * * @param id String * @param fileName String * @return Set<String> * @throws IOException */ private Set<String> getDeletions(String id, String fileName) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // Check state Set<String> deletions = new HashSet<String>(); File location = new File(indexDirectory, id).getCanonicalFile(); File file = new File(location, fileName).getCanonicalFile(); if (!file.exists()) { if (s_logger.isDebugEnabled()) { s_logger.debug("No deletions for " + id); } return Collections.<String>emptySet(); } DataInputStream is = new DataInputStream(new BufferedInputStream(new FileInputStream(file))); int size = is.readInt(); for (int i = 0; i < size; i++) { String ref = is.readUTF(); deletions.add(ref); } is.close(); if (s_logger.isDebugEnabled()) { s_logger.debug("There are " + deletions.size() + " deletions for " + id); } return deletions; } /** * Set the aux data for the index entry for a transactional unit of work. * * @param id - * the tx id * @param toDelete - * noderefs that should be deleted from previous indexes (not this one) * @param documents - * the number of docs in the index * @param deleteNodesOnly - * should deletions on apply to nodes (ie not to containers) * @throws IOException */ public void setPreparedState(String id, Set<String> toDelete, Set<String> containersToDelete, long documents, boolean deleteNodesOnly) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } // Check state int toDeleteSize = toDelete.size(); int containersToDeleteSize = containersToDelete.size(); if (toDeleteSize > 0) { persistDeletions(id, toDelete, INDEX_INFO_DELETIONS); } if (containersToDeleteSize > 0) { persistDeletions(id, containersToDelete, INDEX_INFO_CONTAINER_DELETIONS); } getWriteLock(); try { IndexEntry entry = indexEntries.get(id); if (entry == null) { throw new IndexerException("Invalid index delta id " + id); } if ((entry.getStatus() != TransactionStatus.PREPARING) && (entry.getStatus() != TransactionStatus.COMMITTING)) { throw new IndexerException("Deletes and doc count can only be set on a preparing index"); } entry.setDocumentCount(documents); entry.setDeletions(toDeleteSize + containersToDeleteSize); entry.setDeletOnlyNodes(deleteNodesOnly); } finally { releaseWriteLock(); } } /** * @param id String * @param toDelete Set<String> * @param fileName String * @throws IOException * @throws FileNotFoundException */ private void persistDeletions(String id, Set<String> toDelete, String fileName) throws IOException, FileNotFoundException { File location = new File(indexDirectory, id).getCanonicalFile(); if (!location.exists()) { if (!location.mkdirs()) { throw new IndexerException("Failed to make index directory " + location); } } // Write deletions DataOutputStream os = new DataOutputStream( new BufferedOutputStream(new FileOutputStream(new File(location, fileName).getCanonicalFile()))); os.writeInt(toDelete.size()); for (String ref : toDelete) { os.writeUTF(ref); } os.flush(); os.close(); } private void invalidateMainReadersFromFirst(Set<String> ids) throws IOException { boolean found = false; for (String id : indexEntries.keySet()) { if (!found && ids.contains(id)) { found = true; } if (found) { IndexReader main = mainIndexReaders.remove(id); if (main != null) { ((ReferenceCounting) main).setInvalidForReuse(); } } } if (found) { if (mainIndexReader != null) { ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); mainIndexReader = null; } } } /** * Get the main reader for committed index data * * @return IndexReader * @throws IOException */ public IndexReader getMainIndexReferenceCountingReadOnlyIndexReader() throws IOException { getReadLock(); try { // Check if we need to rebuild the main indexer as it is invalid. // (it is shared and quick version check fails) if (indexIsShared && !checkVersion()) { releaseReadLock(); getWriteLock(); try { if (mainIndexReader != null) { ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); } mainIndexReader = null; } finally { getReadLock(); releaseWriteLock(); } } // Build if required if (mainIndexReader == null) { releaseReadLock(); getWriteLock(); try { if (mainIndexReader == null) { // Sync with disk image if required doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { return null; } public boolean canRetry() { return true; } }); mainIndexReader = createMainIndexReader(); } } finally { getReadLock(); releaseWriteLock(); } } // Manage reference counting mainIndexReader.incRef(); if (s_logger.isDebugEnabled()) { s_logger.debug("Main index reader references = " + ((ReferenceCounting) mainIndexReader).getReferenceCount()); } // ALF-10040: Wrap with a one-off CachingIndexReader (with cache disabled) so that LeafScorer behaves and passes through SingleFieldSelectors to the main index readers IndexReader reader = ReferenceCountingReadOnlyIndexReaderFactory .createReader(MAIN_READER + GUID.generate(), mainIndexReader, false, config); ReferenceCounting refCounting = (ReferenceCounting) reader; reader.incRef(); refCounting.setInvalidForReuse(); return reader; } catch (RuntimeException e) { e.printStackTrace(); throw e; } finally { releaseReadLock(); } } /** * Get the main index reader augmented with the specified TX data As above but we add the TX data * * @param id String * @param deleteOnlyNodes boolean * @return IndexReader * @throws IOException */ public IndexReader getMainIndexReferenceCountingReadOnlyIndexReader(String id, Set<String> deletions, Set<String> containerDeletions, boolean deleteOnlyNodes) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } getReadLock(); try { if (indexIsShared && !checkVersion()) { releaseReadLock(); getWriteLock(); try { if (mainIndexReader != null) { ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); } mainIndexReader = null; } finally { getReadLock(); releaseWriteLock(); } } if (mainIndexReader == null) { releaseReadLock(); getWriteLock(); try { if (mainIndexReader == null) { // Sync with disk image if required doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { return null; } public boolean canRetry() { return true; } }); mainIndexReader = createMainIndexReader(); } } finally { getReadLock(); releaseWriteLock(); } } // Combine the index delta with the main index // Make sure the index is written to disk // TODO: Should use the in memory index but we often end up forcing // to disk anyway. // Is it worth it? // luceneIndexer.flushPending(); IndexReader deltaReader = buildAndRegisterDeltaReader(id); IndexReader reader = null; if ((deletions == null || deletions.size() == 0) && (containerDeletions == null || containerDeletions.size() == 0)) { reader = new MultiReader(new IndexReader[] { mainIndexReader, deltaReader }, false); } else { IndexReader filterReader = new FilterIndexReaderByStringId("main+id", mainIndexReader, deletions, containerDeletions, deleteOnlyNodes); reader = new MultiReader(new IndexReader[] { filterReader, deltaReader }, false); // Cancel out extra incRef made by MultiReader filterReader.decRef(); } // The reference count would have been incremented automatically by MultiReader / // FilterIndexReaderByStringId deltaReader.decRef(); if (s_logger.isDebugEnabled()) { s_logger.debug("Main index reader references = " + ((ReferenceCounting) mainIndexReader).getReferenceCount()); } reader = ReferenceCountingReadOnlyIndexReaderFactory.createReader(MAIN_READER + id, reader, false, config); ReferenceCounting refCounting = (ReferenceCounting) reader; reader.incRef(); refCounting.setInvalidForReuse(); return reader; } finally { releaseReadLock(); } } private boolean shouldBlock() { int pendingDeltas = 0; int maxDeltas = mergerTargetOverlaysBlockingFactor * mergerTargetOverlays; for (IndexEntry entry : indexEntries.values()) { if (entry.getType() == IndexType.DELTA) { TransactionStatus status = entry.getStatus(); if (status == TransactionStatus.PREPARED || status == TransactionStatus.COMMITTING || status.isCommitted()) { if (++pendingDeltas > maxDeltas) { return true; } } } } return false; } public void setStatus(final String id, final TransactionStatus state, final Set<Term> toDelete, final Set<Term> read) throws IOException { if (id == null) { throw new IndexerException("\"null\" is not a valid identifier for a transaction"); } final Transition transition = getTransition(state); getReadLock(); try { transition.beforeWithReadLock(id, toDelete, read); releaseReadLock(); getWriteLock(); try { // we may need to block for some deltas to be merged / rolled back IndexInfo alreadyPreparing = thisThreadPreparing.get(); if (state == TransactionStatus.PREPARED) { // To avoid deadlock (a thread with multiple deltas never proceeding to commit) we don't block if // this thread is already in the prepare phase if (alreadyPreparing != null) { if (s_logger.isDebugEnabled()) { s_logger.debug( "Can't throttle - " + Thread.currentThread().getName() + " already preparing"); } } else { while (shouldBlock()) { synchronized (mergerTargetLock) { if (s_logger.isDebugEnabled()) { s_logger.debug("THROTTLING: " + Thread.currentThread().getName() + " " + indexEntries.size()); } merger.schedule(); releaseWriteLock(); try { mergerTargetLock.wait(60000); } catch (InterruptedException e) { } } getWriteLock(); } thisThreadPreparing.set(this); } } else { // Only clear the flag when the outermost thread exits prepare if (alreadyPreparing == this) { thisThreadPreparing.set(null); } } if (transition.requiresFileLock()) { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { if (s_logger.isDebugEnabled()) { s_logger.debug("Start Index " + id + " state = " + state); } dumpInfo(); transition.transition(id, toDelete, read); if (s_logger.isDebugEnabled()) { s_logger.debug("End Index " + id + " state = " + state); } dumpInfo(); return null; } public boolean canRetry() { return true; } }); } else { if (s_logger.isDebugEnabled()) { s_logger.debug("Start Index " + id + " state = " + state); } dumpInfo(); transition.transition(id, toDelete, read); if (s_logger.isDebugEnabled()) { s_logger.debug("End Index " + id + " state = " + state); } dumpInfo(); } } finally { getReadLock(); releaseWriteLock(); } } finally { releaseReadLock(); } } // // Internal support for status management // private Transition getTransition(TransactionStatus state) { Transition transition = transitions.get(state); if (transition != null) { return transition; } else { throw new IndexerException("Invalid state " + state); } } /** * Initialise the definitions for the available transitions. */ private void initialiseTransitions() { transitions.put(TransactionStatus.PREPARING, new PreparingTransition()); transitions.put(TransactionStatus.PREPARED, new PreparedTransition()); transitions.put(TransactionStatus.COMMITTING, new CommittingTransition()); transitions.put(TransactionStatus.COMMITTED, new CommittedTransition()); transitions.put(TransactionStatus.ROLLINGBACK, new RollingBackTransition()); transitions.put(TransactionStatus.ROLLEDBACK, new RolledBackTransition()); transitions.put(TransactionStatus.DELETABLE, new DeletableTransition()); transitions.put(TransactionStatus.ACTIVE, new ActiveTransition()); } /** * API for transitions * * @author andyh */ private interface Transition { void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException; void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException; boolean requiresFileLock(); } /** * Transition to the perparing state * * @author andyh */ private class PreparingTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { // Nothing to do } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.PREPARING.follows(entry.getStatus())) { entry.setStatus(TransactionStatus.PREPARING); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.PREPARING); } } public boolean requiresFileLock() { return !TransactionStatus.PREPARING.isTransient(); } } /** * Transition to the prepared state. * * @author andyh */ private class PreparedTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.PREPARED.follows(entry.getStatus())) { LinkedHashMap<String, IndexEntry> reordered = new LinkedHashMap<String, IndexEntry>(); boolean addedPreparedEntry = false; for (String key : indexEntries.keySet()) { IndexEntry current = indexEntries.get(key); if (!current.getStatus().canBeReordered()) { reordered.put(current.getName(), current); } else if (!addedPreparedEntry) { reordered.put(entry.getName(), entry); reordered.put(current.getName(), current); addedPreparedEntry = true; invalidateMainReadersFromFirst(Collections.singleton(current.getName())); } else if (current.getName().equals(entry.getName())) { // skip as we are moving it } else { reordered.put(current.getName(), current); } } if (indexEntries.size() != reordered.size()) { indexEntries = reordered; dumpInfo(); throw new IndexerException("Concurrent modification error"); } indexEntries = reordered; entry.setStatus(TransactionStatus.PREPARED); writeStatus(); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.PREPARED); } } public boolean requiresFileLock() { return !TransactionStatus.PREPARED.isTransient(); } } private class CommittingTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.COMMITTING.follows(entry.getStatus())) { entry.setStatus(TransactionStatus.COMMITTING); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.COMMITTING); } } public boolean requiresFileLock() { return !TransactionStatus.COMMITTING.isTransient(); } } private class CommittedTransition implements Transition { ThreadLocal<IndexReader> tl = new ThreadLocal<IndexReader>(); public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { // Make sure we have set up the reader for the data // ... and close it so we do not up the ref count closeDelta(id); IndexEntry entry = indexEntries.get(id); tl.set(buildReferenceCountingIndexReader(id, entry.getDocumentCount())); } /** * This has to be protected to allow for retry */ public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { clearOldReaders(); cleaner.schedule(); throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.COMMITTED.follows(entry.getStatus())) { // Do the deletions invalidateMainReadersFromFirst(Collections.singleton(id)); if ((entry.getDocumentCount() + entry.getDeletions()) == 0) { registerReferenceCountingIndexReader(id, tl.get()); indexEntries.remove(id); if (s_logger.isDebugEnabled()) { s_logger.debug("Removed commit with no new docs and no deletions"); } clearOldReaders(); cleaner.schedule(); } else { registerReferenceCountingIndexReader(id, tl.get()); entry.setStatus(TransactionStatus.COMMITTED); // TODO: optimise to index for no deletions // have to allow for this in the application of deletions, writeStatus(); if (mainIndexReader != null) { if (s_logger.isDebugEnabled()) { s_logger.debug("... invalidating main index reader"); } ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); mainIndexReader = null; } merger.schedule(); } } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.COMMITTED); } notifyListeners("CommittedTransactions", 1); } public boolean requiresFileLock() { return !TransactionStatus.COMMITTED.isTransient(); } } private class RollingBackTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.ROLLINGBACK.follows(entry.getStatus())) { entry.setStatus(TransactionStatus.ROLLINGBACK); writeStatus(); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.ROLLINGBACK); } } public boolean requiresFileLock() { return !TransactionStatus.ROLLINGBACK.isTransient(); } } private class RolledBackTransition implements Transition { ThreadLocal<IndexReader> tl = new ThreadLocal<IndexReader>(); public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { closeDelta(id); IndexEntry entry = indexEntries.get(id); tl.set(buildReferenceCountingIndexReader(id, entry.getDocumentCount())); } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { clearOldReaders(); cleaner.schedule(); throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.ROLLEDBACK.follows(entry.getStatus())) { entry.setStatus(TransactionStatus.ROLLEDBACK); writeStatus(); registerReferenceCountingIndexReader(id, tl.get()); indexEntries.remove(id); if (s_logger.isDebugEnabled()) { s_logger.debug("Removed rollback"); } clearOldReaders(); cleaner.schedule(); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.ROLLEDBACK); } } public boolean requiresFileLock() { return !TransactionStatus.ROLLEDBACK.isTransient(); } } private class DeletableTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry == null) { clearOldReaders(); cleaner.schedule(); throw new IndexerException("Unknown transaction " + id); } if (TransactionStatus.DELETABLE.follows(entry.getStatus())) { invalidateMainReadersFromFirst(Collections.singleton(id)); indexEntries.remove(id); writeStatus(); clearOldReaders(); cleaner.schedule(); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.DELETABLE); } } public boolean requiresFileLock() { return !TransactionStatus.DELETABLE.isTransient(); } } private class ActiveTransition implements Transition { public void beforeWithReadLock(String id, Set<Term> toDelete, Set<Term> read) throws IOException { } public void transition(String id, Set<Term> toDelete, Set<Term> read) throws IOException { IndexEntry entry = indexEntries.get(id); if (entry != null) { if (entry.getStatus() != TransactionStatus.ACTIVE) { throw new IndexerException("TX Already active " + id); } } if (TransactionStatus.ACTIVE.follows(null)) { indexEntries.put(id, new IndexEntry(IndexType.DELTA, id, "", TransactionStatus.ACTIVE, "", 0, 0, false)); } else { throw new IndexerException("Invalid transition for " + id + " from " + entry.getStatus() + " to " + TransactionStatus.ACTIVE); } } public boolean requiresFileLock() { return !TransactionStatus.ACTIVE.isTransient(); } } // // // Internal methods for implementation support // =========================================== // // These methods should all be called with the appropriate locks. // // private static boolean createFile(File file) { if (!file.exists()) { try { file.createNewFile(); return true; } catch (IOException e) { throw new AlfrescoRuntimeException("Failed to create info file", e); } } return false; } private static RandomAccessFile openFile(File file) { try { if (useNIOMemoryMapping) { return new RandomAccessFile(file, "rw"); } else { return new RandomAccessFile(file, "rws"); } } catch (FileNotFoundException e) { throw new AlfrescoRuntimeException("Failed to open index info file", e); } } /** * Check status must be called holding the file lock. * * @throws IOException */ private void setStatusFromFile() throws IOException { try { setStatusFromFile(indexInfoChannel); } catch (IOException e) { // The first data file is corrupt so we fall back to the back up setStatusFromFile(indexInfoBackupChannel); } clearOldReaders(); } private void clearOldReaders() throws IOException { // Find current invalid HashSet<String> inValid = new HashSet<String>(); for (String id : referenceCountingReadOnlyIndexReaders.keySet()) { if (!indexEntries.containsKey(id)) { if (s_logger.isDebugEnabled()) { s_logger.debug(id + " is now INVALID "); } inValid.add(id); } else { if (s_logger.isDebugEnabled()) { s_logger.debug(id + " is still part of the index "); } } } // Clear invalid clearInvalid(inValid); } private void clearInvalid(Set<String> inValid) throws IOException { boolean hasInvalid = false; for (String id : inValid) { IndexReader reader = referenceCountingReadOnlyIndexReaders.remove(id); if (s_logger.isDebugEnabled()) { s_logger.debug("... invalidating sub reader " + id); } if (reader != null) { ReferenceCounting referenceCounting = (ReferenceCounting) reader; referenceCounting.setInvalidForReuse(); deletableReaders.add(reader); hasInvalid = true; } } if (hasInvalid) { for (String id : inValid) { IndexReader main = mainIndexReaders.remove(id); if (main != null) { ((ReferenceCounting) main).setInvalidForReuse(); } } if (mainIndexReader != null) { if (s_logger.isDebugEnabled()) { s_logger.debug("... invalidating main index reader"); } ((ReferenceCounting) mainIndexReader).setInvalidForReuse(); } mainIndexReader = null; } } private IndexReader createMainIndexReader() throws IOException { IndexReader reader = null; IndexReader oldReader = null; for (String id : indexEntries.keySet()) { IndexEntry entry = indexEntries.get(id); if (entry.getStatus().isCommitted()) { IndexReader subReader = getReferenceCountingIndexReader(id); if (reader == null) { reader = subReader; } else { boolean oldReaderIsSubReader = oldReader == null; oldReader = reader; reader = mainIndexReaders.get(id); if (reader == null) { if (entry.getType() == IndexType.INDEX) { reader = new MultiReader(new IndexReader[] { oldReader, subReader }, false); } else if (entry.getType() == IndexType.DELTA) { try { IndexReader filterReader = new FilterIndexReaderByStringId(id, oldReader, getDeletions(entry.getName(), INDEX_INFO_DELETIONS), getDeletions(entry.getName(), INDEX_INFO_CONTAINER_DELETIONS), entry.isDeletOnlyNodes()); reader = new MultiReader(new IndexReader[] { filterReader, subReader }, false); // Cancel out the incRef on the filter reader filterReader.decRef(); } catch (IOException ioe) { s_logger.error("Failed building filter reader beneath " + entry.getName(), ioe); throw ioe; } } reader = ReferenceCountingReadOnlyIndexReaderFactory.createReader(id + "multi", reader, true, config); mainIndexReaders.put(id, reader); } } } } if (reader == null) { reader = IndexReader.open(emptyIndex); } else { // Keep this reader open whilst it is referenced by mainIndexReaders / referenceCountingReadOnlyIndexReaders reader.incRef(); } reader = ReferenceCountingReadOnlyIndexReaderFactory.createReader(MAIN_READER, reader, false, config); return reader; } private IndexReader getReferenceCountingIndexReader(String id) throws IOException { IndexReader reader = referenceCountingReadOnlyIndexReaders.get(id); if (reader == null) { throw new IllegalStateException("Indexer should have been pre-built for " + id); } return reader; } private void registerReferenceCountingIndexReader(String id, IndexReader reader) throws IOException { ReferenceCounting referenceCounting = (ReferenceCounting) reader; if (!referenceCounting.getId().equals(id)) { throw new IllegalStateException("Registering " + referenceCounting.getId() + " as " + id); } // ALF-13981: Be careful not to invalidate the segment reader if we are trying to re-register exactly the same // one (e.g. in a doWithFileLock() retry loop) if (referenceCountingReadOnlyIndexReaders.get(id) != reader) { clearInvalid(Collections.singleton(id)); referenceCountingReadOnlyIndexReaders.put(id, reader); } } private double getSizeInMb(File file) { long size = getSize(file); return size / 1024.0d / 1024.0d; } private long getSize(File file) { long size = 0l; if (file == null) { return size; } if (file.isFile()) { return file.length(); } else { File[] files = file.listFiles(); if (files == null) { return size; } for (File current : files) { if (current.isDirectory()) { size += getSize(current); } else { size += current.length(); } } } return size; } private IndexReader buildReferenceCountingIndexReader(String id, long size) throws IOException { IndexReader reader; File location = new File(indexDirectory, id).getCanonicalFile(); double folderSize = getSizeInMb(location); if (IndexReader.indexExists(location)) { if ((size < maxDocsForInMemoryIndex) && (folderSize < maxRamInMbForInMemoryIndex)) { RAMDirectory rd = new RAMDirectory(location); reader = IndexReader.open(rd); } else { reader = IndexReader.open(location); } } else { reader = IndexReader.open(emptyIndex); } reader = ReferenceCountingReadOnlyIndexReaderFactory.createReader(id, reader, true, config); return reader; } private boolean checkVersion() throws IOException { try { return checkVersion(indexInfoChannel); } catch (IOException e) { // The first data file is corrupt so we fall back to the back up try { return checkVersion(indexInfoBackupChannel); } catch (IOException ee) { return false; } } } private boolean checkVersion(FileChannel channel) throws IOException { if (channel.size() > 0) { channel.position(0); ByteBuffer buffer; if (useNIOMemoryMapping) { MappedByteBuffer mbb = channel.map(MapMode.READ_ONLY, 0, 8); mbb.load(); buffer = mbb; } else { buffer = ByteBuffer.wrap(new byte[8]); channel.read(buffer); buffer.position(0); } buffer.position(0); long onDiskVersion = buffer.getLong(); return (version == onDiskVersion); } return (version == 0); } private void setStatusFromFile(FileChannel channel) throws IOException { if (channel.size() > 0) { channel.position(0); ByteBuffer buffer; if (useNIOMemoryMapping) { MappedByteBuffer mbb = channel.map(MapMode.READ_ONLY, 0, channel.size()); mbb.load(); buffer = mbb; } else { buffer = ByteBuffer.wrap(new byte[(int) channel.size()]); channel.read(buffer); buffer.position(0); } buffer.position(0); long onDiskVersion = buffer.getLong(); if (version != onDiskVersion) { CRC32 crc32 = new CRC32(); crc32.update((int) (onDiskVersion >>> 32) & 0xFFFFFFFF); crc32.update((int) (onDiskVersion >>> 0) & 0xFFFFFFFF); int size = buffer.getInt(); crc32.update(size); LinkedHashMap<String, IndexEntry> newIndexEntries = new LinkedHashMap<String, IndexEntry>(); // Not all state is saved some is specific to this index so we // need to add the transient stuff. // Until things are committed they are not shared unless it is // prepared for (int i = 0; i < size; i++) { String indexTypeString = readString(buffer, crc32); IndexType indexType; try { indexType = IndexType.valueOf(indexTypeString); } catch (IllegalArgumentException e) { throw new IOException("Invalid type " + indexTypeString); } String name = readString(buffer, crc32); String parentName = readString(buffer, crc32); String txStatus = readString(buffer, crc32); TransactionStatus status; try { status = TransactionStatus.valueOf(txStatus); } catch (IllegalArgumentException e) { throw new IOException("Invalid status " + txStatus); } String mergeId = readString(buffer, crc32); long documentCount = buffer.getLong(); crc32.update((int) (documentCount >>> 32) & 0xFFFFFFFF); crc32.update((int) (documentCount >>> 0) & 0xFFFFFFFF); long deletions = buffer.getLong(); crc32.update((int) (deletions >>> 32) & 0xFFFFFFFF); crc32.update((int) (deletions >>> 0) & 0xFFFFFFFF); byte deleteOnlyNodesFlag = buffer.get(); crc32.update(deleteOnlyNodesFlag); boolean isDeletOnlyNodes = deleteOnlyNodesFlag == 1; if (!status.isTransient()) { newIndexEntries.put(name, new IndexEntry(indexType, name, parentName, status, mergeId, documentCount, deletions, isDeletOnlyNodes)); } } long onDiskCRC32 = buffer.getLong(); if (crc32.getValue() == onDiskCRC32) { for (IndexEntry entry : indexEntries.values()) { if (entry.getStatus().isTransient()) { newIndexEntries.put(entry.getName(), entry); } } version = onDiskVersion; indexEntries = newIndexEntries; } else { throw new IOException("Invalid file check sum"); } } } } private String readString(ByteBuffer buffer, CRC32 crc32) throws UnsupportedEncodingException { int size = buffer.getInt(); byte[] bytes = new byte[size]; buffer.get(bytes); char[] chars = new char[size]; for (int i = 0; i < size; i++) { chars[i] = (char) bytes[i]; } crc32.update(bytes); return new String(chars); } private void writeString(ByteBuffer buffer, CRC32 crc32, String string) throws UnsupportedEncodingException { char[] chars = string.toCharArray(); byte[] bytes = new byte[chars.length]; for (int i = 0; i < chars.length; i++) { if (chars[i] > 0xFF) { throw new UnsupportedEncodingException(); } bytes[i] = (byte) chars[i]; } buffer.putInt(bytes.length); buffer.put(bytes); crc32.update(bytes); } private void writeStatus() throws IOException { version++; writeStatusToFile(indexInfoChannel); writeStatusToFile(indexInfoBackupChannel); // We have a state that allows more transactions. Notify waiting threads if (!shouldBlock()) { synchronized (mergerTargetLock) { mergerTargetLock.notifyAll(); } } } private void writeStatusToFile(FileChannel channel) throws IOException { long size = getBufferSize(); ByteBuffer buffer; if (useNIOMemoryMapping) { MappedByteBuffer mbb = channel.map(MapMode.READ_WRITE, 0, size); mbb.load(); buffer = mbb; } else { channel.truncate(size); buffer = ByteBuffer.wrap(new byte[(int) size]); } buffer.position(0); buffer.putLong(version); CRC32 crc32 = new CRC32(); crc32.update((int) (version >>> 32) & 0xFFFFFFFF); crc32.update((int) (version >>> 0) & 0xFFFFFFFF); buffer.putInt(indexEntries.size()); crc32.update(indexEntries.size()); for (IndexEntry entry : indexEntries.values()) { String entryType = entry.getType().toString(); writeString(buffer, crc32, entryType); writeString(buffer, crc32, entry.getName()); writeString(buffer, crc32, entry.getParentName()); String entryStatus = entry.getStatus().toString(); writeString(buffer, crc32, entryStatus); writeString(buffer, crc32, entry.getMergeId()); buffer.putLong(entry.getDocumentCount()); crc32.update((int) (entry.getDocumentCount() >>> 32) & 0xFFFFFFFF); crc32.update((int) (entry.getDocumentCount() >>> 0) & 0xFFFFFFFF); buffer.putLong(entry.getDeletions()); crc32.update((int) (entry.getDeletions() >>> 32) & 0xFFFFFFFF); crc32.update((int) (entry.getDeletions() >>> 0) & 0xFFFFFFFF); buffer.put(entry.isDeletOnlyNodes() ? (byte) 1 : (byte) 0); crc32.update(entry.isDeletOnlyNodes() ? new byte[] { (byte) 1 } : new byte[] { (byte) 0 }); } buffer.putLong(crc32.getValue()); if (useNIOMemoryMapping) { ((MappedByteBuffer) buffer).force(); } else { buffer.rewind(); channel.position(0); channel.write(buffer); } } private long getBufferSize() throws IOException { long size = 0; size += 8; size += 4; for (IndexEntry entry : indexEntries.values()) { String entryType = entry.getType().toString(); size += (entryType.length()) + 4; size += (entry.getName().length()) + 4; size += (entry.getParentName().length()) + 4; String entryStatus = entry.getStatus().toString(); size += (entryStatus.length()) + 4; size += (entry.getMergeId().length()) + 4; size += 8; size += 8; size += 1; } size += 8; return size; } public interface LockWork<Result> { public Result doWork() throws Exception; public boolean canRetry(); } public <R> R doReadOnly(LockWork<R> lockWork) { readOnlyLock.writeLock().lock(); try { getReadLock(); try { return doWithFileLock(lockWork); } finally { releaseReadLock(); } } finally { readOnlyLock.writeLock().unlock(); } } private static final int CHANNEL_OPEN_RETRIES = 5; private <R> R doWithFileLock(LockWork<R> lockWork) { try { return doWithFileLock(lockWork, CHANNEL_OPEN_RETRIES); } catch (Throwable e) { // Re-throw the exception if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException("Error during run with lock.", e); } } } /** * Specific exception to catch channel close issues. * * @author Derek Hulley * @since 2.1.3 */ private static class IndexInfoChannelException extends IOException { /** * */ private static final long serialVersionUID = 1588898991653057286L; public IndexInfoChannelException(String msg) { super(msg); } } /** * An iterative method that retries the operation in the event of the channel being closed. * * @param retriesRemaining * the number of retries remaining * @return Returns the lock work result */ private <R> R doWithFileLock(LockWork<R> lockWork, int retriesRemaining) throws Throwable { FileLock fileLock = null; R result = null; long start = 0L; try { // Check that the channel is open if (!indexInfoChannel.isOpen()) { if (lockWork.canRetry()) { throw new IndexInfoChannelException("Channel is closed. Manually triggering reopen attempts"); } else { reopenChannels(); } } if (indexIsShared) { if (s_logger.isDebugEnabled()) { s_logger.debug(" ... waiting for file lock"); start = System.nanoTime(); } fileLock = indexInfoChannel.lock(); if (s_logger.isDebugEnabled()) { long end = System.nanoTime(); s_logger.debug(" ... got file lock in " + ((end - start) / 10e6f) + " ms"); } if (!checkVersion()) { setStatusFromFile(); } } result = lockWork.doWork(); return result; } catch (IOException e) { if (!lockWork.canRetry()) { // We've done our best s_logger.warn( "This operation can not retry upon an IOException - it has to roll back to its previous state"); throw e; } if (retriesRemaining == 0) { // We've done our best s_logger.warn("No more channel open retries remaining"); throw e; } else { // Attempt to reopen the channel if (s_logger.isDebugEnabled()) { s_logger.debug("\n" + "Channel is closed. Will attempt to open it. \n" + " Retries remaining: " + retriesRemaining); } try { reopenChannels(); // Loop around and try again return doWithFileLock(lockWork, --retriesRemaining); } catch (Throwable ee) { // Report this error, but throw the original s_logger.error("Channel reopen failed on index info files in: " + this.indexDirectory, ee); throw e; } } } finally { if (fileLock != null) { try { fileLock.release(); long end = System.nanoTime(); if (s_logger.isDebugEnabled()) { s_logger.debug(" ... released file lock after " + ((end - start) / 10e6f) + " ms"); } } catch (IOException e) { s_logger.warn("Failed to release file lock: " + e.getMessage(), e); } } } } /** * Reopens all the channels. The channels are closed first. This method is synchronized. */ private synchronized void reopenChannels() throws Throwable { try { indexInfoRAF.close(); } catch (IOException e) { s_logger.warn("Failed to close indexInfoRAF", e); } try { indexInfoBackupRAF.close(); } catch (IOException e) { s_logger.warn("Failed to close indexInfoRAF", e); } File indexInfoFile = new File(this.indexDirectory, INDEX_INFO); File indexInfoBackupFile = new File(this.indexDirectory, INDEX_INFO_BACKUP); // Open the files and channels for the index info file and the backup this.indexInfoRAF = openFile(indexInfoFile); this.indexInfoChannel = this.indexInfoRAF.getChannel(); this.indexInfoBackupRAF = openFile(indexInfoBackupFile); this.indexInfoBackupChannel = this.indexInfoBackupRAF.getChannel(); } /** * Helper to print out index information * * @param args String[] * @throws Throwable */ public static void main(String[] args) throws Throwable { for (int i = 0; i < args.length; i++) { File indexLocation = new File(args[i]); if (!indexLocation.exists()) { System.err.println("Index directory doesn't exist: " + indexLocation); continue; } readIndexInfo(indexLocation); } } static Query getPathQuery(String path) throws SAXPathException { ApplicationContext ac = ApplicationContextHelper.getApplicationContext(); XPathReader reader = new XPathReader(); LuceneXPathHandler handler = new LuceneXPathHandler(); handler.setNamespacePrefixResolver((NamespaceService) ac.getBean("namespaceService")); handler.setDictionaryService((DictionaryService) ac.getBean("dictionaryService")); reader.setXPathHandler(handler); reader.parse(path); PathQuery pathQuery = handler.getQuery(); pathQuery.setRepeats(false); return pathQuery; } private static void readIndexInfo(File indexLocation) throws Throwable { long start; long end; IndexInfo ii = new IndexInfo(indexLocation, null); ii.readWriteLock.writeLock().lock(); try { System.out.println("Entry List for " + indexLocation); System.out.println(" Size = " + ii.indexEntries.size()); int i = 0; for (IndexEntry entry : ii.indexEntries.values()) { System.out.println("\t" + (i++) + "\t" + entry.toString()); } } finally { ii.releaseWriteLock(); } IndexReader reader = ii.getMainIndexReferenceCountingReadOnlyIndexReader(); System.out.println(reader.getFieldNames(FieldOption.ALL)); TermEnum te = reader.terms(); while (te.next()) { if (te.term().field().contains("FTS")) { System.out.println(te.term()); } } // @{http://www.alfresco.org/model/content/1.0}name:product363_ocmwbeersel IndexSearcher searcher = new IndexSearcher(reader); Query query = new TermQuery( new Term("@{http://www.alfresco.org/model/content/1.0}name", "product363_ocmwbeersel")); start = System.nanoTime(); Hits hits = searcher.search(query); end = System.nanoTime(); System.out.println("@{http://www.alfresco.org/model/content/1.0}name:product363_ocmwbeersel = " + hits.length() + " in " + ((end - start) / 1e9)); searcher.close(); searcher = new IndexSearcher(reader); query = new WildcardQuery(new Term("@{http://www.alfresco.org/model/content/1.0}name", "b*")); start = System.nanoTime(); hits = searcher.search(query); end = System.nanoTime(); System.out.println("@{http://www.alfresco.org/model/content/1.0}name:b* = " + hits.length() + " in " + ((end - start) / 1e9)); searcher.close(); searcher = new IndexSearcher(reader); query = new TermQuery(new Term("@{http://www.alfresco.org/model/content/1.0}name", "be")); start = System.nanoTime(); hits = searcher.search(query); end = System.nanoTime(); System.out.println("@{http://www.alfresco.org/model/content/1.0}name:be = " + hits.length() + " in " + ((end - start) / 1e9)); searcher.close(); } /** * Clean up support. * * @author Andy Hind */ private class Cleaner extends AbstractSchedulable { String getLogName() { return "Index cleaner"; } void runImpl() { Iterator<IndexReader> i = deletableReaders.iterator(); while (i.hasNext()) { IndexReader reader = i.next(); ReferenceCounting refCounting = (ReferenceCounting) reader; if (refCounting.getReferenceCount() == 0) { if (s_logger.isDebugEnabled()) { s_logger.debug("Deleting no longer referenced " + refCounting.getId()); s_logger.debug("... queued delete for " + refCounting.getId()); s_logger.debug( "... " + ReferenceCountingReadOnlyIndexReaderFactory.getState(refCounting.getId())); } getReadLock(); try { if (indexEntries.containsKey(refCounting.getId())) { s_logger.error("ERROR - deleting live reader - " + refCounting.getId()); } } finally { releaseReadLock(); } deleteQueue.add(refCounting.getId()); i.remove(); } else if (s_logger.isTraceEnabled() && refCounting.getCreationTime() < System.currentTimeMillis() - 120000) { for (Throwable t : refCounting.getReferences()) { s_logger.trace(t.getMessage(), t); } } } Iterator<String> j = deleteQueue.iterator(); while (j.hasNext()) { String id = j.next(); try { if (s_logger.isDebugEnabled()) { s_logger.debug("Expunging " + id + " remaining " + deleteQueue.size()); s_logger.debug("... " + ReferenceCountingReadOnlyIndexReaderFactory.getState(id)); } // try and delete File location = new File(indexDirectory, id).getCanonicalFile(); if (!deleteDirectory(location)) { if (s_logger.isDebugEnabled()) { s_logger.debug("DELETE FAILED"); } } else { j.remove(); } } catch (IOException ioe) { s_logger.warn("Failed to delete file - invalid canonical file", ioe); } } } ExitState recoverImpl() { return ExitState.DONE; } private boolean deleteDirectory(File file) { File[] children = file.listFiles(); if (children != null) { for (int i = 0; i < children.length; i++) { File child = children[i]; if (child.isDirectory()) { deleteDirectory(child); } else { if (child.exists() && !child.delete() && child.exists()) { return false; } } } } if (file.exists() && !file.delete() && file.exists()) { return false; } return true; } } /** * Supported by one thread. 1) If the first index is a delta we can just change it to an index. There is now here to * apply the deletions 2) Merge indexes Combine indexes together according to the target index merge strategy. This * is a trade off to make an optimised index but not spend too much time merging and optimising small merges. 3) * Apply next deletion set to indexes Apply the deletions for the first delta to all the other indexes. Deletes can * be applied with relative impunity. If any are applied they take effect as required. 1) 2) and 3) are mutually * exclusive try in order This could be supported in another thread 4) Merge deltas Merge two index deltas together. * Starting at the end. Several merges can be going on at once. a) Find merge b) Set state c) apply deletions to the * previous delta d) update state e) add deletions to the previous delta deletion list f) update state */ private enum MergeAction { NONE, MERGE_INDEX, APPLY_DELTA_DELETION, MERGE_DELTA } private enum ScheduledState { UN_SCHEDULED, SCHEDULED, FAILED, RECOVERY_SCHEDULED } private enum ExitState { DONE, RESCHEDULE; } private abstract class AbstractSchedulable implements Schedulable, Runnable { ScheduledState scheduledState = ScheduledState.UN_SCHEDULED; public synchronized void schedule() { switch (scheduledState) { case FAILED: scheduledState = ScheduledState.RECOVERY_SCHEDULED; threadPoolExecutor.execute(this); break; case UN_SCHEDULED: scheduledState = ScheduledState.SCHEDULED; threadPoolExecutor.execute(this); break; case RECOVERY_SCHEDULED: case SCHEDULED: default: // Nothing to do break; } } synchronized void done() { switch (scheduledState) { case RECOVERY_SCHEDULED: case SCHEDULED: scheduledState = ScheduledState.UN_SCHEDULED; break; case FAILED: case UN_SCHEDULED: default: throw new IllegalStateException(); } } private synchronized void rescheduleRecovery() { switch (scheduledState) { case RECOVERY_SCHEDULED: threadPoolExecutor.execute(this); break; case SCHEDULED: case FAILED: case UN_SCHEDULED: default: throw new IllegalStateException(); } } private synchronized void fail() { switch (scheduledState) { case RECOVERY_SCHEDULED: case SCHEDULED: scheduledState = ScheduledState.FAILED; break; case FAILED: case UN_SCHEDULED: default: throw new IllegalStateException(); } } public void run() { try { switch (scheduledState) { case RECOVERY_SCHEDULED: ExitState reschedule = recoverImpl(); s_logger.error(getLogName() + " has recovered - resuming ... "); if (reschedule == ExitState.RESCHEDULE) { rescheduleRecovery(); break; } case SCHEDULED: if (s_logger.isDebugEnabled()) { s_logger.debug(getLogName() + " running ... "); } runImpl(); done(); break; case FAILED: case UN_SCHEDULED: default: throw new IllegalStateException(); } } catch (Throwable t) { try { if (s_logger.isWarnEnabled()) { s_logger.warn(getLogName() + " failed with ", t); } recoverImpl(); if (s_logger.isWarnEnabled()) { s_logger.warn(getLogName() + " recovered from ", t); } done(); } catch (Throwable rbt) { fail(); s_logger.error(getLogName() + " failed to recover - suspending ", rbt); } } } abstract void runImpl() throws Exception; abstract ExitState recoverImpl() throws Exception; abstract String getLogName(); } private class Merger extends AbstractSchedulable { String getLogName() { return "Index merger"; } @Override void done() { // Reschedule if we need to, based on the current index state, that may have changed since we last got the // read lock getReadLock(); try { synchronized (this) { if (decideMergeAction() != MergeAction.NONE) { if (s_logger.isDebugEnabled()) { s_logger.debug(getLogName() + " rescheduling ... "); } switch (scheduledState) { case RECOVERY_SCHEDULED: scheduledState = ScheduledState.SCHEDULED; case SCHEDULED: threadPoolExecutor.execute(this); break; case FAILED: case UN_SCHEDULED: default: throw new IllegalStateException(); } } else { if (s_logger.isDebugEnabled()) { s_logger.debug(getLogName() + " done "); } super.done(); } } } finally { releaseReadLock(); } } void runImpl() throws IOException { // Get the read lock to decide what to do // Single JVM to start with MergeAction action; getReadLock(); try { if (indexIsShared && !checkVersion()) { releaseReadLock(); getWriteLock(); try { // Sync with disk image if required doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { return null; } public boolean canRetry() { return true; } }); } finally { getReadLock(); releaseWriteLock(); } } action = decideMergeAction(); } catch (IOException e) { s_logger.error("Error reading index file", e); return; } finally { releaseReadLock(); } if (s_logger.isDebugEnabled()) { s_logger.debug(getLogName() + " Merger applying MergeAction." + action.toString()); } if (action == MergeAction.APPLY_DELTA_DELETION) { mergeDeletions(); } else if (action == MergeAction.MERGE_INDEX) { mergeIndexes(); } if (s_logger.isDebugEnabled()) { dumpInfo(); } } private MergeAction decideMergeAction() { MergeAction action = MergeAction.NONE; int indexes = 0; boolean mergingIndexes = false; int deltas = 0; boolean applyingDeletions = false; for (IndexEntry entry : indexEntries.values()) { if (entry.getType() == IndexType.INDEX) { indexes++; if ((entry.getStatus() == TransactionStatus.MERGE) || (entry.getStatus() == TransactionStatus.MERGE_TARGET)) { mergingIndexes = true; } } else if (entry.getType() == IndexType.DELTA) { if (entry.getStatus() == TransactionStatus.COMMITTED) { deltas++; } if (entry.getStatus() == TransactionStatus.COMMITTED_DELETING) { applyingDeletions = true; deltas++; } } } if (s_logger.isDebugEnabled()) { s_logger.debug("Indexes = " + indexes); s_logger.debug("Merging = " + mergingIndexes); s_logger.debug("Deltas = " + deltas); s_logger.debug("Deleting = " + applyingDeletions); } if (!mergingIndexes && !applyingDeletions) { if (indexes > mergerTargetIndexes) { // Try merge action = MergeAction.MERGE_INDEX; } else if (deltas > mergerTargetOverlays) { // Try delete action = MergeAction.APPLY_DELTA_DELETION; } } return action; } ExitState recoverImpl() { getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { setStatusFromFile(); // If the index is not shared we can do some easy clean // up if (!indexIsShared) { HashSet<String> deletable = new HashSet<String>(); // clean up for (IndexEntry entry : indexEntries.values()) { switch (entry.getStatus()) { // states which can be deleted // We could check prepared states can be // committed. case ACTIVE: case MARKED_ROLLBACK: case NO_TRANSACTION: case PREPARING: case ROLLEDBACK: case ROLLINGBACK: case UNKNOWN: case PREPARED: case DELETABLE: case COMMITTING: case COMMITTED: default: if (s_logger.isInfoEnabled()) { s_logger.info("Roll back merge: leaving index entry " + entry); } break; // States which are in mid-transition which we // can roll back to the committed state case COMMITTED_DELETING: case MERGE: if (s_logger.isInfoEnabled()) { s_logger.info( "Roll back merge: Resetting merge and committed_deleting to committed " + entry); } entry.setStatus(TransactionStatus.COMMITTED); break; case MERGE_TARGET: if (s_logger.isInfoEnabled()) { s_logger.info("Roll back merge: Deleting merge target " + entry); } entry.setStatus(TransactionStatus.DELETABLE); deletable.add(entry.getName()); break; } // Check we have a reader registered if (referenceCountingReadOnlyIndexReaders.get(entry.getName()) == null) { registerReferenceCountingIndexReader(entry.getName(), buildReferenceCountingIndexReader(entry.getName(), entry.getDocumentCount())); } } if (mainIndexReader != null) { ReferenceCounting rcMain = (ReferenceCounting) mainIndexReader; if (rcMain.isInvalidForReuse()) { mainIndexReader = null; } } // Delete entries that are not required invalidateMainReadersFromFirst(deletable); for (String id : deletable) { indexEntries.remove(id); } clearOldReaders(); cleaner.schedule(); // persist the new state writeStatus(); } return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } return ExitState.DONE; } void mergeDeletions() throws IOException { if (s_logger.isDebugEnabled()) { s_logger.debug("Deleting ..."); } // lock for deletions final LinkedHashMap<String, IndexEntry> toDelete; LinkedHashMap<String, IndexEntry> indexes; getWriteLock(); try { toDelete = doWithFileLock(new LockWork<LinkedHashMap<String, IndexEntry>>() { public LinkedHashMap<String, IndexEntry> doWork() throws Exception { LinkedHashMap<String, IndexEntry> set = new LinkedHashMap<String, IndexEntry>(); for (IndexEntry entry : indexEntries.values()) { if ((entry.getType() == IndexType.INDEX) && (entry.getStatus() == TransactionStatus.MERGE)) { return set; } if ((entry.getType() == IndexType.INDEX) && (entry.getStatus() == TransactionStatus.MERGE_TARGET)) { return set; } if ((entry.getType() == IndexType.DELTA) && (entry.getStatus() == TransactionStatus.COMMITTED_DELETING)) { return set; } } // Check it is not deleting BREAK: for (IndexEntry entry : indexEntries.values()) { // skip indexes at the start if (entry.getType() == IndexType.DELTA) { if (entry.getStatus() == TransactionStatus.COMMITTED) { entry.setStatus(TransactionStatus.COMMITTED_DELETING); set.put(entry.getName(), entry); } else { // If not committed we stop as we can not // span non committed. break BREAK; } } } if (set.size() > 0) { writeStatus(); } return set; } public boolean canRetry() { return false; } }); } finally { getReadLock(); releaseWriteLock(); } try { indexes = new LinkedHashMap<String, IndexEntry>(); BREAK: for (IndexEntry entry : indexEntries.values()) { if (entry.getStatus() == TransactionStatus.COMMITTED_DELETING) { break BREAK; } indexes.put(entry.getName(), entry); } } finally { releaseReadLock(); } if (toDelete.size() == 0) { return; } // Build readers int size = 2 * (toDelete.size() + indexes.size()); final HashSet<String> invalidIndexes = new HashSet<String>(size); final HashMap<String, Long> newIndexCounts = new HashMap<String, Long>(size); LinkedHashMap<String, IndexReader> readers = new LinkedHashMap<String, IndexReader>(size); for (IndexEntry currentDelete : toDelete.values()) { Set<String> deletions = getDeletions(currentDelete.getName(), INDEX_INFO_DELETIONS); Set<String> containerDeletions = getDeletions(currentDelete.getName(), INDEX_INFO_CONTAINER_DELETIONS); if (!deletions.isEmpty()) { for (String key : indexes.keySet()) { IndexReader reader = getReferenceCountingIndexReader(key); Searcher searcher = new IndexSearcher(reader); try { for (String stringRef : deletions) { TermQuery query = new TermQuery(new Term("ID", stringRef)); Hits hits = searcher.search(query); if (hits.length() > 0) { IndexReader writeableReader = readers.get(key); if (writeableReader == null) { File location = new File(indexDirectory, key).getCanonicalFile(); if (IndexReader.indexExists(location)) { writeableReader = IndexReader.open(location); } else { continue; } readers.put(key, writeableReader); } if (currentDelete.isDeletOnlyNodes() && !containerDeletions.contains(stringRef)) { Searcher writeableSearcher = new IndexSearcher(writeableReader); hits = writeableSearcher.search(query); if (hits.length() > 0) { for (int i = 0; i < hits.length(); i++) { Document doc = hits.doc(i); // Exclude all containers except the root (which is also a node!) Field path = doc.getField("PATH"); if (path == null || path.stringValue().length() == 0) { writeableReader.deleteDocument(hits.id(i)); invalidIndexes.add(key); // There should only be one thing to // delete // break; } } } writeableSearcher.close(); } else { int deletedCount = 0; try { deletedCount = writeableReader .deleteDocuments(new Term("ID", stringRef)); } catch (IOException ioe) { if (s_logger.isDebugEnabled()) { s_logger.debug("IO Error for " + key); throw ioe; } } if (deletedCount > 0) { if (s_logger.isDebugEnabled()) { s_logger.debug("Deleted " + deletedCount + " from " + key + " for id " + stringRef + " remaining docs " + writeableReader.numDocs()); } invalidIndexes.add(key); } } } } } finally { searcher.close(); } } } if (!containerDeletions.isEmpty()) { for (String key : indexes.keySet()) { IndexReader reader = getReferenceCountingIndexReader(key); Searcher searcher = new IndexSearcher(reader); try { for (String stringRef : containerDeletions) { TermQuery query = new TermQuery(new Term("ANCESTOR", stringRef)); Hits hits = searcher.search(query); if (hits.length() > 0) { IndexReader writeableReader = readers.get(key); if (writeableReader == null) { File location = new File(indexDirectory, key).getCanonicalFile(); if (IndexReader.indexExists(location)) { writeableReader = IndexReader.open(location); } else { continue; } readers.put(key, writeableReader); } int deletedCount = 0; try { deletedCount = writeableReader .deleteDocuments(new Term("ANCESTOR", stringRef)); } catch (IOException ioe) { if (s_logger.isDebugEnabled()) { s_logger.debug("IO Error for " + key); throw ioe; } } if (deletedCount > 0) { if (s_logger.isDebugEnabled()) { s_logger.debug("Deleted " + deletedCount + " from " + key + " for id " + stringRef + " remaining docs " + writeableReader.numDocs()); } invalidIndexes.add(key); } } } } finally { searcher.close(); } } } // The delta we have just processed now must be included when we process the deletions of its successor indexes.put(currentDelete.getName(), currentDelete); } // Close all readers holding the write lock - so no one tries to // read getWriteLock(); try { for (String key : readers.keySet()) { IndexReader reader = readers.get(key); // TODO:Set the new document count newIndexCounts.put(key, new Long(reader.numDocs())); reader.close(); } } finally { releaseWriteLock(); } // Prebuild all readers for affected indexes // Register them in the commit. final HashMap<String, IndexReader> newReaders = new HashMap<String, IndexReader>(); for (String id : invalidIndexes) { IndexReader reader = buildReferenceCountingIndexReader(id, newIndexCounts.get(id)); newReaders.put(id, reader); } getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { for (IndexEntry entry : toDelete.values()) { entry.setStatus(TransactionStatus.COMMITTED); entry.setType(IndexType.INDEX); entry.setDeletions(0); } for (String key : newIndexCounts.keySet()) { Long newCount = newIndexCounts.get(key); IndexEntry entry = indexEntries.get(key); entry.setDocumentCount(newCount); } writeStatus(); for (String id : invalidIndexes) { IndexReader reader = referenceCountingReadOnlyIndexReaders.remove(id); if (reader != null) { ReferenceCounting referenceCounting = (ReferenceCounting) reader; referenceCounting.setInvalidForReuse(); if (s_logger.isDebugEnabled()) { s_logger.debug("... invalidating sub reader after applying deletions" + id); } } } for (String id : invalidIndexes) { IndexReader newReader = newReaders.get(id); registerReferenceCountingIndexReader(id, newReader); } // Invalidate all main index readers from the first invalid index onwards invalidateMainReadersFromFirst(invalidIndexes); if (s_logger.isDebugEnabled()) { for (String id : toDelete.keySet()) { s_logger.debug("...applied deletion for " + id); } s_logger.debug("...deleting done"); } dumpInfo(); notifyListeners("MergedDeletions", toDelete.size()); return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } } void mergeIndexes() throws IOException { if (s_logger.isDebugEnabled()) { s_logger.debug("Merging..."); } final LinkedHashMap<String, IndexEntry> toMerge; getWriteLock(); try { toMerge = doWithFileLock(new LockWork<LinkedHashMap<String, IndexEntry>>() { public LinkedHashMap<String, IndexEntry> doWork() throws Exception { LinkedHashMap<String, IndexEntry> set = new LinkedHashMap<String, IndexEntry>(); for (IndexEntry entry : indexEntries.values()) { if ((entry.getType() == IndexType.INDEX) && (entry.getStatus() == TransactionStatus.MERGE)) { return set; } if ((entry.getType() == IndexType.INDEX) && (entry.getStatus() == TransactionStatus.MERGE_TARGET)) { return set; } if ((entry.getType() == IndexType.DELTA) && (entry.getStatus() == TransactionStatus.COMMITTED_DELETING)) { return set; } } ArrayList<IndexEntry> mergeList = new ArrayList<IndexEntry>(); for (IndexEntry entry : indexEntries.values()) { if ((entry.getType() == IndexType.INDEX) && (entry.getStatus() == TransactionStatus.COMMITTED)) { mergeList.add(entry); } } int position = findMergeIndex(1, mergerMaxMergeDocs, mergerTargetIndexes, mergeList); String firstMergeId = mergeList.get(position).getName(); long count = 0; String guid = null; if (position >= 0) { guid = GUID.generate(); for (int i = position; i < mergeList.size(); i++) { IndexEntry entry = mergeList.get(i); count += entry.getDocumentCount(); set.put(entry.getName(), entry); entry.setStatus(TransactionStatus.MERGE); entry.setMergeId(guid); } } if (set.size() > 0) { IndexEntry target = new IndexEntry(IndexType.INDEX, guid, "", TransactionStatus.MERGE_TARGET, guid, count, 0, false); set.put(guid, target); // rebuild merged index elements LinkedHashMap<String, IndexEntry> reordered = new LinkedHashMap<String, IndexEntry>(); invalidateMainReadersFromFirst(Collections.singleton(firstMergeId)); for (IndexEntry current : indexEntries.values()) { if (current.getName().equals(firstMergeId)) { reordered.put(target.getName(), target); } reordered.put(current.getName(), current); } indexEntries = reordered; writeStatus(); } return set; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } if (s_logger.isDebugEnabled()) { s_logger.debug("....Merging..." + (toMerge.size() - 1)); } if (toMerge.size() == 0) { return; } String mergeTargetId = null; long docCount = 0; if (toMerge.size() > 0) { int count = 0; IndexReader[] readers = new IndexReader[toMerge.size() - 1]; RAMDirectory ramDirectory = null; IndexWriter writer = null; File outputLocation = null; double mergeSize = 0; for (IndexEntry entry : toMerge.values()) { File location = new File(indexDirectory, entry.getName()).getCanonicalFile(); if (entry.getStatus() == TransactionStatus.MERGE) { IndexReader reader; if (IndexReader.indexExists(location)) { reader = IndexReader.open(location); } else { s_logger.error("Index is missing " + entry.getName()); reader = IndexReader.open(emptyIndex); } readers[count++] = reader; docCount += entry.getDocumentCount(); mergeSize += getSizeInMb(location); } else if (entry.getStatus() == TransactionStatus.MERGE_TARGET) { mergeTargetId = entry.getName(); outputLocation = location; if ((docCount < maxDocsForInMemoryMerge) && (mergeSize < maxRamInMbForInMemoryMerge)) { ramDirectory = new RAMDirectory(); writer = new IndexWriter(ramDirectory, new AlfrescoStandardAnalyser(), true, MaxFieldLength.UNLIMITED); } else { writer = new IndexWriter(location, new AlfrescoStandardAnalyser(), true, MaxFieldLength.UNLIMITED); } writer.setUseCompoundFile(mergerUseCompoundFile); writer.setMaxBufferedDocs(mergerMaxBufferedDocs); writer.setRAMBufferSizeMB(mergerRamBufferSizeMb); writer.setMergeFactor(mergerMergeFactor); writer.setMaxMergeDocs(mergerMaxMergeDocs); writer.setWriteLockTimeout(writeLockTimeout); writer.setMergeScheduler(new SerialMergeScheduler()); writer.setMergePolicy(new LogDocMergePolicy()); } } writer.addIndexes(readers); writer.close(); if (ramDirectory != null) { String[] files = ramDirectory.list(); Directory directory = FSDirectory.getDirectory(outputLocation, true); for (int i = 0; i < files.length; i++) { // make place on ram disk IndexOutput os = directory.createOutput(files[i]); // read current file IndexInput is = ramDirectory.openInput(files[i]); // and copy to ram disk int len = (int) is.length(); byte[] buf = new byte[len]; is.readBytes(buf, 0, len); os.writeBytes(buf, len); // graceful cleanup is.close(); os.close(); } ramDirectory.close(); directory.close(); } for (IndexReader reader : readers) { reader.close(); } } final String finalMergeTargetId = mergeTargetId; IndexReader newReader = null; getReadLock(); try { newReader = buildReferenceCountingIndexReader(mergeTargetId, docCount); } finally { releaseReadLock(); } final IndexReader finalNewReader = newReader; getWriteLock(); try { doWithFileLock(new LockWork<Object>() { public Object doWork() throws Exception { HashSet<String> toDelete = new HashSet<String>(); for (IndexEntry entry : toMerge.values()) { if (entry.getStatus() == TransactionStatus.MERGE) { if (s_logger.isDebugEnabled()) { s_logger.debug("... deleting as merged " + entry.getName()); } toDelete.add(entry.getName()); } else if (entry.getStatus() == TransactionStatus.MERGE_TARGET) { if (s_logger.isDebugEnabled()) { s_logger.debug("... committing merge target " + entry.getName()); } entry.setStatus(TransactionStatus.COMMITTED); } } invalidateMainReadersFromFirst(toDelete); for (String id : toDelete) { indexEntries.remove(id); } registerReferenceCountingIndexReader(finalMergeTargetId, finalNewReader); notifyListeners("MergedIndexes", toMerge.size()); dumpInfo(); writeStatus(); clearOldReaders(); cleaner.schedule(); return null; } public boolean canRetry() { return false; } }); } finally { releaseWriteLock(); } if (s_logger.isDebugEnabled()) { s_logger.debug("..done merging"); } } private final int findMergeIndex(long min, long max, int target, List<IndexEntry> entries) throws IOException { // TODO: Support max if (entries.size() <= target) { return -1; } int total = 0; for (int i = target; i < entries.size(); i++) { total += entries.get(i).getDocumentCount(); } for (int i = target - 1; i > 0; i--) { total += entries.get(i).getDocumentCount(); if (total < entries.get(i - 1).getDocumentCount()) { return i; } } return 0; } } private void dumpInfo() { if (s_logger.isDebugEnabled()) { int count = 0; StringBuilder builder = new StringBuilder(); readWriteLock.writeLock().lock(); try { builder.append("\n"); builder.append(this.getRelativePath()); builder.append("Entry List\n"); for (IndexEntry entry : indexEntries.values()) { builder.append(++count + " " + entry.toString()).append("\n"); } s_logger.debug(builder.toString()); } finally { readWriteLock.writeLock().unlock(); } } } private void getWriteLock() { readOnlyLock.readLock().lock(); try { String threadName = null; long start = 0l; if (s_logger.isDebugEnabled()) { threadName = Thread.currentThread().getName(); s_logger.debug("Waiting for WRITE lock - " + threadName); start = System.nanoTime(); } readWriteLock.writeLock().lock(); if (s_logger.isDebugEnabled()) { long end = System.nanoTime(); s_logger.debug("...GOT WRITE LOCK - " + threadName + " - in " + ((end - start) / 10e6f) + " ms"); } } finally { readOnlyLock.readLock().unlock(); } } private void releaseWriteLock() { if (s_logger.isDebugEnabled()) { s_logger.debug("RELEASED WRITE LOCK - " + Thread.currentThread().getName()); } readWriteLock.writeLock().unlock(); } private void getReadLock() { String threadName = null; long start = 0l; if (s_logger.isDebugEnabled()) { threadName = Thread.currentThread().getName(); s_logger.debug("Waiting for READ lock - " + threadName); start = System.nanoTime(); } readWriteLock.readLock().lock(); if (s_logger.isDebugEnabled()) { long end = System.nanoTime(); s_logger.debug("...GOT READ LOCK - " + threadName + " - in " + ((end - start) / 10e6f) + " ms"); } } private void releaseReadLock() { if (s_logger.isDebugEnabled()) { s_logger.debug("RELEASED READ LOCK - " + Thread.currentThread().getName()); } readWriteLock.readLock().unlock(); } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(indexDirectory.toString()); builder.append(" "); builder.append(super.toString()); return builder.toString(); } private boolean isGUID(String guid) { try { UUID id = new UUID(guid); // We have a valid guid. return true; } catch (NumberFormatException e) { // Not a valid GUID } return false; } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getRelativePath() */ public String getRelativePath() { return this.relativePath; } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getStatusSnapshot() */ public Map<String, Integer> getStatusSnapshot() { Map<String, Integer> snapShot = new TreeMap<String, Integer>(); readWriteLock.writeLock().lock(); try { for (IndexEntry entry : indexEntries.values()) { String stateKey = entry.getType() + "-" + entry.getStatus(); Integer count = snapShot.get(stateKey); snapShot.put(stateKey, count == null ? 1 : count + 1); } return snapShot; } finally { readWriteLock.writeLock().unlock(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getActualSize() */ public long getActualSize() throws IOException { getReadLock(); try { int size = 0; for (IndexEntry entry : this.indexEntries.values()) { File location = new File(this.indexDirectory, entry.getName()).getCanonicalFile(); File[] contents = location.listFiles(); for (File file : contents) { if (file.isFile()) { size += file.length(); } } } return size; } finally { releaseReadLock(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getUsedSize() */ public long getUsedSize() throws IOException { getReadLock(); try { return sizeRecurse(this.indexDirectory); } finally { releaseReadLock(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getNumberOfDocuments() */ public int getNumberOfDocuments() throws IOException { IndexReader reader = getMainIndexReferenceCountingReadOnlyIndexReader(); try { return reader.numDocs(); } finally { reader.close(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getNumberOfFields() */ public int getNumberOfFields() throws IOException { IndexReader reader = getMainIndexReferenceCountingReadOnlyIndexReader(); try { return reader.getFieldNames(IndexReader.FieldOption.ALL).size(); } finally { reader.close(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#getNumberOfIndexedFields() */ public int getNumberOfIndexedFields() throws IOException { IndexReader reader = getMainIndexReferenceCountingReadOnlyIndexReader(); try { return reader.getFieldNames(IndexReader.FieldOption.INDEXED).size(); } finally { reader.close(); } } /* * (non-Javadoc) * * @see org.alfresco.repo.search.impl.lucene.index.IndexMonitor#addApplicationListener(org.springframework.context. * ApplicationListener) */ public void addApplicationListener(ApplicationListener listener) { this.applicationListeners.add(listener); } private long sizeRecurse(File fileOrDir) { long size = 0; if (fileOrDir.isDirectory()) { File[] files = fileOrDir.listFiles(); for (File file : files) { size += sizeRecurse(file); } } else { size = fileOrDir.length(); } return size; } private void publishDiscoveryEvent() { if (this.config == null) { return; } final IndexEvent discoveryEvent = new IndexEvent(this, "Discovery", 1); final ConfigurableApplicationContext applicationContext = this.config.getApplicationContext(); try { applicationContext.publishEvent(discoveryEvent); } catch (IllegalStateException e) { // There's a possibility that the application context hasn't fully refreshed yet, so register a listener // that will fire when it has applicationContext.addApplicationListener(new ApplicationListener() { public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextRefreshedEvent) { applicationContext.publishEvent(discoveryEvent); } } }); } } private void notifyListeners(String description, int count) { if (!this.applicationListeners.isEmpty()) { IndexEvent event = new IndexEvent(this, description, count); for (ApplicationListener listener : this.applicationListeners) { listener.onApplicationEvent(event); } } } interface Schedulable { void schedule(); } }