pylucene 3.5.0-3
[pylucene.git] / lucene-java-3.5.0 / lucene / src / java / org / apache / lucene / search / SearcherLifetimeManager.java
1 package org.apache.lucene.search;
2
3 /**
4  * Licensed to the Apache Software Foundation (ASF) under one or more
5  * contributor license agreements.  See the NOTICE file distributed with
6  * this work for additional information regarding copyright ownership.
7  * The ASF licenses this file to You under the Apache License, Version 2.0
8  * (the "License"); you may not use this file except in compliance with
9  * the License.  You may obtain a copy of the License at
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19
20 import java.io.Closeable;
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.TimeUnit;
27
28 import org.apache.lucene.index.IndexReader;        // javadocs
29 import org.apache.lucene.search.NRTManager;        // javadocs
30 import org.apache.lucene.search.IndexSearcher;        // javadocs
31 import org.apache.lucene.store.AlreadyClosedException;
32 import org.apache.lucene.util.IOUtils;
33
34 /**
35  * Keeps track of current plus old IndexSearchers, closing
36  * the old ones once they have timed out.
37  *
38  * Use it like this:
39  *
40  * <pre>
41  *   SearcherLifetimeManager mgr = new SearcherLifetimeManager();
42  * </pre>
43  *
44  * Per search-request, if it's a "new" search request, then
45  * obtain the latest searcher you have (for example, by
46  * using {@link SearcherManager} or {@link NRTManager}), and
47  * then record this searcher:
48  *
49  * <pre>
50  *   // Record the current searcher, and save the returend
51  *   // token into user's search results (eg as a  hidden
52  *   // HTML form field):
53  *   long token = mgr.record(searcher);
54  * </pre>
55  *
56  * When a follow-up search arrives, for example the user
57  * clicks next page, drills down/up, etc., take the token
58  * that you saved from the previous search and:
59  *
60  * <pre>
61  *   // If possible, obtain the same searcher as the last
62  *   // search:
63  *   IndexSearcher searcher = mgr.acquire(token);
64  *   if (searcher != null) {
65  *     // Searcher is still here
66  *     try {
67  *       // do searching...
68  *     } finally {
69  *       mgr.release(searcher);
70  *       // Do not use searcher after this!
71  *       searcher = null;
72  *     }
73  *   } else {
74  *     // Searcher was pruned -- notify user session timed
75  *     // out, or, pull fresh searcher again
76  *   }
77  * </pre>
78  *
79  * Finally, in a separate thread, ideally the same thread
80  * that's periodically reopening your searchers, you should
81  * periodically prune old searchers:
82  *
83  * <pre>
84  *   mgr.prune(new PruneByAge(600.0));
85  * </pre>
86  *
87  * <p><b>NOTE</b>: keeping many searchers around means
88  * you'll use more resources (open files, RAM) than a single
89  * searcher.  However, as long as you are using {@link
90  * IndexReader#openIfChanged}, the searchers will usually
91  * share almost all segments and the added resource usage is
92  * contained.  When a large merge has completed, and
93  * you reopen, because that is a large change, the new
94  * searcher will use higher additional RAM than other
95  * searchers; but large merges don't complete very often and
96  * it's unlikely you'll hit two of them in your expiration
97  * window.  Still you should budget plenty of heap in the
98  * JVM to have a good safety margin.
99  * 
100  * @lucene.experimental
101  */
102
103 public class SearcherLifetimeManager implements Closeable {
104
105   private static class SearcherTracker implements Comparable<SearcherTracker>, Closeable {
106     public final IndexSearcher searcher;
107     public final long recordTimeSec;
108     public final long version;
109
110     public SearcherTracker(IndexSearcher searcher) {
111       this.searcher = searcher;
112       version = searcher.getIndexReader().getVersion();
113       searcher.getIndexReader().incRef();
114       // Use nanoTime not currentTimeMillis since it [in
115       // theory] reduces risk from clock shift
116       recordTimeSec = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime());
117     }
118
119     // Newer searchers are sort before older ones:
120     public int compareTo(SearcherTracker other) {
121       // Be defensive: cannot subtract since it could
122       // technically overflow long, though, we'd never hit
123       // that in practice:
124       if (recordTimeSec < other.recordTimeSec) {
125         return 1;
126       } else if (other.recordTimeSec < recordTimeSec) {
127         return -1;
128       } else {
129         return 0;
130       }
131     }
132
133     public synchronized void close() throws IOException {
134       searcher.getIndexReader().decRef();
135     }
136   }
137
138   private volatile boolean closed;
139
140   // TODO: we could get by w/ just a "set"; need to have
141   // Tracker hash by its version and have compareTo(Long)
142   // compare to its version
143   private final ConcurrentHashMap<Long,SearcherTracker> searchers = new ConcurrentHashMap<Long,SearcherTracker>();
144
145   private void ensureOpen() {
146     if (closed) {
147       throw new AlreadyClosedException("this SearcherLifetimeManager instance is closed");
148     }
149   }
150
151   /** Records that you are now using this IndexSearcher.
152    *  Always call this when you've obtained a possibly new
153    *  {@link IndexSearcher}, for example from one of the
154    *  <code>get</code> methods in {@link NRTManager} or {@link
155    *  SearcherManager}.  It's fine if you already passed the
156    *  same searcher to this method before.
157    *
158    *  <p>This returns the long token that you can later pass
159    *  to {@link #acquire} to retrieve the same IndexSearcher.
160    *  You should record this long token in the search results
161    *  sent to your user, such that if the user performs a
162    *  follow-on action (clicks next page, drills down, etc.)
163    *  the token is returned. */
164   public long record(IndexSearcher searcher) throws IOException {
165     ensureOpen();
166     // TODO: we don't have to use IR.getVersion to track;
167     // could be risky (if it's buggy); we could get better
168     // bug isolation if we assign our own private ID:
169     final long version = searcher.getIndexReader().getVersion();
170     SearcherTracker tracker = searchers.get(version);
171     if (tracker == null) {
172       tracker = new SearcherTracker(searcher);
173       if (searchers.putIfAbsent(version, tracker) != null) {
174         // Another thread beat us -- must decRef to undo
175         // incRef done by SearcherTracker ctor:
176         tracker.close();
177       }
178     } else if (tracker.searcher != searcher) {
179       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);
180     }
181
182     return version;
183   }
184
185   /** Retrieve a previously recorded {@link IndexSearcher}, if it
186    *  has not yet been closed
187    *
188    *  <p><b>NOTE</b>: this may return null when the
189    *  requested searcher has already timed out.  When this
190    *  happens you should notify your user that their session
191    *  timed out and that they'll have to restart their
192    *  search.
193    *
194    *  <p>If this returns a non-null result, you must match
195    *  later call {@link #release} on this searcher, best
196    *  from a finally clause. */
197   public IndexSearcher acquire(long version) {
198     ensureOpen();
199     final SearcherTracker tracker = searchers.get(version);
200     if (tracker != null &&
201         tracker.searcher.getIndexReader().tryIncRef()) {
202       return tracker.searcher;
203     }
204
205     return null;
206   }
207
208   /** Release a searcher previously obtained from {@link
209    *  #acquire}.
210    * 
211    * <p><b>NOTE</b>: it's fine to call this after close. */
212   public void release(IndexSearcher s) throws IOException {
213     s.getIndexReader().decRef();
214   }
215
216   /** See {@link #prune}. */
217   public interface Pruner {
218     /** Return true if this searcher should be removed. 
219      *  @param ageSec how long ago this searcher was
220      *         recorded vs the most recently recorded
221      *         searcher
222      *  @param searcher Searcher
223      **/
224     public boolean doPrune(int ageSec, IndexSearcher searcher);
225   }
226
227   /** Simple pruner that drops any searcher older by
228    *  more than the specified seconds, than the newest
229    *  searcher. */
230   public final static class PruneByAge implements Pruner {
231     private final int maxAgeSec;
232
233     public PruneByAge(int maxAgeSec) {
234       if (maxAgeSec < 1) {
235         throw new IllegalArgumentException("maxAgeSec must be > 0 (got " + maxAgeSec + ")");
236       }
237       this.maxAgeSec = maxAgeSec;
238     }
239
240     public boolean doPrune(int ageSec, IndexSearcher searcher) {
241       return ageSec > maxAgeSec;
242     }
243   }
244
245   /** Calls provided {@link Pruner} to prune entries.  The
246    *  entries are passed to the Pruner in sorted (newest to
247    *  oldest IndexSearcher) order.
248    * 
249    *  <p><b>NOTE</b>: you must peridiocally call this, ideally
250    *  from the same background thread that opens new
251    *  searchers. */
252   public synchronized void prune(Pruner pruner) throws IOException {
253     // Cannot just pass searchers.values() to ArrayList ctor
254     // (not thread-safe since the values can change while
255     // ArrayList is init'ing itself); must instead iterate
256     // ourselves:
257     final List<SearcherTracker> trackers = new ArrayList<SearcherTracker>();
258     for(SearcherTracker tracker : searchers.values()) {
259       trackers.add(tracker);
260     }
261     Collections.sort(trackers);
262     final long newestSec = trackers.isEmpty() ? 0L : trackers.get(0).recordTimeSec;
263     for (SearcherTracker tracker: trackers) {
264       final int ageSec = (int) (newestSec - tracker.recordTimeSec);
265       assert ageSec >= 0;
266       if (pruner.doPrune(ageSec, tracker.searcher)) {
267         searchers.remove(tracker.version);
268         tracker.close();
269       }
270     }
271   }
272
273   /** Close this to future searching; any searches still in
274    *  process in other threads won't be affected, and they
275    *  should still call {@link #release} after they are
276    *  done.
277    *
278    *  <p><b>NOTE: you must ensure no other threads are
279    *  calling {@link #record} while you call close();
280    *  otherwise it's possible not all searcher references
281    *  will be freed. */
282   public synchronized void close() throws IOException {
283     closed = true;
284     final List<SearcherTracker> toClose = new ArrayList<SearcherTracker>(searchers.values());
285
286     // Remove up front in case exc below, so we don't
287     // over-decRef on double-close:
288     for(SearcherTracker tracker : toClose) {
289       searchers.remove(tracker.version);
290     }
291
292     IOUtils.close(toClose);
293
294     // Make some effort to catch mis-use:
295     if (searchers.size() != 0) {
296       throw new IllegalStateException("another thread called record while this SearcherLifetimeManager instance was being closed; not all searchers were closed");
297     }
298   }
299 }