| /* |
| * 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(Scorable 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 ScoreMode scoreMode() { |
| return ScoreMode.COMPLETE; |
| } |
| |
| } |
| |
| } |