/* * 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. */ package org.apache.lucene.search; import java.io.IOException; import java.util.BitSet; import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.search.TimeLimitingCollector.TimeExceededException; import org.apache.lucene.search.TimeLimitingCollector.TimerThread; import org.apache.lucene.store.Directory; import org.apache.lucene.util.Counter; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.util.TestUtil; import org.apache.lucene.util.ThreadInterruptedException; /** * Tests the {@link TimeLimitingCollector}. This test checks (1) search * correctness (regardless of timeout), (2) expected timeout behavior, * and (3) a sanity test with multiple searching threads. */ @SuppressSysoutChecks(bugUrl = "http://test.is.timing.sensitive.so.it.prints.instead.of.failing") public class TestTimeLimitingCollector extends LuceneTestCase { private static final int SLOW_DOWN = 3; private static final 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 final double MULTI_THREAD_SLACK = 7; private static final int N_DOCS = 3000; private static final int N_THREADS = 50; private IndexSearcher searcher; private Directory directory; private IndexReader reader; private final String FIELD_NAME = "body"; private Query query; private Counter counter; private TimerThread counterThread; /** * initializes searcher with a document set */ @Override public void setUp() throws Exception { super.setUp(); counter = Counter.newCounter(true); counterThread = new TimerThread(counter); counterThread.start(); final String docText[] = { "docThatNeverMatchesSoWeCanRequireLastDocCollectedToBeGreaterThanZero", "one blah three", "one foo three multiOne", "one foobar three multiThree", "blueberry pancakes", "blueberry pie", "blueberry strudel", "blueberry pizza", }; directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory, newIndexWriterConfig(new MockAnalyzer(random())).setMergePolicy(newLogMergePolicy())); for (int i=0; i<N_DOCS; i++) { add(docText[i%docText.length], iw); } reader = iw.getReader(); iw.close(); searcher = newSearcher(reader); BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder(); booleanQuery.add(new TermQuery(new Term(FIELD_NAME, "one")), BooleanClause.Occur.SHOULD); // start from 1, so that the 0th doc never matches for (int i = 1; i < docText.length; i++) { String[] docTextParts = docText[i].split("\\s+"); for (String docTextPart : docTextParts) { // large query so that search will be longer booleanQuery.add(new TermQuery(new Term(FIELD_NAME, docTextPart)), BooleanClause.Occur.SHOULD); } } query = booleanQuery.build(); // warm the searcher searcher.search(query, 1000); } @Override public void tearDown() throws Exception { reader.close(); directory.close(); counterThread.stopTimer(); counterThread.join(); super.tearDown(); } private void add(String value, RandomIndexWriter iw) throws IOException { Document d = new Document(); d.add(newTextField(FIELD_NAME, value, Field.Store.NO)); iw.addDocument(d); } private void search(Collector collector) throws Exception { searcher.search(query, collector); } /** * test search correctness with no timeout */ 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; long duration = TestUtil.nextLong(random(), oneHour, Long.MAX_VALUE); Collector tlCollector = createTimedCollector(myHc, duration, false); search(tlCollector); totalTLCResults = myHc.hitCount(); } catch (Exception e) { e.printStackTrace(); assertTrue("Unexpected exception: "+e, false); //==fail } assertEquals("Wrong number of results!", totalResults, totalTLCResults); } private Collector createTimedCollector(MyHitCollector hc, long timeAllowed, boolean greedy) { TimeLimitingCollector res = new TimeLimitingCollector(hc, counter, 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! */ public void testTimeoutGreedy() { doTestTimeout(false, true); } /** * Test that timeout is obtained, and soon enough! */ public void testTimeoutNotGreedy() { doTestTimeout(false, false); } private void doTestTimeout(boolean multiThreaded, boolean greedy) { // setup MyHitCollector myHc = new MyHitCollector(); myHc.setSlowDown(SLOW_DOWN); Collector tlCollector = createTimedCollector(myHc, TIME_ALLOWED, greedy); // search: must get exception TimeExceededException timeoutException = expectThrows(TimeExceededException.class, () -> { search(tlCollector); }); // greediness affect last doc collected int exceptionDoc = timeoutException.getLastDocCollected(); int lastCollected = myHc.getLastDocCollected(); // exceptionDoc == -1 means we hit the timeout in getLeafCollector: if (exceptionDoc != -1) { assertTrue( "doc collected at timeout must be > 0! or == -1 but was: " + exceptionDoc, exceptionDoc > 0); if (greedy) { assertTrue("greedy="+greedy+" exceptionDoc="+exceptionDoc+" != lastCollected="+lastCollected, exceptionDoc==lastCollected); assertTrue("greedy, but no hits found!", myHc.hitCount() > 0 ); } else { assertTrue("greedy="+greedy+" exceptionDoc="+exceptionDoc+" not > lastCollected="+lastCollected, exceptionDoc>lastCollected); } } // verify that elapsed time at exception is within valid limits assertEquals( timeoutException.getTimeAllowed(), TIME_ALLOWED); // a) Not too early assertTrue ( "elapsed="+timeoutException.getTimeElapsed()+" <= (allowed-resolution)="+(TIME_ALLOWED-counterThread.getResolution()), timeoutException.getTimeElapsed() > TIME_ALLOWED-counterThread.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 (timeoutException.getTimeElapsed() > maxTime(multiThreaded)) { System.out.println("Informative: timeout exceeded (no action required: most probably just " + " because the test machine is slower than usual): " + "lastDoc="+exceptionDoc+ " ,&& allowed="+timeoutException.getTimeAllowed() + " ,&& elapsed="+timeoutException.getTimeElapsed() + " >= " + maxTimeStr(multiThreaded)); } } private long maxTime(boolean multiThreaded) { long res = 2 * counterThread.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(boolean multiThreaded) { String s = "( " + "2*resolution + TIME_ALLOWED + SLOW_DOWN = " + "2*" + counterThread.getResolution() + " + " + TIME_ALLOWED + " + " + SLOW_DOWN + ")"; if (multiThreaded) { s = MULTI_THREAD_SLACK + " * "+s; } return maxTime(multiThreaded) + " = " + s; } /** * Test timeout behavior when resolution is modified. */ public void testModifyResolution() { try { // increase and test long resolution = 20 * TimerThread.DEFAULT_RESOLUTION; //400 counterThread.setResolution(resolution); assertEquals(resolution, counterThread.getResolution()); doTestTimeout(false,true); // decrease much and test resolution = 5; counterThread.setResolution(resolution); assertEquals(resolution, counterThread.getResolution()); doTestTimeout(false,true); // return to default and test resolution = TimerThread.DEFAULT_RESOLUTION; counterThread.setResolution(resolution); assertEquals(resolution, counterThread.getResolution()); doTestTimeout(false,true); } finally { counterThread.setResolution(TimerThread.DEFAULT_RESOLUTION); } } public void testNoHits() throws IOException { MyHitCollector myHc = new MyHitCollector(); Collector collector = createTimedCollector(myHc, -1, random().nextBoolean()); // search: must get exception expectThrows(TimeExceededException.class, () -> { BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder(); // won't match - we only test if we check timeout when collectors are pulled booleanQuery.add(new TermQuery(new Term(FIELD_NAME, "one")), BooleanClause.Occur.MUST); booleanQuery.add(new TermQuery(new Term(FIELD_NAME, "blueberry")), BooleanClause.Occur.MUST); searcher.search(booleanQuery.build(), collector); }); assertEquals(-1, myHc.getLastDocCollected()); } /** * Test correctness with multiple searching threads. */ public void testSearchMultiThreaded() throws Exception { doTestMultiThreads(false); } /** * Test correctness with multiple searching threads. */ public void testTimeoutMultiThreaded() throws Exception { doTestMultiThreads(true); } private void doTestMultiThreads(final boolean withTimeout) throws Exception { Thread [] threadArray = new Thread[N_THREADS]; final BitSet success = new BitSet(N_THREADS); for( int i = 0; i < threadArray.length; ++i ) { final int num = i; threadArray[num] = new Thread() { @Override public void run() { if (withTimeout) { doTestTimeout(true,true); } else { doTestSearch(); } synchronized(success) { success.set(num); } } }; } for( int i = 0; i < threadArray.length; ++i ) { threadArray[i].start(); } for( int i = 0; i < threadArray.length; ++i ) { threadArray[i].join(); } assertEquals("some threads failed!", N_THREADS,success.cardinality()); } // counting collector that can slow down at collect(). private static class MyHitCollector extends SimpleCollector { private final BitSet bits = new BitSet(); private int slowdown = 0; private int lastDocCollected = -1; private int docBase = 0; /** * amount of time to wait on each collect to simulate a long iteration */ public void setSlowDown( int milliseconds ) { slowdown = milliseconds; } public int hitCount() { return bits.cardinality(); } public int getLastDocCollected() { return lastDocCollected; } @Override public void setScorer(Scorer scorer) throws IOException { // scorer is not needed } @Override public void collect(final int doc) throws IOException { int docId = doc + docBase; if( slowdown > 0 ) { try { Thread.sleep(slowdown); } catch (InterruptedException ie) { throw new ThreadInterruptedException(ie); } } assert docId >= 0: " base=" + docBase + " doc=" + doc; bits.set( docId ); lastDocCollected = docId; } @Override protected void doSetNextReader(LeafReaderContext context) throws IOException { docBase = context.docBase; } @Override public boolean needsScores() { return false; } } }