package org.apache.lucene.search; /** * 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. */ import java.io.IOException; import org.apache.lucene.index.IndexReader; import org.apache.lucene.util.ThreadInterruptedException; /** * The {@link TimeLimitingCollector} is used to timeout search requests that * take longer than the maximum allowed search time limit. After this time is * exceeded, the search thread is stopped by throwing a * {@link TimeExceededException}. */ public class TimeLimitingCollector extends Collector { /** * Default timer resolution. * @see #setResolution(long) */ public static final int DEFAULT_RESOLUTION = 20; /** * Default for {@link #isGreedy()}. * @see #isGreedy() */ public boolean DEFAULT_GREEDY = false; private static long resolution = DEFAULT_RESOLUTION; private boolean greedy = DEFAULT_GREEDY ; private static final class TimerThread extends Thread { // NOTE: we can avoid explicit synchronization here for several reasons: // * updates to volatile long variables are atomic // * only single thread modifies this value // * use of volatile keyword ensures that it does not reside in // a register, but in main memory (so that changes are visible to // other threads). // * visibility of changes does not need to be instantaneous, we can // afford losing a tick or two. // // See section 17 of the Java Language Specification for details. private volatile long time = 0; /** * TimerThread provides a pseudo-clock service to all searching * threads, so that they can count elapsed time with less overhead * than repeatedly calling System.currentTimeMillis. A single * thread should be created to be used for all searches. */ private TimerThread() { super("TimeLimitedCollector timer thread"); this.setDaemon( true ); } @Override public void run() { while (true) { // TODO: Use System.nanoTime() when Lucene moves to Java SE 5. time += resolution; try { Thread.sleep( resolution ); } catch (InterruptedException ie) { throw new ThreadInterruptedException(ie); } } } /** * Get the timer value in milliseconds. */ public long getMilliseconds() { return time; } } /** Thrown when elapsed search time exceeds allowed search time. */ public static class TimeExceededException extends RuntimeException { private long timeAllowed; private long timeElapsed; private int lastDocCollected; private TimeExceededException(long timeAllowed, long timeElapsed, int lastDocCollected) { super("Elapsed time: " + timeElapsed + "Exceeded allowed search time: " + timeAllowed + " ms."); this.timeAllowed = timeAllowed; this.timeElapsed = timeElapsed; this.lastDocCollected = lastDocCollected; } /** Returns allowed time (milliseconds). */ public long getTimeAllowed() { return timeAllowed; } /** Returns elapsed time (milliseconds). */ public long getTimeElapsed() { return timeElapsed; } /** Returns last doc (absolute doc id) that was collected when the search time exceeded. */ public int getLastDocCollected() { return lastDocCollected; } } // Declare and initialize a single static timer thread to be used by // all TimeLimitedCollector instances. The JVM assures that // this only happens once. private final static TimerThread TIMER_THREAD = new TimerThread(); static { TIMER_THREAD.start(); } private final long t0; private final long timeout; private final Collector collector; private int docBase; /** * Create a TimeLimitedCollector wrapper over another {@link Collector} with a specified timeout. * @param collector the wrapped {@link Collector} * @param timeAllowed max time allowed for collecting hits after which {@link TimeExceededException} is thrown */ public TimeLimitingCollector(final Collector collector, final long timeAllowed ) { this.collector = collector; t0 = TIMER_THREAD.getMilliseconds(); this.timeout = t0 + timeAllowed; } /** * Return the timer resolution. * @see #setResolution(long) */ public static long getResolution() { return resolution; } /** * Set the timer resolution. * The default timer resolution is 20 milliseconds. * This means that a search required to take no longer than * 800 milliseconds may be stopped after 780 to 820 milliseconds. * <br>Note that: * <ul> * <li>Finer (smaller) resolution is more accurate but less efficient.</li> * <li>Setting resolution to less than 5 milliseconds will be silently modified to 5 milliseconds.</li> * <li>Setting resolution smaller than current resolution might take effect only after current * resolution. (Assume current resolution of 20 milliseconds is modified to 5 milliseconds, * then it can take up to 20 milliseconds for the change to have effect.</li> * </ul> */ public static void setResolution(long newResolution) { resolution = Math.max(newResolution,5); // 5 milliseconds is about the minimum reasonable time for a Object.wait(long) call. } /** * Checks if this time limited collector is greedy in collecting the last hit. * A non greedy collector, upon a timeout, would throw a {@link TimeExceededException} * without allowing the wrapped collector to collect current doc. A greedy one would * first allow the wrapped hit collector to collect current doc and only then * throw a {@link TimeExceededException}. * @see #setGreedy(boolean) */ public boolean isGreedy() { return greedy; } /** * Sets whether this time limited collector is greedy. * @param greedy true to make this time limited greedy * @see #isGreedy() */ public void setGreedy(boolean greedy) { this.greedy = greedy; } /** * Calls {@link Collector#collect(int)} on the decorated {@link Collector} * unless the allowed time has passed, in which case it throws an exception. * * @throws TimeExceededException * if the time allowed has exceeded. */ @Override public void collect(final int doc) throws IOException { long time = TIMER_THREAD.getMilliseconds(); if (timeout < time) { if (greedy) { //System.out.println(this+" greedy: before failing, collecting doc: "+(docBase + doc)+" "+(time-t0)); collector.collect(doc); } //System.out.println(this+" failing on: "+(docBase + doc)+" "+(time-t0)); throw new TimeExceededException( timeout-t0, time-t0, docBase + doc ); } //System.out.println(this+" collecting: "+(docBase + doc)+" "+(time-t0)); collector.collect(doc); } @Override public void setNextReader(IndexReader reader, int base) throws IOException { collector.setNextReader(reader, base); this.docBase = base; } @Override public void setScorer(Scorer scorer) throws IOException { collector.setScorer(scorer); } @Override public boolean acceptsDocsOutOfOrder() { return collector.acceptsDocsOutOfOrder(); } }