/** * Copyright 2008 the original author or authors. * * Licensed 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 net.sf.katta.lib.lucene; import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import static org.fest.assertions.Assertions.*; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import junit.framework.Assert; import net.sf.katta.AbstractTest; import net.sf.katta.lib.lucene.LuceneServer.SearcherHandle; import net.sf.katta.testutil.TestResources; import net.sf.katta.testutil.mockito.ChainedAnswer; import net.sf.katta.testutil.mockito.PauseAnswer; import net.sf.katta.testutil.mockito.SleepingAnswer; import org.apache.lucene.index.Term; import org.apache.lucene.search.Collector; import org.apache.lucene.search.Filter; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.Weight; import org.apache.lucene.store.AlreadyClosedException; import org.junit.Test; import org.mockito.internal.stubbing.answers.CallsRealMethods; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; public class LuceneServerTest extends AbstractTest { @Test public void testConfiguration() throws Exception { // no property in configuration LuceneServer server = new LuceneServer(); server.init("server", newNodeConfiguration()); assertEquals("server", server.getNodeName()); assertEquals(0.75f, server.getTimeoutPercentage(), 0.5); server.shutdown(); // property in configuration server = new LuceneServer(); server.init("server", newNodeConfiguration(LuceneServer.CONF_KEY_COLLECTOR_TIMOUT_PERCENTAGE, "0.5")); assertEquals("server", server.getNodeName()); assertEquals(0.5f, server.getTimeoutPercentage(), 0.5); } @Test public void testSearch_Timeout() throws Exception { int clientTiemout = 10000; // disabled timeout LuceneServer server = new LuceneServer("server", new DefaultSearcherFactory(), 0.0f); String[] shardNames = addIndexShards(server, TestResources.INDEX1); QueryWritable queryWritable = new QueryWritable(parseQuery("foo: b*")); DocumentFrequencyWritable freqs = server.getDocFreqs(queryWritable, shardNames); HitsMapWritable result = server.search(queryWritable, freqs, shardNames, clientTiemout, 1000); assertEquals(4, result.getHitList().size()); server.shutdown(); // timeout - success server = new LuceneServer("server", new DefaultSearcherFactory(), 0.5f); addIndexShards(server, TestResources.INDEX1); freqs = server.getDocFreqs(queryWritable, shardNames); result = server.search(queryWritable, freqs, shardNames, clientTiemout, 1000); assertEquals(4, result.getHitList().size()); server.shutdown(); // timeout - failure final long serverTimeout = 100; final DefaultSearcherFactory seacherFactory = new DefaultSearcherFactory(); ISeacherFactory mockSeacherFactory = mock(ISeacherFactory.class); final AtomicInteger shardsWithTiemoutCount = new AtomicInteger(); when(mockSeacherFactory.createSearcher(anyString(), any(File.class))).thenAnswer(new Answer<IndexSearcher>() { @Override public IndexSearcher answer(InvocationOnMock invocation) throws Throwable { final IndexSearcher indexSearcher = seacherFactory.createSearcher((String) invocation.getArguments()[0], (File) invocation.getArguments()[1]); synchronized (shardsWithTiemoutCount) { if (shardsWithTiemoutCount.intValue() >= 2) { // 2 from 4 shards will get tiemout return indexSearcher; } shardsWithTiemoutCount.incrementAndGet(); } IndexSearcher indexSearcherSpy = spy(indexSearcher); doAnswer(new ChainedAnswer(new SleepingAnswer(serverTimeout * 2), new CallsRealMethods())).when( indexSearcherSpy).search(any(Weight.class), any(Filter.class), any(Collector.class)); return indexSearcherSpy; } }); server = new LuceneServer("server", mockSeacherFactory, 0.01f); assertEquals(serverTimeout, server.getCollectorTiemout(clientTiemout)); addIndexShards(server, TestResources.INDEX1); freqs = server.getDocFreqs(queryWritable, shardNames); result = server.search(queryWritable, freqs, shardNames, clientTiemout, 1000); assertTrue(result.getHitList().size() < 4); assertTrue(result.getHitList().size() >= 1); server.shutdown(); } private String[] addIndexShards(LuceneServer server, File index) throws IOException { File[] shards = index.listFiles(); String[] shardNames = index.list(); for (File shard : shards) { server.addShard(shard.getName(), shard); } return shardNames; } @Test public void testSearch_MultiThread() throws Exception { LuceneServer server = new LuceneServer("ls", new DefaultSearcherFactory(), 0.75f); String[] shardNames = addIndexShards(server, TestResources.INDEX1); QueryWritable writable = new QueryWritable(parseQuery("foo: bar")); DocumentFrequencyWritable freqs = server.getDocFreqs(writable, shardNames); ExecutorService es = Executors.newFixedThreadPool(100); List<Future<HitsMapWritable>> tasks = new ArrayList<Future<HitsMapWritable>>(); for (int i = 0; i < 10000; i++) { QueryClient client = new QueryClient(server, freqs, writable, shardNames); Future<HitsMapWritable> future = es.submit(client); tasks.add(future); } HitsMapWritable last = null; for (Future<HitsMapWritable> future : tasks) { HitsMapWritable hitsMapWritable = future.get(); if (last == null) { last = hitsMapWritable; } else { Assert.assertEquals(last.getTotalHits(), hitsMapWritable.getTotalHits()); float lastScore = last.getHitList().get(0).getScore(); float currentScore = hitsMapWritable.getHitList().get(0).getScore(); Assert.assertEquals(lastScore, currentScore); } } } @Test public void testSearchCall_EmptyIndex() throws Exception { IndexSearcher searcher = mock(IndexSearcher.class); when(searcher.maxDoc()).thenReturn(0); Weight weight = mock(Weight.class); LuceneServer server = mock(LuceneServer.class); when(server.getSearcherHandleByShard("testShard")).thenReturn(new SearcherHandle(searcher)); LuceneServer.SearchCall searchCall = server.new SearchCall("testShard", weight, 10, null, 1000, 1, null); LuceneServer.SearchResult result = searchCall.call(); assertThat(result._totalHits).as("results").isEqualTo(0); } @Test public void testSearchDuringUndeploy() throws Exception { final String[] shardToUndeploy = new String[1]; /* * Create a special LuceneServer that pauses on a call to maxDoc(). Here, * maxDoc() is called only from SearchCall within LuceneServer, right before * performing the real search. */ final PauseAnswer<Void> pauseAnswer = new PauseAnswer<Void>(null); final LuceneServer server = new LuceneServer("ls", new DefaultSearcherFactory(), 0.75f) { @Override protected SearcherHandle getSearcherHandleByShard(String shardName) { SearcherHandle handle; if (shardName.equals(shardToUndeploy[0])) { handle = spy(super.getSearcherHandleByShard(shardName)); Answer<IndexSearcher> answer = new Answer<IndexSearcher>() { @Override public IndexSearcher answer(InvocationOnMock invocation) throws Throwable { IndexSearcher searcher = spy((IndexSearcher) invocation.callRealMethod()); doAnswer(new ChainedAnswer(pauseAnswer, new CallsRealMethods())).when(searcher).maxDoc(); return searcher; } }; doAnswer(answer).when(handle).getSearcher(); } else { handle = super.getSearcherHandleByShard(shardName); } return handle; } }; final String[] shardNames = addIndexShards(server, TestResources.INDEX1); shardToUndeploy[0] = shardNames[0]; final QueryWritable writable = new QueryWritable(new TermQuery(new Term("foo", "bar"))); final DocumentFrequencyWritable freqs = server.getDocFreqs(writable, shardNames); // Storage for the result or exception from the search call final HitsMapWritable[] result = new HitsMapWritable[1]; final Exception[] exception = new Exception[1]; // Run the query in another thread Thread t = new Thread() { @Override public void run() { try { result[0] = server.search(writable, freqs, shardNames, 10000); } catch (Exception e) { exception[0] = e; } } }; t.start(); // Wait until the search gets to the SearchCall, where it will remain paused pauseAnswer.joinExecutionBegin(); // Get the searcher so it can be checked that it's really closed LuceneServer.SearcherHandle handle = server.getSearcherHandleByShard(shardNames[0]); IndexSearcher searcher = handle.getSearcher(); // ... decrement the ref count though handle.finishSearcher(); // Undeploy the shard (in another thread) Thread t2 = new Thread() { @Override public void run() { server.removeShard(shardNames[0]); } }; t2.start(); // Resume the SearchCall thread, then wait for the search thread to finish pauseAnswer.resumeExecution(true); t.join(); t2.join(); // Fail the test if there was an exception if (exception[0] != null) { throw exception[0]; } assertThat(result[0].getTotalHits()).as("Results returned from search").isEqualTo(4); try { // Expected: java.lang.IllegalStateException: no index-server for shard // 'aIndex' found - probably undeployed server.getSearcherHandleByShard(shardNames[0]); fail("IllegalStateException not thrown when trying to get undeployed shard handle"); } catch (IllegalStateException e) { } try { // Expected: org.apache.lucene.store.AlreadyClosedException: this // IndexReader is closed searcher.doc(0); fail("AlreadyClosedException not thrown when trying to access closed index"); } catch (AlreadyClosedException e) { } } private static class QueryClient implements Callable<HitsMapWritable> { private LuceneServer _server; private QueryWritable _query; private DocumentFrequencyWritable _freqs; private String[] _shards; public QueryClient(LuceneServer server, DocumentFrequencyWritable freqs, QueryWritable query, String[] shards) { _server = server; _freqs = freqs; _query = query; _shards = shards; } @Override public HitsMapWritable call() throws Exception { return _server.search(_query, _freqs, _shards, 10000, 2); } } }