/* * 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: * * <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 * {@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 {@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 {@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 {@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 **/ public boolean doPrune(double ageSec, IndexSearcher 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 {@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 peridiocally 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 {@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"); } } }