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;
21 import java.util.BitSet;
23 import org.apache.lucene.analysis.MockAnalyzer;
24 import org.apache.lucene.document.Document;
25 import org.apache.lucene.document.Field;
26 import org.apache.lucene.index.IndexReader;
27 import org.apache.lucene.index.RandomIndexWriter;
28 import org.apache.lucene.queryParser.QueryParser;
29 import org.apache.lucene.search.TimeLimitingCollector.TimeExceededException;
30 import org.apache.lucene.search.TimeLimitingCollector.TimerThread;
31 import org.apache.lucene.store.Directory;
32 import org.apache.lucene.util.Counter;
33 import org.apache.lucene.util.LuceneTestCase;
34 import org.apache.lucene.util.ThreadInterruptedException;
37 * Tests the {@link TimeLimitingCollector}. This test checks (1) search
38 * correctness (regardless of timeout), (2) expected timeout behavior,
39 * and (3) a sanity test with multiple searching threads.
41 public class TestTimeLimitingCollector extends LuceneTestCase {
42 private static final int SLOW_DOWN = 3;
43 private static final long TIME_ALLOWED = 17 * SLOW_DOWN; // so searches can find about 17 docs.
45 // max time allowed is relaxed for multithreading tests.
46 // the multithread case fails when setting this to 1 (no slack) and launching many threads (>2000).
47 // but this is not a real failure, just noise.
48 private static final double MULTI_THREAD_SLACK = 7;
50 private static final int N_DOCS = 3000;
51 private static final int N_THREADS = 50;
53 private Searcher searcher;
54 private Directory directory;
55 private IndexReader reader;
57 private final String FIELD_NAME = "body";
59 private Counter counter;
60 private TimerThread counterThread;
63 * initializes searcher with a document set
66 public void setUp() throws Exception {
68 counter = Counter.newCounter(true);
69 counterThread = new TimerThread(counter);
70 counterThread.start();
71 final String docText[] = {
72 "docThatNeverMatchesSoWeCanRequireLastDocCollectedToBeGreaterThanZero",
74 "one foo three multiOne",
75 "one foobar three multiThree",
81 directory = newDirectory();
82 RandomIndexWriter iw = new RandomIndexWriter(random, directory, newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer(random)).setMergePolicy(newLogMergePolicy()));
84 for (int i=0; i<N_DOCS; i++) {
85 add(docText[i%docText.length], iw);
87 reader = iw.getReader();
89 searcher = newSearcher(reader);
92 // start from 1, so that the 0th doc never matches
93 for (int i = 1; i < docText.length; i++) {
94 qtxt += ' ' + docText[i]; // large query so that search will be longer
96 QueryParser queryParser = new QueryParser(TEST_VERSION_CURRENT, FIELD_NAME, new MockAnalyzer(random));
97 query = queryParser.parse(qtxt);
100 searcher.search(query, null, 1000);
104 public void tearDown() throws Exception {
108 counterThread.stopTimer();
109 counterThread.join();
113 private void add(String value, RandomIndexWriter iw) throws IOException {
114 Document d = new Document();
115 d.add(newField(FIELD_NAME, value, Field.Store.NO, Field.Index.ANALYZED));
119 private void search(Collector collector) throws Exception {
120 searcher.search(query, collector);
124 * test search correctness with no timeout
126 public void testSearch() {
130 private void doTestSearch() {
131 int totalResults = 0;
132 int totalTLCResults = 0;
134 MyHitCollector myHc = new MyHitCollector();
136 totalResults = myHc.hitCount();
138 myHc = new MyHitCollector();
139 long oneHour = 3600000;
140 Collector tlCollector = createTimedCollector(myHc, oneHour, false);
142 totalTLCResults = myHc.hitCount();
143 } catch (Exception e) {
145 assertTrue("Unexpected exception: "+e, false); //==fail
147 assertEquals( "Wrong number of results!", totalResults, totalTLCResults );
150 private Collector createTimedCollector(MyHitCollector hc, long timeAllowed, boolean greedy) {
151 TimeLimitingCollector res = new TimeLimitingCollector(hc, counter, timeAllowed);
152 res.setGreedy(greedy); // set to true to make sure at least one doc is collected.
157 * Test that timeout is obtained, and soon enough!
159 public void testTimeoutGreedy() {
160 doTestTimeout(false, true);
164 * Test that timeout is obtained, and soon enough!
166 public void testTimeoutNotGreedy() {
167 doTestTimeout(false, false);
170 private void doTestTimeout(boolean multiThreaded, boolean greedy) {
172 MyHitCollector myHc = new MyHitCollector();
173 myHc.setSlowDown(SLOW_DOWN);
174 Collector tlCollector = createTimedCollector(myHc, TIME_ALLOWED, greedy);
177 TimeExceededException timoutException = null;
180 } catch (TimeExceededException x) {
182 } catch (Exception e) {
183 assertTrue("Unexpected exception: "+e, false); //==fail
186 // must get exception
187 assertNotNull( "Timeout expected!", timoutException );
189 // greediness affect last doc collected
190 int exceptionDoc = timoutException.getLastDocCollected();
191 int lastCollected = myHc.getLastDocCollected();
192 assertTrue( "doc collected at timeout must be > 0!", exceptionDoc > 0 );
194 assertTrue("greedy="+greedy+" exceptionDoc="+exceptionDoc+" != lastCollected="+lastCollected, exceptionDoc==lastCollected);
195 assertTrue("greedy, but no hits found!", myHc.hitCount() > 0 );
197 assertTrue("greedy="+greedy+" exceptionDoc="+exceptionDoc+" not > lastCollected="+lastCollected, exceptionDoc>lastCollected);
200 // verify that elapsed time at exception is within valid limits
201 assertEquals( timoutException.getTimeAllowed(), TIME_ALLOWED);
203 assertTrue ( "elapsed="+timoutException.getTimeElapsed()+" <= (allowed-resolution)="+(TIME_ALLOWED-counterThread.getResolution()),
204 timoutException.getTimeElapsed() > TIME_ALLOWED-counterThread.getResolution());
206 // This part is problematic in a busy test system, so we just print a warning.
207 // We already verified that a timeout occurred, we just can't be picky about how long it took.
208 if (timoutException.getTimeElapsed() > maxTime(multiThreaded)) {
209 System.out.println("Informative: timeout exceeded (no action required: most probably just " +
210 " because the test machine is slower than usual): " +
211 "lastDoc="+exceptionDoc+
212 " ,&& allowed="+timoutException.getTimeAllowed() +
213 " ,&& elapsed="+timoutException.getTimeElapsed() +
214 " >= " + maxTimeStr(multiThreaded));
218 private long maxTime(boolean multiThreaded) {
219 long res = 2 * counterThread.getResolution() + TIME_ALLOWED + SLOW_DOWN; // some slack for less noise in this test
221 res *= MULTI_THREAD_SLACK; // larger slack
226 private String maxTimeStr(boolean multiThreaded) {
229 "2*resolution + TIME_ALLOWED + SLOW_DOWN = " +
230 "2*" + counterThread.getResolution() + " + " + TIME_ALLOWED + " + " + SLOW_DOWN +
233 s = MULTI_THREAD_SLACK + " * "+s;
235 return maxTime(multiThreaded) + " = " + s;
239 * Test timeout behavior when resolution is modified.
241 public void testModifyResolution() {
244 long resolution = 20 * TimerThread.DEFAULT_RESOLUTION; //400
245 counterThread.setResolution(resolution);
246 assertEquals(resolution, counterThread.getResolution());
247 doTestTimeout(false,true);
248 // decrease much and test
250 counterThread.setResolution(resolution);
251 assertEquals(resolution, counterThread.getResolution());
252 doTestTimeout(false,true);
253 // return to default and test
254 resolution = TimerThread.DEFAULT_RESOLUTION;
255 counterThread.setResolution(resolution);
256 assertEquals(resolution, counterThread.getResolution());
257 doTestTimeout(false,true);
259 counterThread.setResolution(TimerThread.DEFAULT_RESOLUTION);
264 * Test correctness with multiple searching threads.
266 public void testSearchMultiThreaded() throws Exception {
267 doTestMultiThreads(false);
271 * Test correctness with multiple searching threads.
273 public void testTimeoutMultiThreaded() throws Exception {
274 doTestMultiThreads(true);
277 private void doTestMultiThreads(final boolean withTimeout) throws Exception {
278 Thread [] threadArray = new Thread[N_THREADS];
279 final BitSet success = new BitSet(N_THREADS);
280 for( int i = 0; i < threadArray.length; ++i ) {
282 threadArray[num] = new Thread() {
286 doTestTimeout(true,true);
290 synchronized(success) {
296 for( int i = 0; i < threadArray.length; ++i ) {
297 threadArray[i].start();
299 for( int i = 0; i < threadArray.length; ++i ) {
300 threadArray[i].join();
302 assertEquals("some threads failed!", N_THREADS,success.cardinality());
305 // counting collector that can slow down at collect().
306 private class MyHitCollector extends Collector {
307 private final BitSet bits = new BitSet();
308 private int slowdown = 0;
309 private int lastDocCollected = -1;
310 private int docBase = 0;
313 * amount of time to wait on each collect to simulate a long iteration
315 public void setSlowDown( int milliseconds ) {
316 slowdown = milliseconds;
319 public int hitCount() {
320 return bits.cardinality();
323 public int getLastDocCollected() {
324 return lastDocCollected;
328 public void setScorer(Scorer scorer) throws IOException {
329 // scorer is not needed
333 public void collect(final int doc) throws IOException {
334 int docId = doc + docBase;
337 Thread.sleep(slowdown);
338 } catch (InterruptedException ie) {
339 throw new ThreadInterruptedException(ie);
342 assert docId >= 0: " base=" + docBase + " doc=" + doc;
344 lastDocCollected = docId;
348 public void setNextReader(IndexReader reader, int base) {
353 public boolean acceptsDocsOutOfOrder() {