Index: src/test/org/apache/lucene/search/TestTimeLimitedCollector.java
===================================================================
--- src/test/org/apache/lucene/search/TestTimeLimitedCollector.java	(revision 0)
+++ src/test/org/apache/lucene/search/TestTimeLimitedCollector.java	(revision 0)
@@ -0,0 +1,185 @@
+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 junit.framework.TestCase;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.RAMDirectory;
+
+import java.io.IOException;
+import java.util.BitSet;
+
+/**
+ * Tests the TimeLimitedCollector.  This test checks (1) search
+ * correctness (regardless of timeout), (2) expected timeout behavior,
+ * and (3) a sanity test with multiple searching threads.
+ */
+public class TestTimeLimitedCollector extends TestCase {
+
+  private Searcher searcher;
+  private final String FIELD_NAME = "body";
+
+  public TestTimeLimitedCollector(String name)
+  {
+    super(name);
+  }
+
+  /**
+   * initializes searcher with a document set
+   */
+  protected void setUp() throws Exception {
+    Directory d = new RAMDirectory();
+    IndexWriter iw = new IndexWriter(d, new StandardAnalyzer(), true);
+    add("one blah three", iw);
+    add("one foo three multiOne", iw);
+    add("one foobar three multiThree", iw);
+    add("blueberry pancakes", iw);
+    add("blueberry pie", iw);
+    add("blueberry strudel", iw);
+    add("blueberry pizza", iw);
+    iw.close();
+
+    searcher = new IndexSearcher(d);
+  }
+
+  public void tearDown() throws Exception {
+    searcher.close();
+  }
+
+  private void add(String value, IndexWriter iw) throws IOException {
+    Document d = new Document();
+    d.add(new Field(FIELD_NAME, value, Field.Store.YES, Field.Index.TOKENIZED));
+    iw.addDocument(d);
+  }
+
+  private void search() throws Exception {
+    QueryParser queryParser
+      = new QueryParser(FIELD_NAME, new StandardAnalyzer());
+    Query query = queryParser.parse("blueberry");
+
+    // do search
+    MyHitCollector myHc = new MyHitCollector();
+    HitCollector collector = new TimeLimitedCollector(myHc, 10000);
+    searcher.search(query, collector);
+    int totalTLCResults = myHc.hitCount();
+    
+    myHc = new MyHitCollector();
+    searcher.search(query, myHc);
+    int totalResults = myHc.hitCount();
+
+    assertEquals( totalResults, totalTLCResults );
+  }
+
+  /**
+   * test search correctness with no timeout
+   */
+  public void testSearch() throws Exception
+  {
+    search();
+  }
+
+  public void testTimeout() throws Exception
+  {
+    QueryParser queryParser
+      = new QueryParser(FIELD_NAME, new StandardAnalyzer());
+    Query query = queryParser.parse("blueberry");
+
+    // do search
+    MyHitCollector myHc = new MyHitCollector();
+    myHc.setSlowDown(500);
+    HitCollector collector = new TimeLimitedCollector(myHc, 1000);
+
+    TimeLimitedCollector.TimeExceeded exception = null;
+    try {
+      searcher.search(query, collector);
+    }
+    catch (TimeLimitedCollector.TimeExceeded x) {
+      exception = x;
+    }
+    assertNotNull( "Timeout expected.", exception );
+    assertTrue( myHc.hitCount() > 0 );
+    assertTrue( exception.maxDoc > 0 );
+    assertTrue( exception.maxTime > 0 );
+  }
+
+  public void testMultiThreaded() throws Exception
+  {
+    Thread [] threadArray = new Thread[5];
+    for( int i = 0; i < threadArray.length; ++i ) {
+      threadArray[i] = new Thread() {
+          public void run() {
+            for( int j = 0; j < 1000; ++j ) {
+              try {
+                search();
+              }
+              catch( Exception x ) {
+                fail( "Caught exception: " + x );
+              }
+            }
+          }
+      };
+    }
+    for( int i = 0; i < threadArray.length; ++i ) {
+      threadArray[i].start();
+    }
+    for( int i = 0; i < threadArray.length; ++i ) {
+      threadArray[i].join();
+    }
+  }
+
+  private class MyHitCollector extends HitCollector
+  {
+    private final BitSet bits = new BitSet();
+    private int slowdown = 0;
+
+    /**
+     * amount of time to wait on each collect to simulate a long iteration
+     */
+    public void setSlowDown( int milliseconds )
+    {
+      slowdown = milliseconds;
+    }
+    
+    public void collect( final int doc, final float score )
+    {
+      if( slowdown > 0 ) {
+        try {
+          Thread.currentThread().sleep(slowdown);
+        }
+        catch(InterruptedException x) {
+          System.out.println("caught " + x);
+        }
+      }
+      bits.set( doc );
+    }
+    
+    public int hitCount()
+    {
+      return bits.cardinality();
+    }
+    
+  }
+
+}
+  
Index: src/java/org/apache/lucene/search/TimeLimitedCollector.java
===================================================================
--- src/java/org/apache/lucene/search/TimeLimitedCollector.java	(revision 0)
+++ src/java/org/apache/lucene/search/TimeLimitedCollector.java	(revision 0)
@@ -0,0 +1,130 @@
+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.
+ */
+
+/**
+ * <p>The TimeLimitedCollector 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
+ * TimeExceeded Exception.</p>
+ */
+public class TimeLimitedCollector extends HitCollector
+{
+  private static class TimerThread extends Thread
+  {
+    private final long resolution = 10;  // this is about the minimum reasonable time for a Object.wait(long) call.
+
+    // 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 instantanous, 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 );
+    }
+
+    public void run()
+    {
+      boolean interrupted = false;
+      try
+      {
+        while( true ) {
+          // TODO: Use System.nanoTime() when Lucene moves to Java SE 5.
+          time += resolution;
+          try {
+            Thread.sleep( resolution );
+          }
+          catch( final InterruptedException e ) {
+            interrupted = true;
+          }
+        }
+      }
+      finally {
+        if( interrupted ) {
+          Thread.currentThread().interrupt();
+        }
+      }
+    }
+
+    /**
+     * Get the timer value in milliseconds.
+     */
+    public long getMilliseconds()
+    {
+      return time;
+    }
+  }
+
+  public static class TimeExceeded extends RuntimeException {
+    public long maxTime;
+    public int maxDoc;
+    public TimeExceeded(long maxTime, int maxDoc) {
+      super("Exceeded search time: " + maxTime + " ms.");
+      this.maxTime = maxTime;
+      this.maxDoc = maxDoc;
+    }
+  }
+
+  // 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 HitCollector hc;
+
+  public TimeLimitedCollector( final HitCollector hc, final long timeAllowed )
+  {
+    this.hc = hc;
+    t0 = TIMER_THREAD.getMilliseconds();
+    this.timeout = t0 + timeAllowed;
+  }
+
+  /**
+   * Calls collect() on the decorated HitCollector.
+   * 
+   * @throws TimeExceeded if the time allowed has been exceeded.
+   */
+  public void collect( final int doc, final float score )
+  {
+    if( timeout < TIMER_THREAD.getMilliseconds() ) {
+      throw new TimeExceeded( (TIMER_THREAD.getMilliseconds() - t0), doc );
+    }
+    hc.collect( doc, score );
+  }
+}
