/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.search;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.util.IOUtils;
Keeps track of current plus old IndexSearchers, closing
the old ones once they have timed out.
Use it like this:
SearcherLifetimeManager mgr = new SearcherLifetimeManager();
Per search-request, if it's a "new" search request, then obtain the latest searcher you have (for example, by using SearcherManager
), and then record this searcher: // Record the current searcher, and save the returend
// token into user's search results (eg as a hidden
// HTML form field):
long token = mgr.record(searcher);
When a follow-up search arrives, for example the user
clicks next page, drills down/up, etc., take the token
that you saved from the previous search and:
// If possible, obtain the same searcher as the last
// search:
IndexSearcher searcher = mgr.acquire(token);
if (searcher != null) {
// Searcher is still here
try {
// do searching...
} finally {
mgr.release(searcher);
// Do not use searcher after this!
searcher = null;
}
} else {
// Searcher was pruned -- notify user session timed
// out, or, pull fresh searcher again
}
Finally, in a separate thread, ideally the same thread
that's periodically reopening your searchers, you should
periodically prune old searchers:
mgr.prune(new PruneByAge(600.0));
NOTE: keeping many searchers around means you'll use more resources (open files, RAM) than a single searcher. However, as long as you are using DirectoryReader.openIfChanged(DirectoryReader)
, the searchers will usually share almost all segments and the added resource usage is contained. When a large merge has completed, and you reopen, because that is a large change, the new searcher will use higher additional RAM than other searchers; but large merges don't complete very often and it's unlikely you'll hit two of them in your expiration window. Still you should budget plenty of heap in the JVM to have a good safety margin.
@lucene.experimental
/**
* Keeps track of current plus old IndexSearchers, closing
* the old ones once they have timed out.
*
* Use it like this:
*
* <pre class="prettyprint">
* SearcherLifetimeManager mgr = new SearcherLifetimeManager();
* </pre>
*
* Per search-request, if it's a "new" search request, then
* obtain the latest searcher you have (for example, by
* using {@link SearcherManager}), and then record this
* searcher:
*
* <pre class="prettyprint">
* // Record the current searcher, and save the returend
* // token into user's search results (eg as a hidden
* // HTML form field):
* long token = mgr.record(searcher);
* </pre>
*
* When a follow-up search arrives, for example the user
* clicks next page, drills down/up, etc., take the token
* that you saved from the previous search and:
*
* <pre class="prettyprint">
* // If possible, obtain the same searcher as the last
* // search:
* IndexSearcher searcher = mgr.acquire(token);
* if (searcher != null) {
* // Searcher is still here
* try {
* // do searching...
* } finally {
* mgr.release(searcher);
* // Do not use searcher after this!
* searcher = null;
* }
* } else {
* // Searcher was pruned -- notify user session timed
* // out, or, pull fresh searcher again
* }
* </pre>
*
* Finally, in a separate thread, ideally the same thread
* that's periodically reopening your searchers, you should
* periodically prune old searchers:
*
* <pre class="prettyprint">
* mgr.prune(new PruneByAge(600.0));
* </pre>
*
* <p><b>NOTE</b>: keeping many searchers around means
* you'll use more resources (open files, RAM) than a single
* searcher. However, as long as you are using {@link
* DirectoryReader#openIfChanged(DirectoryReader)}, the searchers
* will usually share almost all segments and the added resource usage
* is contained. When a large merge has completed, and
* you reopen, because that is a large change, the new
* searcher will use higher additional RAM than other
* searchers; but large merges don't complete very often and
* it's unlikely you'll hit two of them in your expiration
* window. Still you should budget plenty of heap in the
* JVM to have a good safety margin.
*
* @lucene.experimental
*/
public class SearcherLifetimeManager implements Closeable {
static final double NANOS_PER_SEC = 1000000000.0;
private static class SearcherTracker implements Comparable<SearcherTracker>, Closeable {
public final IndexSearcher searcher;
public final double recordTimeSec;
public final long version;
public SearcherTracker(IndexSearcher searcher) {
this.searcher = searcher;
version = ((DirectoryReader) searcher.getIndexReader()).getVersion();
searcher.getIndexReader().incRef();
// Use nanoTime not currentTimeMillis since it [in
// theory] reduces risk from clock shift
recordTimeSec = System.nanoTime() / NANOS_PER_SEC;
}
// Newer searchers are sort before older ones:
@Override
public int compareTo(SearcherTracker other) {
return Double.compare(other.recordTimeSec, recordTimeSec);
}
@Override
public synchronized void close() throws IOException {
searcher.getIndexReader().decRef();
}
}
private volatile boolean closed;
// TODO: we could get by w/ just a "set"; need to have
// Tracker hash by its version and have compareTo(Long)
// compare to its version
private final ConcurrentHashMap<Long,SearcherTracker> searchers = new ConcurrentHashMap<>();
private void ensureOpen() {
if (closed) {
throw new AlreadyClosedException("this SearcherLifetimeManager instance is closed");
}
}
Records that you are now using this IndexSearcher. Always call this when you've obtained a possibly new IndexSearcher
, for example from SearcherManager
. It's fine if you already passed the same searcher to this method before. This returns the long token that you can later pass to acquire
to retrieve the same IndexSearcher. You should record this long token in the search results sent to your user, such that if the user performs a follow-on action (clicks next page, drills down, etc.) the token is returned.
/** Records that you are now using this IndexSearcher.
* Always call this when you've obtained a possibly new
* {@link IndexSearcher}, for example from {@link
* SearcherManager}. It's fine if you already passed the
* same searcher to this method before.
*
* <p>This returns the long token that you can later pass
* to {@link #acquire} to retrieve the same IndexSearcher.
* You should record this long token in the search results
* sent to your user, such that if the user performs a
* follow-on action (clicks next page, drills down, etc.)
* the token is returned. */
public long record(IndexSearcher searcher) throws IOException {
ensureOpen();
// TODO: we don't have to use IR.getVersion to track;
// could be risky (if it's buggy); we could get better
// bug isolation if we assign our own private ID:
final long version = ((DirectoryReader) searcher.getIndexReader()).getVersion();
SearcherTracker tracker = searchers.get(version);
if (tracker == null) {
//System.out.println("RECORD version=" + version + " ms=" + System.currentTimeMillis());
tracker = new SearcherTracker(searcher);
if (searchers.putIfAbsent(version, tracker) != null) {
// Another thread beat us -- must decRef to undo
// incRef done by SearcherTracker ctor:
tracker.close();
}
} else if (tracker.searcher != searcher) {
throw new IllegalArgumentException("the provided searcher has the same underlying reader version yet the searcher instance differs from before (new=" + searcher + " vs old=" + tracker.searcher);
}
return version;
}
Retrieve a previously recorded IndexSearcher
, if it has not yet been closed NOTE: this may return null when the
requested searcher has already timed out. When this
happens you should notify your user that their session
timed out and that they'll have to restart their
search.
If this returns a non-null result, you must match later call release
on this searcher, best from a finally clause.
/** Retrieve a previously recorded {@link IndexSearcher}, if it
* has not yet been closed
*
* <p><b>NOTE</b>: this may return null when the
* requested searcher has already timed out. When this
* happens you should notify your user that their session
* timed out and that they'll have to restart their
* search.
*
* <p>If this returns a non-null result, you must match
* later call {@link #release} on this searcher, best
* from a finally clause. */
public IndexSearcher acquire(long version) {
ensureOpen();
final SearcherTracker tracker = searchers.get(version);
if (tracker != null &&
tracker.searcher.getIndexReader().tryIncRef()) {
return tracker.searcher;
}
return null;
}
Release a searcher previously obtained from acquire
. NOTE: it's fine to call this after close.
/** Release a searcher previously obtained from {@link
* #acquire}.
*
* <p><b>NOTE</b>: it's fine to call this after close. */
public void release(IndexSearcher s) throws IOException {
s.getIndexReader().decRef();
}
See prune
. /** See {@link #prune}. */
public interface Pruner {
Return true if this searcher should be removed.
@param ageSec how much time has passed since this
searcher was the current (live) searcher
@param searcher Searcher
/** Return true if this searcher should be removed.
* @param ageSec how much time has passed since this
* searcher was the current (live) searcher
* @param searcher Searcher
**/
public boolean doPrune(double ageSec, IndexSearcher searcher);
}
Simple pruner that drops any searcher older by
more than the specified seconds, than the newest
searcher. /** Simple pruner that drops any searcher older by
* more than the specified seconds, than the newest
* searcher. */
public final static class PruneByAge implements Pruner {
private final double maxAgeSec;
public PruneByAge(double maxAgeSec) {
if (maxAgeSec < 0) {
throw new IllegalArgumentException("maxAgeSec must be > 0 (got " + maxAgeSec + ")");
}
this.maxAgeSec = maxAgeSec;
}
@Override
public boolean doPrune(double ageSec, IndexSearcher searcher) {
return ageSec > maxAgeSec;
}
}
Calls provided Pruner
to prune entries. The entries are passed to the Pruner in sorted (newest to oldest IndexSearcher) order. NOTE: you must periodically call this, ideally
from the same background thread that opens new
searchers.
/** Calls provided {@link Pruner} to prune entries. The
* entries are passed to the Pruner in sorted (newest to
* oldest IndexSearcher) order.
*
* <p><b>NOTE</b>: you must periodically call this, ideally
* from the same background thread that opens new
* searchers. */
public synchronized void prune(Pruner pruner) throws IOException {
// Cannot just pass searchers.values() to ArrayList ctor
// (not thread-safe since the values can change while
// ArrayList is init'ing itself); must instead iterate
// ourselves:
final List<SearcherTracker> trackers = new ArrayList<>();
for(SearcherTracker tracker : searchers.values()) {
trackers.add(tracker);
}
Collections.sort(trackers);
double lastRecordTimeSec = 0.0;
final double now = System.nanoTime()/NANOS_PER_SEC;
for (SearcherTracker tracker: trackers) {
final double ageSec;
if (lastRecordTimeSec == 0.0) {
ageSec = 0.0;
} else {
ageSec = now - lastRecordTimeSec;
}
// First tracker is always age 0.0 sec, since it's
// still "live"; second tracker's age (= seconds since
// it was "live") is now minus first tracker's
// recordTime, etc:
if (pruner.doPrune(ageSec, tracker.searcher)) {
//System.out.println("PRUNE version=" + tracker.version + " age=" + ageSec + " ms=" + System.currentTimeMillis());
searchers.remove(tracker.version);
tracker.close();
}
lastRecordTimeSec = tracker.recordTimeSec;
}
}
Close this to future searching; any searches still in process in other threads won't be affected, and they should still call release
after they are done. NOTE: you must ensure no other threads are calling record
while you call close(); otherwise it's possible not all searcher references will be freed.
/** Close this 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>: you must ensure no other threads are
* calling {@link #record} while you call close();
* otherwise it's possible not all searcher references
* will be freed. */
@Override
public synchronized void close() throws IOException {
closed = true;
final List<SearcherTracker> toClose = new ArrayList<>(searchers.values());
// Remove up front in case exc below, so we don't
// over-decRef on double-close:
for(SearcherTracker tracker : toClose) {
searchers.remove(tracker.version);
}
IOUtils.close(toClose);
// Make some effort to catch mis-use:
if (searchers.size() != 0) {
throw new IllegalStateException("another thread called record while this SearcherLifetimeManager instance was being closed; not all searchers were closed");
}
}
}