public static class

TimeLimitingCollector.TimeExceededException

extends RuntimeException
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();
  }

}