1
package org.apache.lucene.search;
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
11
* http://www.apache.org/licenses/LICENSE-2.0
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.
20
import java.io.IOException;
22
import org.apache.lucene.index.IndexReader;
23
import org.apache.lucene.util.Counter;
24
import org.apache.lucene.util.ThreadInterruptedException;
27
* The {@link TimeLimitingCollector} is used to timeout search requests that
28
* take longer than the maximum allowed search time limit. After this time is
29
* exceeded, the search thread is stopped by throwing a
30
* {@link TimeExceededException}.
32
public class TimeLimitingCollector extends Collector {
35
/** Thrown when elapsed search time exceeds allowed search time. */
36
@SuppressWarnings("serial")
37
public static class TimeExceededException extends RuntimeException {
38
private long timeAllowed;
39
private long timeElapsed;
40
private int lastDocCollected;
41
private TimeExceededException(long timeAllowed, long timeElapsed, int lastDocCollected) {
42
super("Elapsed time: " + timeElapsed + "Exceeded allowed search time: " + timeAllowed + " ms.");
43
this.timeAllowed = timeAllowed;
44
this.timeElapsed = timeElapsed;
45
this.lastDocCollected = lastDocCollected;
47
/** Returns allowed time (milliseconds). */
48
public long getTimeAllowed() {
51
/** Returns elapsed time (milliseconds). */
52
public long getTimeElapsed() {
55
/** Returns last doc (absolute doc id) that was collected when the search time exceeded. */
56
public int getLastDocCollected() {
57
return lastDocCollected;
61
private long t0 = Long.MIN_VALUE;
62
private long timeout = Long.MIN_VALUE;
63
private final Collector collector;
64
private final Counter clock;
65
private final long ticksAllowed;
66
private boolean greedy = false;
70
* Create a TimeLimitedCollector wrapper over another {@link Collector} with a specified timeout.
71
* @param collector the wrapped {@link Collector}
72
* @param clock the timer clock
73
* @param ticksAllowed max time allowed for collecting
74
* hits after which {@link TimeExceededException} is thrown
76
public TimeLimitingCollector(final Collector collector, Counter clock, final long ticksAllowed ) {
77
this.collector = collector;
79
this.ticksAllowed = ticksAllowed;
83
* Sets the baseline for this collector. By default the collectors baseline is
84
* initialized once the first reader is passed to the collector.
85
* To include operations executed in prior to the actual document collection
86
* set the baseline through this method in your prelude.
90
* Counter clock = ...;
91
* long baseline = clock.get();
92
* // ... prepare search
93
* TimeLimitingCollector collector = new TimeLimitingCollector(c, clock, numTicks);
94
* collector.setBaseline(baseline);
95
* indexSearcher.search(query, collector);
101
public void setBaseline(long clockTime) {
103
timeout = t0 + ticksAllowed;
107
* Syntactic sugar for {@link #setBaseline(long)} using {@link Counter#get()}
108
* on the clock passed to the construcutor.
110
public void setBaseline() {
111
setBaseline(clock.get());
115
* Checks if this time limited collector is greedy in collecting the last hit.
116
* A non greedy collector, upon a timeout, would throw a {@link TimeExceededException}
117
* without allowing the wrapped collector to collect current doc. A greedy one would
118
* first allow the wrapped hit collector to collect current doc and only then
119
* throw a {@link TimeExceededException}.
120
* @see #setGreedy(boolean)
122
public boolean isGreedy() {
127
* Sets whether this time limited collector is greedy.
128
* @param greedy true to make this time limited greedy
131
public void setGreedy(boolean greedy) {
132
this.greedy = greedy;
136
* Calls {@link Collector#collect(int)} on the decorated {@link Collector}
137
* unless the allowed time has passed, in which case it throws an exception.
139
* @throws TimeExceededException
140
* if the time allowed has exceeded.
143
public void collect(final int doc) throws IOException {
144
final long time = clock.get();
145
if (timeout < time) {
147
//System.out.println(this+" greedy: before failing, collecting doc: "+(docBase + doc)+" "+(time-t0));
148
collector.collect(doc);
150
//System.out.println(this+" failing on: "+(docBase + doc)+" "+(time-t0));
151
throw new TimeExceededException( timeout-t0, time-t0, docBase + doc );
153
//System.out.println(this+" collecting: "+(docBase + doc)+" "+(time-t0));
154
collector.collect(doc);
158
public void setNextReader(IndexReader reader, int base) throws IOException {
159
collector.setNextReader(reader, base);
161
if (Long.MIN_VALUE == t0) {
167
public void setScorer(Scorer scorer) throws IOException {
168
collector.setScorer(scorer);
172
public boolean acceptsDocsOutOfOrder() {
173
return collector.acceptsDocsOutOfOrder();
178
* Returns the global TimerThreads {@link Counter}
180
* Invoking this creates may create a new instance of {@link TimerThread} iff
181
* the global {@link TimerThread} has never been accessed before. The thread
182
* returned from this method is started on creation and will be alive unless
183
* you stop the {@link TimerThread} via {@link TimerThread#stopTimer()}.
185
* @return the global TimerThreads {@link Counter}
186
* @lucene.experimental
188
public static Counter getGlobalCounter() {
189
return TimerThreadHolder.THREAD.counter;
193
* Returns the global {@link TimerThread}.
195
* Invoking this creates may create a new instance of {@link TimerThread} iff
196
* the global {@link TimerThread} has never been accessed before. The thread
197
* returned from this method is started on creation and will be alive unless
198
* you stop the {@link TimerThread} via {@link TimerThread#stopTimer()}.
201
* @return the global {@link TimerThread}
202
* @lucene.experimental
204
public static TimerThread getGlobalTimerThread() {
205
return TimerThreadHolder.THREAD;
208
private static final class TimerThreadHolder {
209
static final TimerThread THREAD;
211
THREAD = new TimerThread(Counter.newCounter(true));
217
* @lucene.experimental
219
public static final class TimerThread extends Thread {
221
public static final String THREAD_NAME = "TimeLimitedCollector timer thread";
222
public static final int DEFAULT_RESOLUTION = 20;
223
// NOTE: we can avoid explicit synchronization here for several reasons:
224
// * updates to volatile long variables are atomic
225
// * only single thread modifies this value
226
// * use of volatile keyword ensures that it does not reside in
227
// a register, but in main memory (so that changes are visible to
229
// * visibility of changes does not need to be instantaneous, we can
230
// afford losing a tick or two.
232
// See section 17 of the Java Language Specification for details.
233
private volatile long time = 0;
234
private volatile boolean stop = false;
235
private volatile long resolution;
236
final Counter counter;
238
public TimerThread(long resolution, Counter counter) {
240
this.resolution = resolution;
241
this.counter = counter;
242
this.setDaemon(true);
245
public TimerThread(Counter counter) {
246
this(DEFAULT_RESOLUTION, counter);
252
// TODO: Use System.nanoTime() when Lucene moves to Java SE 5.
253
counter.addAndGet(resolution);
255
Thread.sleep( resolution );
256
} catch (InterruptedException ie) {
257
throw new ThreadInterruptedException(ie);
263
* Get the timer value in milliseconds.
265
public long getMilliseconds() {
270
* Stops the timer thread
272
public void stopTimer() {
277
* Return the timer resolution.
278
* @see #setResolution(long)
280
public long getResolution() {
285
* Set the timer resolution.
286
* The default timer resolution is 20 milliseconds.
287
* This means that a search required to take no longer than
288
* 800 milliseconds may be stopped after 780 to 820 milliseconds.
291
* <li>Finer (smaller) resolution is more accurate but less efficient.</li>
292
* <li>Setting resolution to less than 5 milliseconds will be silently modified to 5 milliseconds.</li>
293
* <li>Setting resolution smaller than current resolution might take effect only after current
294
* resolution. (Assume current resolution of 20 milliseconds is modified to 5 milliseconds,
295
* then it can take up to 20 milliseconds for the change to have effect.</li>
298
public void setResolution(long resolution) {
299
this.resolution = Math.max(resolution, 5); // 5 milliseconds is about the minimum reasonable time for a Object.wait(long) call.