LUCENE-3488: Factor out SearcherManager out of NRTManager

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1179956 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Simon Willnauer 2011-10-07 08:05:11 +00:00
parent 12a93306a8
commit 70ee6dbdb6
6 changed files with 478 additions and 398 deletions

View File

@ -177,6 +177,11 @@ API Changes
FieldableQueryNode and thew new ValueQueryNode FieldableQueryNode and thew new ValueQueryNode
(Vinicius Barros via Uwe Schindler) (Vinicius Barros via Uwe Schindler)
* LUCENE-3488: Factored out SearcherManager from NRTManager. NRTManager
now manages SearcherManager instances instead of IndexSearcher directly.
Acquiring a SearcherManager is non-blocking unless the caller explicitly
requires to acquire a certain SearcherManager generation. (Simon Willnauer)
Optimizations Optimizations
* LUCENE-3306: Disabled indexing of positions for spellchecker n-gram * LUCENE-3306: Disabled indexing of positions for spellchecker n-gram

View File

@ -22,23 +22,25 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexReader; // javadocs import org.apache.lucene.index.IndexReader; // javadocs
import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.IndexSearcher; // javadocs
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.SearcherWarmer; import org.apache.lucene.search.SearcherWarmer;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.ThreadInterruptedException; import org.apache.lucene.util.ThreadInterruptedException;
// TODO
// - we could make this work also w/ "normal" reopen/commit?
/** /**
* Utility class to manage sharing near-real-time searchers * Utility class to manage sharing near-real-time searchers
* across multiple searching threads. * across multiple searching threads.
* *
* <p>NOTE: to use this class, you must call reopen * <p>NOTE: to use this class, you must call {@link #maybeReopen(boolean)}
* periodically. The {@link NRTManagerReopenThread} is a * periodically. The {@link NRTManagerReopenThread} is a
* simple class to do this on a periodic basis. If you * simple class to do this on a periodic basis. If you
* implement your own reopener, be sure to call {@link * implement your own reopener, be sure to call {@link
@ -50,15 +52,12 @@ import org.apache.lucene.util.ThreadInterruptedException;
public class NRTManager implements Closeable { public class NRTManager implements Closeable {
private final IndexWriter writer; private final IndexWriter writer;
private final ExecutorService es; private final SearcherManagerRef withoutDeletes;
private final SearcherManagerRef withDeletes;
private final AtomicLong indexingGen; private final AtomicLong indexingGen;
private final AtomicLong searchingGen;
private final AtomicLong noDeletesSearchingGen;
private final SearcherWarmer warmer;
private final List<WaitingListener> waitingListeners = new CopyOnWriteArrayList<WaitingListener>(); private final List<WaitingListener> waitingListeners = new CopyOnWriteArrayList<WaitingListener>();
private final ReentrantLock reopenLock = new ReentrantLock();
private volatile IndexSearcher currentSearcher; private final Condition newGeneration = reopenLock.newCondition();
private volatile IndexSearcher noDeletesCurrentSearcher;
/** /**
* Create new NRTManager. * Create new NRTManager.
@ -66,9 +65,8 @@ public class NRTManager implements Closeable {
* @param writer IndexWriter to open near-real-time * @param writer IndexWriter to open near-real-time
* readers * readers
* @param es optional ExecutorService so different segments can * @param es optional ExecutorService so different segments can
* be searched concurrently (see {@link * be searched concurrently (see {@link IndexSearcher#IndexSearcher(IndexReader, ExecutorService)}.
* IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null * Pass <code>null</code> to search segments sequentially.
* to search segments sequentially.
* @param warmer optional {@link SearcherWarmer}. Pass * @param warmer optional {@link SearcherWarmer}. Pass
* null if you don't require the searcher to warmed * null if you don't require the searcher to warmed
* before going live. If this is non-null then a * before going live. If this is non-null then a
@ -79,27 +77,28 @@ public class NRTManager implements Closeable {
* not invoked for the initial searcher; you should * not invoked for the initial searcher; you should
* warm it yourself if necessary. * warm it yourself if necessary.
*/ */
public NRTManager(IndexWriter writer, ExecutorService es, SearcherWarmer warmer) throws IOException { public NRTManager(IndexWriter writer, ExecutorService es,
SearcherWarmer warmer) throws IOException {
this(writer, es, warmer, true);
}
/**
* Expert: just like {@link
* #NRTManager(IndexWriter,ExecutorService,SearcherWarmer)},
* but you can also specify whether every searcher must
* apply deletes. This is useful for cases where certain
* uses can tolerate seeing some deleted docs, since
* reopen time is faster if deletes need not be applied. */
public NRTManager(IndexWriter writer, ExecutorService es,
SearcherWarmer warmer, boolean alwaysApplyDeletes) throws IOException {
this.writer = writer; this.writer = writer;
this.es = es; if (alwaysApplyDeletes) {
this.warmer = warmer; withoutDeletes = withDeletes = new SearcherManagerRef(true, 0, SearcherManager.open(writer, true, warmer, es));
} else {
withDeletes = new SearcherManagerRef(true, 0, SearcherManager.open(writer, true, warmer, es));
withoutDeletes = new SearcherManagerRef(false, 0, SearcherManager.open(writer, false, warmer, es));
}
indexingGen = new AtomicLong(1); indexingGen = new AtomicLong(1);
searchingGen = new AtomicLong(-1);
noDeletesSearchingGen = new AtomicLong(-1);
// Create initial reader:
swapSearcher(new IndexSearcher(IndexReader.open(writer, true), es), 0, true);
if (this.warmer != null) {
writer.getConfig().setMergedSegmentWarmer(
new IndexWriter.IndexReaderWarmer() {
@Override
public void warm(IndexReader reader) throws IOException {
NRTManager.this.warmer.warm(new IndexSearcher(reader, NRTManager.this.es));
}
});
}
} }
/** NRTManager invokes this interface to notify it when a /** NRTManager invokes this interface to notify it when a
@ -182,201 +181,162 @@ public class NRTManager implements Closeable {
return indexingGen.get(); return indexingGen.get();
} }
/** Returns the most current searcher. If you require a /**
* certain indexing generation be visible in the returned * Waits for a given {@link SearcherManager} target generation to be available
* searcher, call {@link #get(long)} * via {@link #getSearcherManager(boolean)}. If the current generation is less
* instead. * than the given target generation this method will block until the
* correspondent {@link SearcherManager} is reopened by another thread via
* {@link #maybeReopen(boolean)} or until the {@link NRTManager} is closed.
*
* @param targetGen the generation to wait for
* @param requireDeletes <code>true</code> iff the generation requires deletes to be applied otherwise <code>false</code>
* @return the {@link SearcherManager} with the given target generation
*/ */
public synchronized IndexSearcher get() { public SearcherManager waitForGeneration(long targetGen, boolean requireDeletes) {
return get(true); return waitForGeneration(targetGen, requireDeletes, -1, TimeUnit.NANOSECONDS);
} }
/** Just like {@link #get}, but by passing <code>false</code> for /**
* requireDeletes, you can get faster reopen time, but * Waits for a given {@link SearcherManager} target generation to be available
* the returned reader is allowed to not reflect all * via {@link #getSearcherManager(boolean)}. If the current generation is less
* deletions. See {@link IndexReader#open(IndexWriter,boolean)} */ * than the given target generation this method will block until the
public synchronized IndexSearcher get(boolean requireDeletes) { * correspondent {@link SearcherManager} is reopened by another thread via
final IndexSearcher s; * {@link #maybeReopen(boolean)}, the given waiting time has elapsed, or until
if (requireDeletes) { * the {@link NRTManager} is closed.
s = currentSearcher; * <p>
} else if (noDeletesSearchingGen.get() > searchingGen.get()) { * NOTE: if the waiting time elapses before the requested target generation is
s = noDeletesCurrentSearcher; * available the latest {@link SearcherManager} is returned instead.
} else {
s = currentSearcher;
}
s.getIndexReader().incRef();
return s;
}
/** Call this if you require a searcher reflecting all
* changes as of the target generation.
* *
* @param targetGen Returned searcher must reflect changes * @param targetGen
* as of this generation * the generation to wait for
* @param requireDeletes
* <code>true</code> iff the generation requires deletes to be
* applied otherwise <code>false</code>
* @param time
* the time to wait for the target generation
* @param unit
* the waiting time's time unit
* @return the {@link SearcherManager} with the given target generation or the
* latest {@link SearcherManager} if the waiting time elapsed before
* the requested generation is available.
*/ */
public synchronized IndexSearcher get(long targetGen) { public SearcherManager waitForGeneration(long targetGen, boolean requireDeletes, long time, TimeUnit unit) {
return get(targetGen, true); try {
} reopenLock.lockInterruptibly();
try {
/** Call this if you require a searcher reflecting all
* changes as of the target generation, and you don't
* require deletions to be reflected. Note that the
* returned searcher may still reflect some or all
* deletions.
*
* @param targetGen Returned searcher must reflect changes
* as of this generation
*
* @param requireDeletes If true, the returned searcher must
* reflect all deletions. This can be substantially more
* costly than not applying deletes. Note that if you
* pass false, it's still possible that some or all
* deletes may have been applied.
**/
public synchronized IndexSearcher get(long targetGen, boolean requireDeletes) {
assert noDeletesSearchingGen.get() >= searchingGen.get(): "noDeletesSearchingGen=" + noDeletesSearchingGen.get() + " searchingGen=" + searchingGen.get();
if (targetGen > getCurrentSearchingGen(requireDeletes)) { if (targetGen > getCurrentSearchingGen(requireDeletes)) {
// Must wait
//final long t0 = System.nanoTime();
for (WaitingListener listener : waitingListeners) { for (WaitingListener listener : waitingListeners) {
listener.waiting(requireDeletes, targetGen); listener.waiting(requireDeletes, targetGen);
} }
while (targetGen > getCurrentSearchingGen(requireDeletes)) { while (targetGen > getCurrentSearchingGen(requireDeletes)) {
//System.out.println(Thread.currentThread().getName() + ": wait fresh searcher targetGen=" + targetGen + " vs searchingGen=" + getCurrentSearchingGen(requireDeletes) + " requireDeletes=" + requireDeletes); if (!waitOnGenCondition(time, unit)) {
try { return getSearcherManager(requireDeletes);
wait(); }
}
}
} finally {
reopenLock.unlock();
}
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
throw new ThreadInterruptedException(ie); throw new ThreadInterruptedException(ie);
} }
} return getSearcherManager(requireDeletes);
//final long waitNS = System.nanoTime()-t0;
//System.out.println(Thread.currentThread().getName() + ": done wait fresh searcher targetGen=" + targetGen + " vs searchingGen=" + getCurrentSearchingGen(requireDeletes) + " requireDeletes=" + requireDeletes + " WAIT msec=" + (waitNS/1000000.0));
} }
return get(requireDeletes); private boolean waitOnGenCondition(long time, TimeUnit unit)
throws InterruptedException {
assert reopenLock.isHeldByCurrentThread();
if (time < 0) {
newGeneration.await();
return true;
} else {
return newGeneration.await(time, unit);
}
} }
/** Returns generation of current searcher. */ /** Returns generation of current searcher. */
public long getCurrentSearchingGen(boolean requiresDeletes) { public long getCurrentSearchingGen(boolean applyAllDeletes) {
return requiresDeletes ? searchingGen.get() : noDeletesSearchingGen.get(); if (applyAllDeletes) {
return withDeletes.generation;
} else {
return Math.max(withoutDeletes.generation, withDeletes.generation);
}
} }
/** Release the searcher obtained from {@link public boolean maybeReopen(boolean applyAllDeletes) throws IOException {
* #get()} or {@link #get(long)}. if (reopenLock.tryLock()) {
* try {
* <p><b>NOTE</b>: it's safe to call this after {@link final SearcherManagerRef reference = applyAllDeletes ? withDeletes : withoutDeletes;
* #close}. */
public void release(IndexSearcher s) throws IOException {
s.getIndexReader().decRef();
}
/** Call this when you need the NRT reader to reopen.
*
* @param applyDeletes If true, the newly opened reader
* will reflect all deletes
*/
public boolean reopen(boolean applyDeletes) throws IOException {
// Mark gen as of when reopen started: // Mark gen as of when reopen started:
final long newSearcherGen = indexingGen.getAndIncrement(); final long newSearcherGen = indexingGen.getAndIncrement();
boolean setSearchGen = false;
if (applyDeletes && currentSearcher.getIndexReader().isCurrent()) { if (!(setSearchGen = reference.manager.isSearcherCurrent())) {
//System.out.println("reopen: skip: isCurrent both force gen=" + newSearcherGen + " vs current gen=" + searchingGen); setSearchGen = reference.manager.maybeReopen();
searchingGen.set(newSearcherGen);
noDeletesSearchingGen.set(newSearcherGen);
synchronized(this) {
notifyAll();
} }
//System.out.println("reopen: skip: return"); if (setSearchGen) {
return false; reference.generation = newSearcherGen;// update searcher gen
} else if (!applyDeletes && noDeletesCurrentSearcher.getIndexReader().isCurrent()) { newGeneration.signalAll(); // wake up threads if we have a new generation
//System.out.println("reopen: skip: isCurrent force gen=" + newSearcherGen + " vs current gen=" + noDeletesSearchingGen);
noDeletesSearchingGen.set(newSearcherGen);
synchronized(this) {
notifyAll();
} }
//System.out.println("reopen: skip: return"); return setSearchGen;
return false;
}
//System.out.println("indexingGen now " + indexingGen);
// .reopen() returns a new reference:
// Start from whichever searcher is most current:
final IndexSearcher startSearcher = noDeletesSearchingGen.get() > searchingGen.get() ? noDeletesCurrentSearcher : currentSearcher;
IndexReader nextReader = IndexReader.openIfChanged(startSearcher.getIndexReader(), writer, applyDeletes);
if (nextReader == null) {
// NOTE: doesn't happen currently in Lucene (reopen on
// NRT reader always returns new reader), but could in
// the future:
nextReader = startSearcher.getIndexReader();
nextReader.incRef();
}
if (nextReader != startSearcher.getIndexReader()) {
final IndexSearcher nextSearcher = new IndexSearcher(nextReader, es);
if (warmer != null) {
boolean success = false;
try {
warmer.warm(nextSearcher);
success = true;
} finally { } finally {
if (!success) { reopenLock.unlock();
nextReader.decRef();
} }
} }
}
// Transfer reference to swapSearcher:
swapSearcher(nextSearcher,
newSearcherGen,
applyDeletes);
return true;
} else {
return false; return false;
} }
}
// Steals a reference from newSearcher: /**
private synchronized void swapSearcher(IndexSearcher newSearcher, long newSearchingGen, boolean applyDeletes) throws IOException { * Close this NRTManager to future searching. Any searches still in process in
//System.out.println(Thread.currentThread().getName() + ": swap searcher gen=" + newSearchingGen + " applyDeletes=" + applyDeletes); * other threads won't be affected, and they should still call
* {@link SearcherManager#release(IndexSearcher)} after they are done.
// Always replace noDeletesCurrentSearcher:
if (noDeletesCurrentSearcher != null) {
noDeletesCurrentSearcher.getIndexReader().decRef();
}
noDeletesCurrentSearcher = newSearcher;
assert newSearchingGen > noDeletesSearchingGen.get(): "newSearchingGen=" + newSearchingGen + " noDeletesSearchingGen=" + noDeletesSearchingGen;
noDeletesSearchingGen.set(newSearchingGen);
if (applyDeletes) {
// Deletes were applied, so we also update currentSearcher:
if (currentSearcher != null) {
currentSearcher.getIndexReader().decRef();
}
currentSearcher = newSearcher;
if (newSearcher != null) {
newSearcher.getIndexReader().incRef();
}
assert newSearchingGen > searchingGen.get(): "newSearchingGen=" + newSearchingGen + " searchingGen=" + searchingGen;
searchingGen.set(newSearchingGen);
}
notifyAll();
//System.out.println(Thread.currentThread().getName() + ": done");
}
/** Close this NRTManager to future searching. Any
* searches still in process in other threads won't be
* affected, and they should still call {@link #release}
* after they are done.
* *
* <p><b>NOTE</b>: caller must separately close the writer. */ * <p>
@Override * <b>NOTE</b>: caller must separately close the writer.
*/
public synchronized void close() throws IOException {
reopenLock.lock();
try {
IOUtils.close(withDeletes, withoutDeletes);
newGeneration.signalAll();
} finally {
reopenLock.unlock();
}
}
/**
* Returns a {@link SearcherManager}. If <code>applyAllDeletes</code> is
* <code>true</code> the returned manager is guaranteed to have all deletes
* applied on the last reopen. Otherwise the latest manager with or without deletes
* is returned.
*/
public SearcherManager getSearcherManager(boolean applyAllDeletes) {
if (applyAllDeletes) {
return withDeletes.manager;
} else {
if (withDeletes.generation > withoutDeletes.generation) {
return withDeletes.manager;
} else {
return withoutDeletes.manager;
}
}
}
static final class SearcherManagerRef implements Closeable {
final boolean applyDeletes;
volatile long generation;
final SearcherManager manager;
SearcherManagerRef(boolean applyDeletes, long generation, SearcherManager manager) {
super();
this.applyDeletes = applyDeletes;
this.generation = generation;
this.manager = manager;
}
public void close() throws IOException { public void close() throws IOException {
swapSearcher(null, indexingGen.getAndIncrement(), true); generation = Long.MAX_VALUE; // max it out to make sure nobody can wait on another gen
manager.close();
}
} }
} }

View File

@ -80,12 +80,13 @@ import org.apache.lucene.util.ThreadInterruptedException;
*/ */
public class NRTManagerReopenThread extends Thread implements NRTManager.WaitingListener, Closeable { public class NRTManagerReopenThread extends Thread implements NRTManager.WaitingListener, Closeable {
private final NRTManager manager; private final NRTManager manager;
private final long targetMaxStaleNS; private final long targetMaxStaleNS;
private final long targetMinStaleNS; private final long targetMinStaleNS;
private boolean finish; private boolean finish;
private boolean waitingNeedsDeletes;
private long waitingGen; private long waitingGen;
private boolean waitingNeedsDeletes;
/** /**
* Create NRTManagerReopenThread, to periodically reopen the NRT searcher. * Create NRTManagerReopenThread, to periodically reopen the NRT searcher.
@ -140,8 +141,6 @@ public class NRTManagerReopenThread extends Thread implements NRTManager.Waiting
try { try {
while (true) { while (true) {
final boolean doApplyDeletes;
boolean hasWaiting = false; boolean hasWaiting = false;
synchronized(this) { synchronized(this) {
@ -176,16 +175,13 @@ public class NRTManagerReopenThread extends Thread implements NRTManager.Waiting
//System.out.println("reopen: finish"); //System.out.println("reopen: finish");
return; return;
} }
doApplyDeletes = hasWaiting ? waitingNeedsDeletes : true;
waitingNeedsDeletes = false;
//System.out.println("reopen: start hasWaiting=" + hasWaiting); //System.out.println("reopen: start hasWaiting=" + hasWaiting);
} }
lastReopenStartNS = System.nanoTime(); lastReopenStartNS = System.nanoTime();
try { try {
//final long t0 = System.nanoTime(); //final long t0 = System.nanoTime();
manager.reopen(doApplyDeletes); manager.maybeReopen(waitingNeedsDeletes);
//System.out.println("reopen took " + ((System.nanoTime()-t0)/1000000.0) + " msec"); //System.out.println("reopen took " + ((System.nanoTime()-t0)/1000000.0) + " msec");
} catch (IOException ioe) { } catch (IOException ioe) {
//System.out.println(Thread.currentThread().getName() + ": IOE"); //System.out.println(Thread.currentThread().getName() + ": IOE");

View File

@ -17,11 +17,11 @@ package org.apache.lucene.search;
* limitations under the License. * limitations under the License.
*/ */
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.NRTManager; // javadocs import org.apache.lucene.index.NRTManager; // javadocs
@ -29,13 +29,14 @@ import org.apache.lucene.search.IndexSearcher; // javadocs
import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.Directory;
/** Utility class to safely share {@link IndexSearcher} instances /**
* across multiple threads, while periodically reopening. * Utility class to safely share {@link IndexSearcher} instances across multiple
* This class ensures each IndexSearcher instance is not * threads, while periodically reopening. This class ensures each searcher is
* closed until it is no longer needed. * closed only once all threads have finished using it.
* *
* <p>Use {@link #acquire} to obtain the current searcher, and * <p>
* {@link #release} to release it, like this: * Use {@link #acquire} to obtain the current searcher, and {@link #release} to
* release it, like this:
* *
* <pre> * <pre>
* IndexSearcher s = manager.acquire(); * IndexSearcher s = manager.acquire();
@ -48,100 +49,63 @@ import org.apache.lucene.store.Directory;
* s = null; * s = null;
* </pre> * </pre>
* *
* <p>In addition you should periodically call {@link * <p>
* #maybeReopen}. While it's possible to call this just * In addition you should periodically call {@link #maybeReopen}. While it's
* before running each query, this is discouraged since it * possible to call this just before running each query, this is discouraged
* penalizes the unlucky queries that do the reopen. It's * since it penalizes the unlucky queries that do the reopen. It's better to use
* better to use a separate background thread, that * a separate background thread, that periodically calls maybeReopen. Finally,
* periodically calls maybeReopen. Finally, be sure to * be sure to call {@link #close} once you are done.
* call {@link #close} once you are done.
* *
* <p><b>NOTE</b>: if you have an {@link IndexWriter}, it's * <p>
* better to use {@link NRTManager} since that class pulls * <b>NOTE</b>: if you have an {@link IndexWriter}, it's better to use
* near-real-time readers from the IndexWriter. * {@link NRTManager} since that class pulls near-real-time readers from the
* IndexWriter.
* *
* @lucene.experimental * @lucene.experimental
*/ */
public class SearcherManager implements Closeable { public abstract class SearcherManager {
// Current searcher protected volatile IndexSearcher currentSearcher;
private volatile IndexSearcher currentSearcher; protected final ExecutorService es;
private final SearcherWarmer warmer; protected final SearcherWarmer warmer;
private final Semaphore reopening = new Semaphore(1); protected final Semaphore reopenLock = new Semaphore(1);
private final ExecutorService es;
/** Opens an initial searcher from the Directory. protected SearcherManager(IndexReader openedReader, SearcherWarmer warmer,
* ExecutorService es) throws IOException {
* @param dir Directory to open the searcher from
*
* @param warmer optional {@link SearcherWarmer}. Pass
* null if you don't require the searcher to warmed
* before going live.
*
* <p><b>NOTE</b>: the provided {@link SearcherWarmer} is
* not invoked for the initial searcher; you should
* warm it yourself if necessary.
*/
public SearcherManager(Directory dir, SearcherWarmer warmer) throws IOException {
this(dir, warmer, null);
}
/** Opens an initial searcher from the Directory.
*
* @param dir Directory to open the searcher from
*
* @param warmer optional {@link SearcherWarmer}. Pass
* null if you don't require the searcher to warmed
* before going live.
*
* @param es optional ExecutorService so different segments can
* be searched concurrently (see {@link
* IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass null
* to search segments sequentially.
*
* <p><b>NOTE</b>: the provided {@link SearcherWarmer} is
* not invoked for the initial searcher; you should
* warm it yourself if necessary.
*/
public SearcherManager(Directory dir, SearcherWarmer warmer, ExecutorService es) throws IOException {
this.es = es; this.es = es;
currentSearcher = new IndexSearcher(IndexReader.open(dir), this.es);
this.warmer = warmer; this.warmer = warmer;
currentSearcher = new IndexSearcher(openedReader, es);
} }
/** You must call this, periodically, to perform a /**
* reopen. This calls {@link IndexReader#openIfChanged} on the * You must call this, periodically, to perform a reopen. This calls
* underlying reader, and if that returns a new reader, * {@link #openIfChanged(IndexReader)} with the underlying reader, and if that returns a
* it's warmed (if you provided a {@link SearcherWarmer} * new reader, it's warmed (if you provided a {@link SearcherWarmer} and then
* and then swapped into production. * swapped into production.
* *
* <p><b>Threads</b>: it's fine for more than one thread to * <p>
* call this at once. Only the first thread will attempt * <b>Threads</b>: it's fine for more than one thread to call this at once.
* the reopen; subsequent threads will see that another * Only the first thread will attempt the reopen; subsequent threads will see
* thread is already handling reopen and will return * that another thread is already handling reopen and will return immediately.
* immediately. Note that this means if another thread * Note that this means if another thread is already reopening then subsequent
* is already reopening then subsequent threads will * threads will return right away without waiting for the reader reopen to
* return right away without waiting for the reader * complete.
* reopen to complete.</p> * </p>
* *
* <p>This method returns true if a new reader was in * <p>
* fact opened.</p> * This method returns true if a new reader was in fact opened.
* </p>
*/ */
public boolean maybeReopen() public boolean maybeReopen() throws IOException {
throws IOException { ensureOpen();
if (currentSearcher == null) {
throw new AlreadyClosedException("this SearcherManager is closed");
}
// Ensure only 1 thread does reopen at once; other // Ensure only 1 thread does reopen at once; other
// threads just return immediately: // threads just return immediately:
if (reopening.tryAcquire()) { if (reopenLock.tryAcquire()) {
try { try {
IndexReader newReader = IndexReader.openIfChanged(currentSearcher.getIndexReader()); final IndexReader newReader = openIfChanged(currentSearcher.getIndexReader());
if (newReader != null) { if (newReader != null) {
IndexSearcher newSearcher = new IndexSearcher(newReader, es); final IndexSearcher newSearcher = new IndexSearcher(newReader, es);
boolean success = false; boolean success = false;
try { try {
if (warmer != null) { if (warmer != null) {
@ -159,16 +123,57 @@ public class SearcherManager implements Closeable {
return false; return false;
} }
} finally { } finally {
reopening.release(); reopenLock.release();
} }
} else { } else {
return false; return false;
} }
} }
/** Obtain the current IndexSearcher. You must match /**
* every call to acquire with one call to {@link #release}; * Returns <code>true</code> if no changes have occured since this searcher
* it's best to do so in a finally clause. */ * ie. reader was opened, otherwise <code>false</code>.
* @see IndexReader#isCurrent()
*/
public boolean isSearcherCurrent() throws CorruptIndexException,
IOException {
final IndexSearcher searcher = acquire();
try {
return searcher.getIndexReader().isCurrent();
} finally {
release(searcher);
}
}
/**
* Release the searcher previously obtained with {@link #acquire}.
*
* <p>
* <b>NOTE</b>: it's safe to call this after {@link #close}.
*/
public void release(IndexSearcher searcher) throws IOException {
assert searcher != null;
searcher.getIndexReader().decRef();
}
/**
* Close this SearcherManager to future searching. Any searches still in
* process in other threads won't be affected, and they should still call
* {@link #release} after they are done.
*/
public synchronized void close() throws IOException {
if (currentSearcher != null) {
// make sure we can call this more than once
// closeable javadoc says:
// if this is already closed then invoking this method has no effect.
swapSearcher(null);
}
}
/**
* Obtain the current IndexSearcher. You must match every call to acquire with
* one call to {@link #release}; it's best to do so in a finally clause.
*/
public IndexSearcher acquire() { public IndexSearcher acquire() {
IndexSearcher searcher; IndexSearcher searcher;
do { do {
@ -179,38 +184,128 @@ public class SearcherManager implements Closeable {
return searcher; return searcher;
} }
/** Release the searcher previously obtained with {@link private void ensureOpen() {
* #acquire}. if (currentSearcher == null) {
*
* <p><b>NOTE</b>: it's safe to call this after {@link
* #close}. */
public void release(IndexSearcher searcher)
throws IOException {
searcher.getIndexReader().decRef();
}
// Replaces old searcher with new one - needs to be synced to make close() work
private synchronized void swapSearcher(IndexSearcher newSearcher)
throws IOException {
IndexSearcher oldSearcher = currentSearcher;
if (oldSearcher == null) {
throw new AlreadyClosedException("this SearcherManager is closed"); throw new AlreadyClosedException("this SearcherManager is closed");
} }
}
protected synchronized void swapSearcher(IndexSearcher newSearcher) throws IOException {
ensureOpen();
final IndexSearcher oldSearcher = currentSearcher;
currentSearcher = newSearcher; currentSearcher = newSearcher;
release(oldSearcher); release(oldSearcher);
} }
/** Close this SearcherManager to future searching. Any protected abstract IndexReader openIfChanged(IndexReader oldReader)
* searches still in process in other threads won't be throws IOException;
* affected, and they should still call {@link #release}
* after they are done. */ /**
* Creates and returns a new SearcherManager from the given {@link IndexWriter}.
* @param writer the IndexWriter to open the IndexReader from.
* @param applyAllDeletes If <code>true</code>, all buffered deletes will
* be applied (made visible) in the {@link IndexSearcher} / {@link IndexReader}.
* If <code>false</code>, the deletes are not applied but remain buffered
* (in IndexWriter) so that they will be applied in the future.
* Applying deletes can be costly, so if your app can tolerate deleted documents
* being returned you might gain some performance by passing <code>false</code>.
* @param warmer An optional {@link SearcherWarmer}. Pass
* <code>null</code> if you don't require the searcher to warmed
* before going live. If this is <code>non-null</code> then a
* merged segment warmer is installed on the
* provided IndexWriter's config.
* @param es An optional {@link ExecutorService} so different segments can
* be searched concurrently (see {@link
* IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass <code>null</code>
* to search segments sequentially.
*
* @see IndexReader#openIfChanged(IndexReader, IndexWriter, boolean)
* @throws IOException
*/
public static SearcherManager open(IndexWriter writer, boolean applyAllDeletes,
SearcherWarmer warmer, ExecutorService es) throws IOException {
final IndexReader open = IndexReader.open(writer, true);
boolean success = false;
try {
SearcherManager manager = new NRTSearcherManager(writer, applyAllDeletes,
open, warmer, es);
success = true;
return manager;
} finally {
if (!success) {
open.close();
}
}
}
/**
* Creates and returns a new SearcherManager from the given {@link Directory}.
* @param dir the directory to open the IndexReader on.
* @param warmer An optional {@link SearcherWarmer}. Pass
* <code>null</code> if you don't require the searcher to warmed
* before going live. If this is <code>non-null</code> then a
* merged segment warmer is installed on the
* provided IndexWriter's config.
* @param es And optional {@link ExecutorService} so different segments can
* be searched concurrently (see {@link
* IndexSearcher#IndexSearcher(IndexReader,ExecutorService)}. Pass <code>null</code>
* to search segments sequentially.
*
* @throws IOException
*/
public static SearcherManager open(Directory dir, SearcherWarmer warmer,
ExecutorService es) throws IOException {
final IndexReader open = IndexReader.open(dir, true);
boolean success = false;
try {
SearcherManager manager = new DirectorySearchManager(open, warmer, es);
success = true;
return manager;
} finally {
if (!success) {
open.close();
}
}
}
static final class NRTSearcherManager extends SearcherManager {
private final IndexWriter writer;
private final boolean applyDeletes;
NRTSearcherManager(final IndexWriter writer, final boolean applyDeletes,
final IndexReader openedReader, final SearcherWarmer warmer, final ExecutorService es)
throws IOException {
super(openedReader, warmer, es);
this.writer = writer;
this.applyDeletes = applyDeletes;
if (warmer != null) {
writer.getConfig().setMergedSegmentWarmer(
new IndexWriter.IndexReaderWarmer() {
@Override @Override
public synchronized void close() throws IOException { public void warm(IndexReader reader) throws IOException {
if (currentSearcher != null) { warmer.warm(new IndexSearcher(reader, es));
// make sure we can call this more than once }
// closeable javadoc says: });
// if this is already closed then invoking this method has no effect. }
swapSearcher(null); }
@Override
protected IndexReader openIfChanged(IndexReader oldReader)
throws IOException {
return IndexReader.openIfChanged(oldReader, writer, applyDeletes);
}
}
static final class DirectorySearchManager extends SearcherManager {
DirectorySearchManager(IndexReader openedReader,
SearcherWarmer warmer, ExecutorService es) throws IOException {
super(openedReader, warmer, es);
}
@Override
protected IndexReader openIfChanged(IndexReader oldReader)
throws IOException {
return IndexReader.openIfChanged(oldReader, true);
} }
} }
} }

View File

@ -22,6 +22,7 @@ import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.SearcherWarmer; import org.apache.lucene.search.SearcherWarmer;
import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.Directory;
@ -41,7 +42,8 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println("TEST: finalSearcher maxGen=" + maxGen); System.out.println("TEST: finalSearcher maxGen=" + maxGen);
} }
return nrt.get(maxGen, true); final SearcherManager manager = nrt.waitForGeneration(maxGen, true);
return manager.acquire();
} }
@Override @Override
@ -67,14 +69,15 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id); System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id);
} }
final IndexSearcher s = nrt.get(gen, true); SearcherManager manager = nrt.waitForGeneration(gen, true);
final IndexSearcher s = manager.acquire();
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s); System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s);
} }
try { try {
assertEquals(docs.size(), s.search(new TermQuery(id), 10).totalHits); assertEquals(docs.size(), s.search(new TermQuery(id), 10).totalHits);
} finally { } finally {
nrt.release(s); manager.release(s);
} }
} }
@ -89,14 +92,15 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id); System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id);
} }
final IndexSearcher s = nrt.get(gen, false); final SearcherManager manager = nrt.waitForGeneration(gen, false);
final IndexSearcher s = manager.acquire();// nocommit get(gen, false);
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s); System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s);
} }
try { try {
assertEquals(docs.size(), s.search(new TermQuery(id), 10).totalHits); assertEquals(docs.size(), s.search(new TermQuery(id), 10).totalHits);
} finally { } finally {
nrt.release(s); manager.release(s);
} }
} }
lastGens.set(gen); lastGens.set(gen);
@ -111,14 +115,15 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id); System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id);
} }
final IndexSearcher s = nrt.get(gen, false); final SearcherManager manager = nrt.waitForGeneration(gen, false);
final IndexSearcher s = manager.acquire();
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s); System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s);
} }
try { try {
assertEquals(1, s.search(new TermQuery(id), 10).totalHits); assertEquals(1, s.search(new TermQuery(id), 10).totalHits);
} finally { } finally {
nrt.release(s); manager.release(s);
} }
} }
lastGens.set(gen); lastGens.set(gen);
@ -132,14 +137,15 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id); System.out.println(Thread.currentThread().getName() + ": nrt: verify " + id);
} }
final IndexSearcher s = nrt.get(gen, true); final SearcherManager manager = nrt.waitForGeneration(gen, true);
final IndexSearcher s = manager.acquire();
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s); System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s);
} }
try { try {
assertEquals(1, s.search(new TermQuery(id), 10).totalHits); assertEquals(1, s.search(new TermQuery(id), 10).totalHits);
} finally { } finally {
nrt.release(s); manager.release(s);
} }
} }
lastGens.set(gen); lastGens.set(gen);
@ -153,14 +159,15 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: verify del " + id); System.out.println(Thread.currentThread().getName() + ": nrt: verify del " + id);
} }
final IndexSearcher s = nrt.get(gen, true); final SearcherManager manager = nrt.waitForGeneration(gen, true);
final IndexSearcher s = manager.acquire();
if (VERBOSE) { if (VERBOSE) {
System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s); System.out.println(Thread.currentThread().getName() + ": nrt: got searcher=" + s);
} }
try { try {
assertEquals(0, s.search(new TermQuery(id), 10).totalHits); assertEquals(0, s.search(new TermQuery(id), 10).totalHits);
} finally { } finally {
nrt.release(s); manager.release(s);
} }
} }
lastGens.set(gen); lastGens.set(gen);
@ -168,7 +175,6 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
private NRTManager nrt; private NRTManager nrt;
private NRTManagerReopenThread nrtThread; private NRTManagerReopenThread nrtThread;
@Override @Override
protected void doAfterWriter(ExecutorService es) throws Exception { protected void doAfterWriter(ExecutorService es) throws Exception {
final double minReopenSec = 0.01 + 0.05 * random.nextDouble(); final double minReopenSec = 0.01 + 0.05 * random.nextDouble();
@ -185,7 +191,8 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
TestNRTManager.this.warmCalled = true; TestNRTManager.this.warmCalled = true;
s.search(new TermQuery(new Term("body", "united")), 10); s.search(new TermQuery(new Term("body", "united")), 10);
} }
}); }, false);
nrtThread = new NRTManagerReopenThread(nrt, maxReopenSec, minReopenSec); nrtThread = new NRTManagerReopenThread(nrt, maxReopenSec, minReopenSec);
nrtThread.setName("NRT Reopen Thread"); nrtThread.setName("NRT Reopen Thread");
nrtThread.setPriority(Math.min(Thread.currentThread().getPriority()+2, Thread.MAX_PRIORITY)); nrtThread.setPriority(Math.min(Thread.currentThread().getPriority()+2, Thread.MAX_PRIORITY));
@ -214,12 +221,12 @@ public class TestNRTManager extends ThreadedIndexingAndSearchingTestCase {
@Override @Override
protected IndexSearcher getCurrentSearcher() throws Exception { protected IndexSearcher getCurrentSearcher() throws Exception {
return nrt.get(random.nextBoolean()); return nrt.getSearcherManager(false).acquire();
} }
@Override @Override
protected void releaseSearcher(IndexSearcher s) throws Exception { protected void releaseSearcher(IndexSearcher s) throws Exception {
nrt.release(s); nrt.getSearcherManager(false).release(s);
} }
@Override @Override

View File

@ -20,6 +20,8 @@ package org.apache.lucene.search;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.analysis.MockAnalyzer;
@ -29,6 +31,7 @@ import org.apache.lucene.index.Term;
import org.apache.lucene.index.ThreadedIndexingAndSearchingTestCase; import org.apache.lucene.index.ThreadedIndexingAndSearchingTestCase;
import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.Directory;
import org.apache.lucene.util.NamedThreadFactory;
import org.apache.lucene.util._TestUtil; import org.apache.lucene.util._TestUtil;
public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase { public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
@ -41,25 +44,35 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
@Override @Override
protected IndexSearcher getFinalSearcher() throws Exception { protected IndexSearcher getFinalSearcher() throws Exception {
if (!isNRT) {
writer.commit(); writer.commit();
mgr.maybeReopen(); }
assertTrue(mgr.maybeReopen() || mgr.isSearcherCurrent());
return mgr.acquire(); return mgr.acquire();
} }
private SearcherManager mgr; private SearcherManager mgr;
private boolean isNRT;
@Override @Override
protected void doAfterWriter(ExecutorService es) throws Exception { protected void doAfterWriter(ExecutorService es) throws Exception {
// SearcherManager needs to see empty commit: // SearcherManager needs to see empty commit:
writer.commit(); final SearcherWarmer warmer = new SearcherWarmer() {
mgr = new SearcherManager(dir,
new SearcherWarmer() {
@Override @Override
public void warm(IndexSearcher s) throws IOException { public void warm(IndexSearcher s) throws IOException {
TestSearcherManager.this.warmCalled = true; TestSearcherManager.this.warmCalled = true;
s.search(new TermQuery(new Term("body", "united")), 10); s.search(new TermQuery(new Term("body", "united")), 10);
} }
}, es); };
if (random.nextBoolean()) {
mgr = SearcherManager.open(writer, true, warmer, es);
isNRT = true;
} else {
writer.commit();
mgr = SearcherManager.open(dir, warmer, es);
isNRT = false;
}
} }
@Override @Override
@ -126,9 +139,8 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
writer.commit(); writer.commit();
final CountDownLatch awaitEnterWarm = new CountDownLatch(1); final CountDownLatch awaitEnterWarm = new CountDownLatch(1);
final CountDownLatch awaitClose = new CountDownLatch(1); final CountDownLatch awaitClose = new CountDownLatch(1);
final ExecutorService es = random.nextBoolean() ? null : Executors.newCachedThreadPool(new NamedThreadFactory("testIntermediateClose"));
final SearcherManager searcherManager = new SearcherManager(dir, final SearcherWarmer warmer = new SearcherWarmer() {
new SearcherWarmer() {
@Override @Override
public void warm(IndexSearcher s) throws IOException { public void warm(IndexSearcher s) throws IOException {
try { try {
@ -138,7 +150,9 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
// //
} }
} }
}); };
final SearcherManager searcherManager = random.nextBoolean() ? SearcherManager.open(dir,
warmer, es) : SearcherManager.open(writer, random.nextBoolean(), warmer, es);
IndexSearcher searcher = searcherManager.acquire(); IndexSearcher searcher = searcherManager.acquire();
try { try {
assertEquals(1, searcher.getIndexReader().numDocs()); assertEquals(1, searcher.getIndexReader().numDocs());
@ -185,6 +199,9 @@ public class TestSearcherManager extends ThreadedIndexingAndSearchingTestCase {
assertNull("" + exc[0], exc[0]); assertNull("" + exc[0], exc[0]);
writer.close(); writer.close();
dir.close(); dir.close();
if (es != null) {
es.shutdown();
es.awaitTermination(1, TimeUnit.SECONDS);
}
} }
} }