You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by do...@apache.org on 2009/08/04 18:47:00 UTC

svn commit: r800871 - in /incubator/lucene.net/trunk/C#/src: Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj Lucene.Net/Search/TimeLimitedCollector.cs Lucene.Net/SupportClass.cs Test/Search/TestTimeLimitedCollector.cs Test/Test-VS2005.csproj Test/Test.csproj

Author: dougsale
Date: Tue Aug  4 16:47:00 2009
New Revision: 800871

URL: http://svn.apache.org/viewvc?rev=800871&view=rev
Log:
Adding Lucene.Net.Search.TimeLimitedCollector.cs and Lucene.Net.Search.TestTimeLimitedCollector.cs

Note that the tests pass intermittenly.  This might be an issue with the timing.  The amount of time passed to the TimeLimitedCollector might be insufficient to collect the expected results.

Added:
    incubator/lucene.net/trunk/C#/src/Lucene.Net/Search/TimeLimitedCollector.cs
    incubator/lucene.net/trunk/C#/src/Test/Search/TestTimeLimitedCollector.cs
Modified:
    incubator/lucene.net/trunk/C#/src/Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj
    incubator/lucene.net/trunk/C#/src/Lucene.Net/SupportClass.cs
    incubator/lucene.net/trunk/C#/src/Test/Test-VS2005.csproj
    incubator/lucene.net/trunk/C#/src/Test/Test.csproj

Modified: incubator/lucene.net/trunk/C#/src/Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj?rev=800871&r1=800870&r2=800871&view=diff
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj (original)
+++ incubator/lucene.net/trunk/C#/src/Lucene.Net/Lucene.Net-2.4.0-VS2005.csproj Tue Aug  4 16:47:00 2009
@@ -701,6 +701,7 @@
     <Compile Include="Search\TermScorer.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="Search\TimeLimitedCollector.cs" />
     <Compile Include="Search\TopDocCollector.cs">
       <SubType>Code</SubType>
     </Compile>

Added: incubator/lucene.net/trunk/C#/src/Lucene.Net/Search/TimeLimitedCollector.cs
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Lucene.Net/Search/TimeLimitedCollector.cs?rev=800871&view=auto
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Lucene.Net/Search/TimeLimitedCollector.cs (added)
+++ incubator/lucene.net/trunk/C#/src/Lucene.Net/Search/TimeLimitedCollector.cs Tue Aug  4 16:47:00 2009
@@ -0,0 +1,252 @@
+/**
+ * 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.
+ */
+
+namespace Lucene.Net.Search
+{
+    /**
+     * <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 : HitCollector
+    {
+
+        /** 
+         * Default timer resolution.
+         * @see #setResolution(long) 
+         */
+        public static readonly uint DEFAULT_RESOLUTION = 20;
+
+        /**
+         * Default for {@link #isGreedy()}.
+         * @see #isGreedy()
+         */
+        public static readonly bool DEFAULT_GREEDY = false;
+
+        private static uint resolution = DEFAULT_RESOLUTION;
+
+        private bool greedy = DEFAULT_GREEDY;
+
+        internal class TimerThread : SupportClass.ThreadClass
+        {
+
+            // 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.
+            //
+            // {{dougsale-2.4.0}}
+            // C# does not support volatile longs.
+            // So, we must either use atomic reads and writes or we can use
+            // a smaler volatile container.  As practical timer considerations
+            // do not require the resolution of a long, use an uint.
+            private volatile uint 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.
+             */
+            internal TimerThread()
+                : base("TimeLimitedCollector timer thread")
+            {
+                this.SetDaemon(true);
+            }
+
+            override public void Run()
+            {
+                bool interrupted = false;
+                try
+                {
+                    while (true)
+                    {
+                        // TODO: Use System.nanoTime() when Lucene moves to Java SE 5.
+                        time += resolution;
+                        try
+                        {
+                            SupportClass.ThreadClass.Sleep(resolution);
+                        }
+                        catch (System.Threading.ThreadInterruptedException)
+                        {
+                            interrupted = true;
+                        }
+                    }
+                }
+                finally
+                {
+                    if (interrupted)
+                    {
+                        SupportClass.ThreadClass.CurrentThread().Interrupt();
+                    }
+                }
+            }
+
+            /**
+             * Get the timer value in milliseconds.
+             */
+            public long getMilliseconds()
+            {
+                return time;
+            }
+        }
+
+        /**
+         * Thrown when elapsed search time exceeds allowed search time. 
+         */
+        public class TimeExceededException : System.Exception
+        {
+            private long timeAllowed;
+            private long timeElapsed;
+            private int lastDocCollected;
+            public TimeExceededException(long timeAllowed, long timeElapsed, int lastDocCollected)
+                : base("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 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 static readonly TimerThread TIMER_THREAD = new TimerThread();
+
+        static TimeLimitedCollector()
+        {
+            TIMER_THREAD.Start();
+        }
+
+        private readonly long t0;
+        private readonly long timeout;
+        private readonly HitCollector hc;
+
+        /**
+         * Create a TimeLimitedCollector wrapper over another HitCollector with a specified timeout.
+         * @param hc the wrapped HitCollector
+         * @param timeAllowed max time allowed for collecting hits after which {@link TimeExceededException} is thrown
+         */
+        public TimeLimitedCollector(HitCollector hc, long timeAllowed)
+        {
+            this.hc = hc;
+            t0 = TIMER_THREAD.getMilliseconds();
+            this.timeout = t0 + timeAllowed;
+        }
+
+        /**
+         * Calls collect() on the decorated HitCollector.
+         * 
+         * @throws TimeExceededException if the time allowed has been exceeded.
+         */
+        override public void Collect(int doc, float score)
+        {
+            long time = TIMER_THREAD.getMilliseconds();
+            if (timeout < time)
+            {
+                if (greedy)
+                {
+                    //System.out.println(this+"  greedy: before failing, collecting doc: "+doc+"  "+(time-t0));
+                    hc.Collect(doc, score);
+                }
+                //System.out.println(this+"  failing on:  "+doc+"  "+(time-t0));
+                throw new TimeExceededException(timeout - t0, time - t0, doc);
+            }
+            //System.out.println(this+"  collecting: "+doc+"  "+(time-t0));
+            hc.Collect(doc, score);
+        }
+
+        /** 
+         * 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(uint newResolution)
+        {
+            resolution = System.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(bool)
+         */
+        public bool 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(bool greedy)
+        {
+            this.greedy = greedy;
+        }
+    }
+}
\ No newline at end of file

Modified: incubator/lucene.net/trunk/C#/src/Lucene.Net/SupportClass.cs
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Lucene.Net/SupportClass.cs?rev=800871&r1=800870&r2=800871&view=diff
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Lucene.Net/SupportClass.cs (original)
+++ incubator/lucene.net/trunk/C#/src/Lucene.Net/SupportClass.cs Tue Aug  4 16:47:00 2009
@@ -131,6 +131,11 @@
         {
             private System.Collections.BitArray bitArray = null;
 
+            public BitSet()
+                : this(0)
+            {
+            }
+
             public BitSet(int size)
             {
                 bitArray = new System.Collections.BitArray(size, false);
@@ -143,6 +148,15 @@
                 bitArray.Set(index, true);
             }
 
+            public int Cardinality()
+            {
+                int cardinality = 0;
+                for (int i = 0; i < bitArray.Length; i++)
+                    if (bitArray.Get(i))
+                        cardinality++;
+                return cardinality;
+            }
+
             /// <summary>
             /// Returns the next set bit at or index, or -1 if no such bit exists.
             /// </summary>
@@ -409,6 +423,11 @@
             }
         }
 
+        public void SetDaemon(bool isDaemon)
+        {
+            threadField.IsBackground = isDaemon;
+        }
+
         /// <summary>
         /// Gets or sets a value indicating the scheduling priority of a thread
         /// </summary>
@@ -548,6 +567,20 @@
         [ThreadStatic]
         static ThreadClass This = null;
 
+        // named as the Java version
+        public static ThreadClass CurrentThread()
+        {
+            return Current();
+        }
+
+        public static void Sleep(long ms)
+        {
+            // casting long ms to int ms could lose resolution, however unlikely
+            // that someone would want to sleep for that long...
+            // TimeSpan takes a 'ticks' argument, where 1 tick = 100 nanoseconds
+            System.Threading.Thread.Sleep(new TimeSpan(10 * ms));
+        }
+
         /// <summary>
         /// Gets the currently running thread
         /// </summary>

Added: incubator/lucene.net/trunk/C#/src/Test/Search/TestTimeLimitedCollector.cs
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Test/Search/TestTimeLimitedCollector.cs?rev=800871&view=auto
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Test/Search/TestTimeLimitedCollector.cs (added)
+++ incubator/lucene.net/trunk/C#/src/Test/Search/TestTimeLimitedCollector.cs Tue Aug  4 16:47:00 2009
@@ -0,0 +1,424 @@
+/**
+ * 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.
+ */
+
+using WhitespaceAnalyzer = Lucene.Net.Analysis.WhitespaceAnalyzer;
+using Document = Lucene.Net.Documents.Document;
+using Field = Lucene.Net.Documents.Field;
+using IndexWriter = Lucene.Net.Index.IndexWriter;
+using MaxFieldLength = Lucene.Net.Index.IndexWriter.MaxFieldLength;
+using QueryParser = Lucene.Net.QueryParsers.QueryParser;
+using Directory = Lucene.Net.Store.Directory;
+using RAMDirectory = Lucene.Net.Store.RAMDirectory;
+using LuceneTestCase = Lucene.Net.Util.LuceneTestCase;
+
+using BitSet = SupportClass.CollectionsSupport.BitSet;
+using Thread = SupportClass.ThreadClass;
+
+using Exception = System.Exception;
+using InterruptedException = System.Threading.ThreadInterruptedException;
+using IOException = System.IO.IOException;
+using String = System.String;
+
+using NUnit.Framework;
+
+namespace Lucene.Net.Search
+{
+
+
+    /**
+     * 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.
+     */
+    [TestFixture()]
+    public class TestTimeLimitedCollector : LuceneTestCase
+    {
+        private static readonly int SLOW_DOWN = 47;
+        private static readonly long TIME_ALLOWED = 17 * SLOW_DOWN; // so searches can find about 17 docs.
+
+        // max time allowed is relaxed for multithreading tests. 
+        // the multithread case fails when setting this to 1 (no slack) and launching many threads (>2000).  
+        // but this is not a real failure, just noise.
+        private static readonly long MULTI_THREAD_SLACK = 7;
+
+        private static readonly int N_DOCS = 3000;
+        private static readonly int N_THREADS = 50;
+
+        private Searcher searcher;
+        private readonly String FIELD_NAME = "body";
+        private Query query;
+
+        public TestTimeLimitedCollector()
+        {
+        }
+
+        /**
+         * initializes searcher with a document set
+         */
+        [TestFixtureSetUp()]
+        protected void setUp()
+        {
+            String[] docText = {
+                "docThatNeverMatchesSoWeCanRequireLastDocCollectedToBeGreaterThanZero",
+                "one blah three",
+                "one foo three multiOne",
+                "one foobar three multiThree",
+                "blueberry pancakes",
+                "blueberry pie",
+                "blueberry strudel",
+                "blueberry pizza",
+            };
+            Directory directory = new RAMDirectory();
+            IndexWriter iw = new IndexWriter(directory, new WhitespaceAnalyzer(), true, MaxFieldLength.UNLIMITED);
+
+            for (int i = 0; i < N_DOCS; i++)
+            {
+                add(docText[i % docText.Length], iw);
+            }
+            iw.Close();
+            searcher = new IndexSearcher(directory);
+
+            String qtxt = "one";
+            for (int i = 0; i < docText.Length; i++)
+            {
+                qtxt += ' ' + docText[i]; // large query so that search will be longer
+            }
+            QueryParser queryParser = new QueryParser(FIELD_NAME, new WhitespaceAnalyzer());
+            query = queryParser.Parse(qtxt);
+
+            // warm the searcher
+            searcher.Search(query, null, 1000);
+        }
+
+        [TestFixtureTearDown()]
+        public void tearDown()
+        {
+            searcher.Close();
+        }
+
+        private void add(String value, IndexWriter iw)
+        {
+            Document d = new Document();
+            d.Add(new Field(FIELD_NAME, value, Field.Store.NO, Field.Index.ANALYZED));
+            iw.AddDocument(d);
+        }
+
+        private void search(HitCollector collector)
+        {
+            searcher.Search(query, collector);
+        }
+
+        /**
+         * test search correctness with no timeout
+         */
+        [Test]
+        public void testSearch()
+        {
+            doTestSearch();
+        }
+
+        private void doTestSearch()
+        {
+            int totalResults = 0;
+            int totalTLCResults = 0;
+            try
+            {
+                MyHitCollector myHc = new MyHitCollector();
+                search(myHc);
+                totalResults = myHc.hitCount();
+
+                myHc = new MyHitCollector();
+                long oneHour = 3600000;
+                HitCollector tlCollector = createTimedCollector(myHc, oneHour, false);
+                search(tlCollector);
+                totalTLCResults = myHc.hitCount();
+            }
+            catch (Exception e)
+            {
+                Assert.IsTrue(false, "Unexpected exception: " + e); //==fail
+            }
+            Assert.AreEqual(totalResults, totalTLCResults, "Wrong number of results!");
+        }
+
+        private HitCollector createTimedCollector(MyHitCollector hc, long timeAllowed, bool greedy)
+        {
+            TimeLimitedCollector res = new TimeLimitedCollector(hc, timeAllowed);
+            res.setGreedy(greedy); // set to true to make sure at least one doc is collected.
+            return res;
+        }
+
+        /**
+         * Test that timeout is obtained, and soon enough!
+         */
+        [Test]
+        public void testTimeoutGreedy()
+        {
+            doTestTimeout(false, true);
+        }
+
+        /**
+         * Test that timeout is obtained, and soon enough!
+         */
+        [Test]
+        public void testTimeoutNotGreedy()
+        {
+            doTestTimeout(false, false);
+        }
+
+        private void doTestTimeout(bool multiThreaded, bool greedy)
+        {
+            // setup
+            MyHitCollector myHc = new MyHitCollector();
+            myHc.setSlowDown(SLOW_DOWN);
+            HitCollector tlCollector = createTimedCollector(myHc, TIME_ALLOWED, greedy);
+
+            // search
+            TimeLimitedCollector.TimeExceededException timoutException = null;
+            try
+            {
+                search(tlCollector);
+            }
+            catch (TimeLimitedCollector.TimeExceededException x)
+            {
+                timoutException = x;
+            }
+            catch (Exception e)
+            {
+                Assert.IsTrue(false, "Unexpected exception: " + e); //==fail
+            }
+
+            // must get exception
+            Assert.IsNotNull(timoutException, "Timeout expected!");
+
+            // greediness affect last doc collected
+            int exceptionDoc = timoutException.getLastDocCollected();
+            int lastCollected = myHc.getLastDocCollected();
+            Assert.IsTrue(exceptionDoc > 0, "doc collected at timeout must be > 0!");
+            if (greedy)
+            {
+                Assert.IsTrue(exceptionDoc == lastCollected, "greedy=" + greedy + " exceptionDoc=" + exceptionDoc + " != lastCollected=" + lastCollected);
+                Assert.IsTrue(myHc.hitCount() > 0, "greedy, but no hits found!");
+            }
+            else
+            {
+                Assert.IsTrue(exceptionDoc > lastCollected, "greedy=" + greedy + " exceptionDoc=" + exceptionDoc + " not > lastCollected=" + lastCollected);
+            }
+
+            // verify that elapsed time at exception is within valid limits
+            Assert.AreEqual(timoutException.getTimeAllowed(), TIME_ALLOWED);
+            // a) Not too early
+            Assert.IsTrue(timoutException.getTimeElapsed() > TIME_ALLOWED - TimeLimitedCollector.getResolution(),
+                "elapsed=" + timoutException.getTimeElapsed() + " <= (allowed-resolution)=" + (TIME_ALLOWED - TimeLimitedCollector.getResolution())
+                );
+            // b) Not too late.
+            //    This part is problematic in a busy test system, so we just print a warning.
+            //    We already verified that a timeout occurred, we just can't be picky about how long it took.
+            if (timoutException.getTimeElapsed() > maxTime(multiThreaded))
+            {
+                System.Console.Out.WriteLine("Informative: timeout exceeded (no action required: most probably just " +
+                  " because the test machine is slower than usual):  " +
+                  "lastDoc=" + exceptionDoc +
+                  " ,&& allowed=" + timoutException.getTimeAllowed() +
+                  " ,&& elapsed=" + timoutException.getTimeElapsed() +
+                  " >= " + maxTimeStr(multiThreaded));
+            }
+        }
+
+        private long maxTime(bool multiThreaded)
+        {
+            long res = 2 * TimeLimitedCollector.getResolution() + TIME_ALLOWED + SLOW_DOWN; // some slack for less noise in this test
+            if (multiThreaded)
+            {
+                res *= MULTI_THREAD_SLACK; // larger slack  
+            }
+            return res;
+        }
+
+        private String maxTimeStr(bool multiThreaded)
+        {
+            String s =
+              "( " +
+              "2*resolution +  TIME_ALLOWED + SLOW_DOWN = " +
+              "2*" + TimeLimitedCollector.getResolution() + " + " + TIME_ALLOWED + " + " + SLOW_DOWN +
+              ")";
+            if (multiThreaded)
+            {
+                s = MULTI_THREAD_SLACK + " * " + s;
+            }
+            return maxTime(multiThreaded) + " = " + s;
+        }
+
+        /**
+         * Test timeout behavior when resolution is modified. 
+         */
+        [Test]
+        public void testModifyResolution()
+        {
+            try
+            {
+                // increase and test
+                uint resolution = 20 * TimeLimitedCollector.DEFAULT_RESOLUTION; //400
+                //TimeLimitedCollector.setResolution(resolution);
+                //Assert.AreEqual(resolution, TimeLimitedCollector.getResolution());
+                doTestTimeout(false, true);
+                // decrease much and test
+                resolution = 5;
+                //TimeLimitedCollector.setResolution(resolution);
+                //Assert.AreEqual(resolution, TimeLimitedCollector.getResolution());
+                doTestTimeout(false, true);
+                // return to default and test
+                resolution = TimeLimitedCollector.DEFAULT_RESOLUTION;
+                //TimeLimitedCollector.setResolution(resolution);
+                //Assert.AreEqual(resolution, TimeLimitedCollector.getResolution());
+                doTestTimeout(false, true);
+            }
+            finally
+            {
+                TimeLimitedCollector.setResolution(TimeLimitedCollector.DEFAULT_RESOLUTION);
+            }
+        }
+
+        /** 
+         * Test correctness with multiple searching threads.
+         */
+        [Test]
+        public void testSearchMultiThreaded()
+        {
+            doTestMultiThreads(false);
+        }
+
+        /** 
+         * Test correctness with multiple searching threads.
+         */
+        [Test]
+        public void testTimeoutMultiThreaded()
+        {
+            doTestMultiThreads(true);
+        }
+
+        internal class AnonymousClassThread : Thread
+        {
+            private TestTimeLimitedCollector enclosingInstance;
+            private BitSet success;
+            private bool withTimeout;
+            private int num;
+
+            internal AnonymousClassThread(TestTimeLimitedCollector enclosingInstance, BitSet success, bool withTimeout, int num)
+                : base()
+            {
+                this.enclosingInstance = enclosingInstance;
+                this.success = success;
+                this.withTimeout = withTimeout;
+                this.num = num;
+            }
+
+            override public void Run()
+            {
+                if (withTimeout)
+                {
+                    enclosingInstance.doTestTimeout(true, true);
+                }
+                else
+                {
+                    enclosingInstance.doTestSearch();
+                }
+                lock (success)
+                {
+                    success.Set(num);
+                }
+            }
+        }
+
+        private void doTestMultiThreads(bool withTimeout)
+        {
+            Thread[] threadArray = new Thread[N_THREADS];
+            BitSet success = new BitSet(N_THREADS);
+            for (int i = 0; i < threadArray.Length; ++i)
+            {
+                int num = i;
+                threadArray[num] = new AnonymousClassThread(this, success, withTimeout, num);
+            }
+            for (int i = 0; i < threadArray.Length; ++i)
+            {
+                threadArray[i].Start();
+            }
+            bool interrupted = false;
+            for (int i = 0; i < threadArray.Length; ++i)
+            {
+                try
+                {
+                    threadArray[i].Join();
+                }
+                catch (InterruptedException)
+                {
+                    interrupted = true;
+                }
+            }
+            if (interrupted)
+            {
+                Thread.CurrentThread().Interrupt();
+            }
+            Assert.AreEqual(N_THREADS, success.Cardinality(), "some threads failed!");
+        }
+
+        // counting hit collector that can slow down at collect().
+        private class MyHitCollector : HitCollector
+        {
+            private readonly BitSet bits = new BitSet();
+            private int slowdown = 0;
+            private int lastDocCollected = -1;
+
+            /**
+             * amount of time to wait on each collect to simulate a long iteration
+             */
+            public void setSlowDown(int milliseconds)
+            {
+                slowdown = milliseconds;
+            }
+
+            override public void Collect(int doc, float score)
+            {
+                if (slowdown > 0)
+                {
+                    try
+                    {
+                        Thread.Sleep(slowdown);
+                    }
+                    catch (InterruptedException x)
+                    {
+                        System.Console.Out.WriteLine("caught " + x);
+                    }
+                }
+                bits.Set(doc);
+                lastDocCollected = doc;
+            }
+
+            public int hitCount()
+            {
+                return bits.Cardinality();
+            }
+
+            public int getLastDocCollected()
+            {
+                return lastDocCollected;
+            }
+
+        }
+
+    }
+
+}

Modified: incubator/lucene.net/trunk/C#/src/Test/Test-VS2005.csproj
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Test/Test-VS2005.csproj?rev=800871&r1=800870&r2=800871&view=diff
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Test/Test-VS2005.csproj (original)
+++ incubator/lucene.net/trunk/C#/src/Test/Test-VS2005.csproj Tue Aug  4 16:47:00 2009
@@ -366,6 +366,7 @@
     <Compile Include="Search\TestTermScorer.cs" />
     <Compile Include="Search\TestTermVectors.cs" />
     <Compile Include="Search\TestThreadSafe.cs" />
+    <Compile Include="Search\TestTimeLimitedCollector.cs" />
     <Compile Include="Search\TestWildcard.cs" />
     <Compile Include="StoreTest.cs">
       <SubType>Code</SubType>

Modified: incubator/lucene.net/trunk/C#/src/Test/Test.csproj
URL: http://svn.apache.org/viewvc/incubator/lucene.net/trunk/C%23/src/Test/Test.csproj?rev=800871&r1=800870&r2=800871&view=diff
==============================================================================
--- incubator/lucene.net/trunk/C#/src/Test/Test.csproj (original)
+++ incubator/lucene.net/trunk/C#/src/Test/Test.csproj Tue Aug  4 16:47:00 2009
@@ -366,6 +366,7 @@
     <Compile Include="Search\TestTermScorer.cs" />
     <Compile Include="Search\TestTermVectors.cs" />
     <Compile Include="Search\TestThreadSafe.cs" />
+    <Compile Include="Search\TestTimeLimitedCollector.cs" />
     <Compile Include="Search\TestWildcard.cs" />
     <Compile Include="StoreTest.cs">
       <SubType>Code</SubType>
@@ -432,6 +433,7 @@
     <Compile Include="Util\TestStringHelper.cs">
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="TestSupportClass.cs" />
     <Compile Include="Util\_TestUtil.cs">
       <SubType>Code</SubType>
     </Compile>