1   package com.atlassian.bonnie;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.lang.reflect.InvocationTargetException;
6   import java.lang.reflect.Method;
7   import java.lang.reflect.Modifier;
8   import java.util.ArrayList;
9   import java.util.List;
10  import java.util.UUID;
11  import java.util.concurrent.Callable;
12  import java.util.concurrent.CountDownLatch;
13  import java.util.concurrent.ExecutorService;
14  import java.util.concurrent.Executors;
15  import java.util.concurrent.Future;
16  import static java.util.concurrent.TimeUnit.SECONDS;
17  import java.util.concurrent.atomic.AtomicBoolean;
18  import java.util.concurrent.atomic.AtomicReference;
19  
20  import junit.framework.AssertionFailedError;
21  import junit.framework.TestCase;
22  
23  import org.apache.lucene.analysis.standard.StandardAnalyzer;
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.IndexWriter;
28  import org.apache.lucene.index.Term;
29  import org.apache.lucene.search.Hits;
30  import org.apache.lucene.search.IndexSearcher;
31  import org.apache.lucene.search.TermQuery;
32  import org.apache.lucene.store.RAMDirectory;
33  
34  import com.atlassian.bonnie.search.SearcherInitialisation;
35  
36  public class TestLuceneConnection extends TestCase
37  {
38      private static final String FIELD_NAME = "content.space";
39      private static final String FIELD_VALUE = "space";
40      private static final String FIELD_NAME_COUNT = "count";
41      private static final TermQuery QUERY = new TermQuery(new Term(FIELD_NAME, FIELD_VALUE));
42  
43      private ILuceneConnection lcon;
44      private RAMDirectory directory = new RAMDirectory();
45  
46      private static final ILuceneConnection.WriterAction NOOP_WRITER_ACTION = new ILuceneConnection.WriterAction() {
47          public void perform(IndexWriter writer) {}
48      };
49      private static final ILuceneConnection.SearcherAction NOOP_SEARCHER_ACTION = new ILuceneConnection.SearcherAction() {
50          public void perform(IndexSearcher searcher) {}
51      };
52  
53      protected void setUp() throws Exception
54      {
55          super.setUp();
56          lcon = new LuceneConnection(directory, new StandardAnalyzer());
57      }
58  
59      protected void tearDown() throws Exception
60      {
61          super.tearDown();
62      }
63      
64      public void testIndexDirectoryIsUnlockedWhenConstructed() throws IOException
65      {
66          lcon.close(); // close shared connection first
67  
68          // grab lucene write lock without using the connection
69          IndexWriter writer = new IndexWriter(directory, new StandardAnalyzer(), false);
70          writer.addDocument(createDocument());
71          
72          assertTrue("index directory should be locked by write above", IndexReader.isLocked(directory));
73  
74          lcon = new LuceneConnection(directory, new StandardAnalyzer());
75          lcon.withWriter(NOOP_WRITER_ACTION);
76          lcon.close();
77  
78          assertFalse("index directory should have been unlocked by opening and closing the connection", IndexReader.isLocked(directory));
79      }
80  
81      /**
82       * NOTE: this test requires the use of a FSDirectory and multifile index format to test that close is properly being
83       * called. Use of a RAMDirectory or compound index file format cause this test to fail.
84       */
85      public void testWithSearch() throws IOException
86      {
87          final int numDocs = 5;
88          addDocsToIndex(numDocs);
89  
90          lcon.withSearch(new LuceneConnection.SearcherAction()
91          {
92              public void perform(IndexSearcher searcher) throws IOException
93              {
94                  Hits hits = searcher.search(QUERY);
95                  assertEquals(5, hits.length());
96              }
97          });
98      }
99  
100     /**
101      * Test that Bonnie does its weird stuff with the int flags that are passed to withWriter(Action, int). Modify this
102      * test to match the required defaults.
103      *
104      * @throws Exception
105      */
106     public void testDefaultConfiguration() throws Exception
107     {
108         lcon.withWriter(new LuceneConnection.WriterAction()
109         {
110             public void perform(IndexWriter writer)
111             {
112                 assertEquals(4, writer.getMergeFactor());
113                 assertEquals(300, writer.getMaxBufferedDocs());
114                 assertEquals(5000, writer.getMaxMergeDocs());
115                 assertTrue(writer.getUseCompoundFile());
116             }
117         });
118         lcon.withBatchUpdate(new LuceneConnection.BatchUpdateAction()
119         {
120             public void perform() throws Exception
121             {
122                 lcon.withWriter(new LuceneConnection.WriterAction()
123                 {
124                     public void perform(IndexWriter writer)
125                     {
126                         assertEquals(50, writer.getMergeFactor());
127                         assertEquals(300, writer.getMaxBufferedDocs());
128                         assertEquals(Integer.MAX_VALUE, writer.getMaxMergeDocs());
129                         assertTrue(writer.getUseCompoundFile());
130                     }
131                 });
132             }
133         });
134     }
135 
136     /**
137      * Test that Bonnie does its weird stuff with the int flags that are passed to withWriter(Action, int). Modify this
138      * test to match the required defaults.
139      * 
140      * @throws Exception
141      */
142     public void testCustomConfiguration() throws Exception
143     {
144         LuceneConnection.Configuration config = new LuceneConnection.Configuration()
145         {
146 
147             public int getBatchMaxBufferedDocs()
148             {
149                 return 100;
150             }
151 
152             public int getBatchMaxMergeDocs()
153             {
154                 return 200;
155             }
156 
157             public int getBatchMergeFactor()
158             {
159                 return 300;
160             }
161 
162             public int getInteractiveMaxBufferedDocs()
163             {
164                 return 10;
165             }
166 
167             public int getInteractiveMaxMergeDocs()
168             {
169                 return 20;
170             }
171 
172             public int getInteractiveMergeFactor()
173             {
174                 return 30;
175             }
176 
177             public int getMaxFieldLength()
178             {
179                 return 1000000;
180             }
181 
182             public boolean isCompoundIndexFileFormat()
183             {
184                 return false;
185             }
186         };
187         assertLuceneConnectionConfig(new LuceneConnection(new RAMDirectory(), new StandardAnalyzer(), config));
188         assertLuceneConnectionConfig(new LuceneConnection(directory, new StandardAnalyzer(), config));
189     }
190 
191     private void assertLuceneConnectionConfig(final LuceneConnection lcon)
192     {
193         lcon.withWriter(new LuceneConnection.WriterAction()
194         {
195             public void perform(IndexWriter writer)
196             {
197                 assertEquals(30, writer.getMergeFactor());
198                 assertEquals(10, writer.getMaxBufferedDocs());
199                 assertEquals(20, writer.getMaxMergeDocs());
200                 assertFalse(writer.getUseCompoundFile());
201             }
202         });
203 
204         lcon.withBatchUpdate(new LuceneConnection.BatchUpdateAction()
205         {
206             public void perform() throws Exception
207             {
208                 lcon.withWriter(new LuceneConnection.WriterAction()
209                 {
210                     public void perform(IndexWriter writer)
211                     {
212                         assertEquals(300, writer.getMergeFactor());
213                         assertEquals(100, writer.getMaxBufferedDocs());
214                         assertEquals(200, writer.getMaxMergeDocs());
215                         assertFalse(writer.getUseCompoundFile());
216                     }
217                 });
218             }
219         });
220     }
221 
222     public void testSearcherReuse()
223     {
224         final int numDocs = 5;
225         addDocsToIndex(numDocs);
226 
227         final AtomicReference<IndexSearcher> searcherRef = new AtomicReference<IndexSearcher>(null);
228         lcon.withSearch(new LuceneConnection.SearcherAction()
229         {
230             public void perform(IndexSearcher searcher) throws IOException
231             {
232                 searcherRef.set(searcher);
233                 assertEquals(5, searcher.search(QUERY).length());
234             }
235         });
236 
237         lcon.withSearch(new LuceneConnection.SearcherAction()
238         {
239             public void perform(IndexSearcher searcher) throws IOException
240             {
241                 assertEquals(5, searcher.search(QUERY).length());
242                 // should be the same searcher, no updates have been done
243                 assertSame(searcherRef.get(), searcher);
244                 searcherRef.set(searcher);
245             }
246         });
247 
248         lcon.withWriter(new LuceneConnection.WriterAction()
249         {
250             public void perform(IndexWriter writer)
251             {
252             }
253         });
254 
255         lcon.withSearch(new LuceneConnection.SearcherAction()
256         {
257             public void perform(IndexSearcher searcher) throws IOException
258             {
259                 assertEquals(5, searcher.search(QUERY).length());
260                 // should not be the same searcher, updates have been done
261                 assertNotSame(searcherRef.get(), searcher);
262                 searcherRef.set(searcher);
263             }
264         });
265         lcon.withSearch(new LuceneConnection.SearcherAction()
266         {
267             public void perform(IndexSearcher searcher) throws IOException
268             {
269                 assertEquals(5, searcher.search(QUERY).length());
270                 // should be the same searcher, no updates have been done
271                 assertSame(searcherRef.get(), searcher);
272                 searcherRef.set(searcher);
273             }
274         });
275         lcon.withReader(new ILuceneConnection.ReaderAction()
276         {
277             public Object perform(IndexReader reader)
278             {
279                 return null;
280             }
281         });
282         lcon.withSearch(new LuceneConnection.SearcherAction()
283         {
284             public void perform(IndexSearcher searcher) throws IOException
285             {
286                 assertEquals(5, searcher.search(QUERY).length());
287                 // should be the same searcher, only reads have been done
288                 assertSame(searcherRef.get(), searcher);
289             }
290         });
291     }
292 
293     public void testWriterIsInteractiveModeConfiguration() throws Exception
294     {
295         lcon.withWriter(new LuceneConnection.WriterAction()
296         {
297             public void perform(IndexWriter writer)
298             {
299                 // check that we are using the interactive settings
300                 assertEquals("Should be interactive mode maxMergeSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getInteractiveMaxMergeDocs(), writer.getMaxMergeDocs());
301                 assertEquals("Should be interactive mode maxBufferedSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getInteractiveMaxBufferedDocs(), writer.getMaxBufferedDocs());
302                 assertEquals("Should be interactive mode mergeFactorSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getInteractiveMergeFactor(), writer.getMergeFactor());
303                 assertEquals("Should be default maxField", ILuceneConnection.DEFAULT_CONFIGURATION.getMaxFieldLength(), writer.getMaxFieldLength());
304             }
305         });
306     }
307 
308     public void testReaderReuse()
309     {
310         final int numDocs = 5;
311         addDocsToIndex(numDocs);
312 
313         final AtomicReference<IndexReader> readerRef = new AtomicReference<IndexReader>(null);
314         lcon.withReader(new ILuceneConnection.ReaderAction()
315         {
316             public Object perform(IndexReader reader)
317             {
318                 readerRef.set(reader);
319                 assertEquals(5, reader.numDocs());
320                 return null;
321             }
322         });
323 
324         lcon.withReader(new ILuceneConnection.ReaderAction()
325         {
326             public Object perform(IndexReader reader)
327             {
328                 assertEquals(5, reader.numDocs());
329                 // should be the same searcher, no updates have been done
330                 assertSame(readerRef.get(), reader);
331                 readerRef.set(reader);
332                 return null;
333             }
334         });
335 
336         lcon.withWriter(new LuceneConnection.WriterAction()
337         {
338             public void perform(IndexWriter writer)
339             {
340             }
341         });
342 
343         lcon.withReader(new ILuceneConnection.ReaderAction()
344         {
345             public Object perform(IndexReader reader)
346             {
347                 assertEquals(5, reader.numDocs());
348                 // should not be the same searcher, updates have been done
349                 assertNotSame(readerRef.get(), reader);
350                 readerRef.set(reader);
351                 return null;
352             }
353         });
354         lcon.withReader(new ILuceneConnection.ReaderAction()
355         {
356             public Object perform(IndexReader reader)
357             {
358                 assertEquals(5, reader.numDocs());
359                 // should be the same searcher, no updates have been done
360                 assertSame(readerRef.get(), reader);
361                 readerRef.set(reader);
362                 return null;
363             }
364         });
365         lcon.withReader(new ILuceneConnection.ReaderAction()
366         {
367             public Object perform(IndexReader reader)
368             {
369                 assertEquals(5, reader.numDocs());
370                 // should be the same searcher, no updates have been done
371                 assertSame(readerRef.get(), reader);
372                 readerRef.set(reader);
373                 return null;
374             }
375         });
376         lcon.withSearch(new ILuceneConnection.SearcherAction()
377         {
378             public void perform(IndexSearcher searcher)
379             {
380                 assertNotNull(searcher.getIndexReader());
381                 assertSame(readerRef.get(), searcher.getIndexReader());
382             }
383         });
384         lcon.withReader(new ILuceneConnection.ReaderAction()
385         {
386             public Object perform(IndexReader reader)
387             {
388                 assertEquals(5, reader.numDocs());
389                 // should be the same searcher, only reads have been done
390                 assertSame(readerRef.get(), reader);
391                 return null;
392             }
393         });
394     }
395 
396     public void testFileConstructor() throws IOException
397     {
398         final File tempDir = File.createTempFile(this.getClass().getName(), ".idx");
399         try
400         {
401             tempDir.delete();
402             lcon = new LuceneConnection(tempDir, new StandardAnalyzer(), ILuceneConnection.DEFAULT_CONFIGURATION);
403             assertTrue(IndexReader.indexExists(tempDir));
404         }
405         finally
406         {
407             tempDir.delete();
408         }
409     }
410 
411     public void testOptimizeCallsCorrectly() throws IOException
412     {
413         lcon.close();
414 
415         final AtomicBoolean optimizeCalled = new AtomicBoolean(false);
416         final IndexWriter mockWriter = new IndexWriter(directory, null, false)
417         {
418             public synchronized void optimize()
419             {
420                 optimizeCalled.set(true);
421             }
422         };
423 
424         lcon = new LuceneConnection(directory, new StandardAnalyzer(), ILuceneConnection.DEFAULT_CONFIGURATION)
425         {
426             public void withWriter(ILuceneConnection.WriterAction action) throws LuceneException
427             {
428                 try
429                 {
430                     action.perform(mockWriter);
431                 }
432                 catch (IOException e)
433                 {
434                     throw new LuceneException(e);
435                 }
436             }
437         };
438 
439         lcon.optimize();
440 
441         assertTrue(optimizeCalled.get());
442     }
443 
444     public void testWriterBlocksDelete() throws InterruptedException
445     {
446         addDocsToIndex(5);
447 
448         final ExecutorService pool = Executors.newSingleThreadExecutor();
449         final AtomicBoolean innerThreadStarted = new AtomicBoolean(false);
450         final CountDownLatch deleteComplete = new CountDownLatch(1);
451 
452         // test withWrite, then simultaneously, withReadAndDeletes
453         lcon.withWriter(new ILuceneConnection.WriterAction()
454         {
455             public void perform(final IndexWriter writer) throws IOException
456             {
457                 final CountDownLatch latch = new CountDownLatch(1);
458 
459                 // Execute the deleteTask in a different thread
460                 Future<?> deleteTask = pool.submit(new Runnable()
461                 {
462                     public void run()
463                     {
464                         innerThreadStarted.set(true);
465                         // Let the latch know that the deleteTask has began
466                         // running
467                         latch.countDown();
468                         // this should only be run _after_ the WriterAction
469                         // completes
470                         lcon.withWriter(new ILuceneConnection.WriterAction()
471                         {
472                             public void perform(IndexWriter writer) throws IOException
473                             {
474                                 assertEquals("threaded withWriter: Added 2 documents not detected from thread", 7, writer.numDocs());
475                                 writer.deleteDocuments(new Term(FIELD_NAME_COUNT, "0"));
476                             }
477                         });
478                         deleteComplete.countDown();
479                     }
480                 });
481 
482                 // Wait for the deleteTask to begin executing
483                 try
484                 {
485                     latch.await();
486                 }
487                 catch (InterruptedException e)
488                 {
489                     throw new RuntimeException(e);
490                 }
491 
492                 // Assert that the deleteTask has started executing
493                 assertTrue(innerThreadStarted.get());
494                 // But blocked before deleteing a document. The task is blocked
495                 // as we should hold the lock on the index
496                 // until we leave the withWriter() method.
497                 assertFalse(deleteTask.isDone());
498                 // Therefore we should still have 5 documents in the index
499                 assertEquals(5, writer.docCount());
500                 // Add another document
501                 writer.addDocument(createDocument());
502                 writer.addDocument(createDocument());
503 
504                 assertFalse(deleteTask.isDone());
505             }
506         });
507 
508         // wait for the deleteTask to complete and assert it did...
509         deleteComplete.await();
510 
511         lcon.withReader(new ILuceneConnection.ReaderAction()
512         {
513             public Object perform(IndexReader reader)
514             {
515                 // 2 docs added, 1 removed - should have 6 docs
516                 assertEquals(6, reader.numDocs());
517                 return null;
518             }
519         });
520         assertEquals("threaded withReaderAndDeletes: Document not added in thread", 6, lcon.getNumDocs());
521     }
522     
523 
524     public void testDeleteBlocksWriter() throws InterruptedException
525     {
526         addDocsToIndex(5);
527 
528         final ExecutorService pool = Executors.newSingleThreadExecutor();
529         final AtomicBoolean innerThreadStarted = new AtomicBoolean(false);
530         final CountDownLatch writeComplete = new CountDownLatch(1);
531         final CountDownLatch latch = new CountDownLatch(1);
532         final AtomicReference<AssertionFailedError> innerError = new AtomicReference<AssertionFailedError>(null);
533         // test withWrite, then simultaneously, withReadAndDeletes
534         lcon.withWriter(new ILuceneConnection.WriterAction()
535         {
536             public void perform(final IndexWriter writer) throws IOException
537             {
538                 // Execute the writeTask in a different thread
539                 Future<?> writeTask = pool.submit(new Runnable()
540                 {
541                     public void run()
542                     {
543                         innerThreadStarted.set(true);
544                         // Let the latch know that the writeTask has began running
545                         latch.countDown();
546                         // This should only be run _after_ the ReaderAction which does the deletes completes
547                         lcon.withWriter(new ILuceneConnection.WriterAction()
548                         {
549                             public void perform(IndexWriter writer) throws IOException
550                             {
551                                 // Cannot use the 'reader' because it was closed already
552                                 lcon.withReader(new ILuceneConnection.ReaderAction()
553                                 {
554 
555                                     public Object perform(IndexReader r) throws IOException
556                                     {
557                                         try
558                                         {
559                                             assertEquals("threaded withReaderAndDeletes: deleted documents not detected from thread", 3, r.numDocs());
560                                         }
561                                         catch (AssertionFailedError e)
562                                         {
563                                             innerError.set(e);
564                                         }
565                                         return null;
566                                     }
567                                 });
568                                 writer.addDocument(createDocument());
569                             }
570                         });
571                         writeComplete.countDown();
572                     }
573                 });
574 
575                 // Wait for the writeTask to begin executing
576                 try
577                 {
578                     latch.await();
579                 }
580                 catch (InterruptedException e)
581                 {
582                     throw new RuntimeException(e);
583                 }
584 
585                 // Assert that the writeTask has started executing ...
586                 assertTrue(innerThreadStarted.get());
587                 // ... but not done yet, because it should be blocking on withWriter()
588                 // until we leave the withReaderAndDeletes() method.
589                 assertFalse(writeTask.isDone());
590 
591                 // delete documents
592                 writer.deleteDocuments(new Term(FIELD_NAME_COUNT, "0"));
593                 writer.deleteDocuments(new Term(FIELD_NAME_COUNT, "1"));
594             }
595         });
596 
597         // wait for the writeTask to complete and assert it did...
598         writeComplete.await();
599 
600         lcon.withReader(new ILuceneConnection.ReaderAction()
601         {
602             public Object perform(IndexReader reader)
603             {
604                 // 2 docs removed, 1 added - should have 4 docs
605                 assertEquals(4, reader.numDocs());
606                 return null;
607             }
608         });
609         assertEquals("threaded withReaderAndDeletes: Document not added in thread", 4, lcon.getNumDocs());
610         if (innerError.get() != null)
611             throw innerError.get();
612     }
613 
614     public void testBatchUpdateHoldsLock() throws Exception
615     {
616         final CountDownLatch latch = new CountDownLatch(1);
617         final ExecutorService pool = Executors.newSingleThreadExecutor();
618         final AtomicReference<Future<?>> secondUpdateFuture = new AtomicReference<Future<?>>(null);
619 
620         lcon.withBatchUpdate(new ILuceneConnection.BatchUpdateAction()
621         {
622             public void perform() throws Exception
623             {
624                 secondUpdateFuture.set(
625                     pool.submit(
626                         new Runnable()
627                         {
628                             public void run()
629                             {
630                                 latch.countDown();
631                                 lcon.withWriter(
632                                     new ILuceneConnection.WriterAction()
633                                     {
634                                         public void perform(IndexWriter writer)
635                                         {
636                                             assertNotNull(writer);
637                                         }
638                                     }
639                                 );
640                             }
641                         }
642                     )
643                 );
644                 latch.await();
645                 Thread.yield();
646                 lcon.withWriter(
647                     new ILuceneConnection.WriterAction()
648                     {
649                         public void perform(IndexWriter writer)
650                         {
651                             assertNotNull(writer);
652                             assertEquals("Should be batch mode maxMergeSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getBatchMaxMergeDocs(), writer.getMaxMergeDocs());
653                             assertEquals("Should be batch mode maxBufferedSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getBatchMaxBufferedDocs(), writer.getMaxBufferedDocs());
654                             assertEquals("Should be batch mode mergeFactorSettings", ILuceneConnection.DEFAULT_CONFIGURATION.getBatchMergeFactor(), writer.getMergeFactor());
655                             assertEquals("Should be default maxField", ILuceneConnection.DEFAULT_CONFIGURATION.getMaxFieldLength(), writer.getMaxFieldLength());
656                         }
657                     }
658                 );
659 
660                 Future<?> future = secondUpdateFuture.get();
661                 assertFalse(future.isDone());
662                 assertFalse(future.isCancelled());
663             }
664         });
665         Future<?> future = secondUpdateFuture.get();
666         assertNull(future.get());
667         assertTrue(future.isDone());
668         assertFalse(future.isCancelled());
669     }
670 
671     public void testWriterDoesNotBlockSearches()
672     {
673         addDocsToIndex(5);
674 
675         final int noOfSearches = 1000;
676         final ExecutorService pool = Executors.newFixedThreadPool(4);
677         final CountDownLatch complete = new CountDownLatch(noOfSearches);
678         final AtomicReference<IndexReader> readerRef = new AtomicReference<IndexReader>(null);
679 
680         lcon.withReader(
681             new ILuceneConnection.ReaderAction()
682             {
683                 public Object perform(IndexReader reader)
684                 {
685                     readerRef.set(reader);
686                     return null;
687                 }
688             }
689         );
690 
691         // test withWrite, then simultaneously, withReadAndDeletes
692         lcon.withWriter(new ILuceneConnection.WriterAction()
693         {
694             public void perform(final IndexWriter writer)
695             {
696                 for (int i = 0; i < noOfSearches; i++)
697                 {
698 
699                     // Execute the deleteTask in a different thread
700                     pool.submit(
701                         new Runnable()
702                         {
703                             public void run()
704                             {
705                                 // these should not be blocked
706                                 lcon.withSearch(new ILuceneConnection.SearcherAction()
707                                 {
708                                     public void perform(IndexSearcher searcher)
709                                     {
710                                         assertSame(readerRef.get(), searcher.getIndexReader());
711                                     }
712                                 });
713                                 lcon.withReader(new ILuceneConnection.ReaderAction()
714                                 {
715                                     public Object perform(IndexReader reader)
716                                     {
717                                         assertSame(readerRef.get(), reader);
718                                         return null;
719                                     }
720                                 });
721                                 // Let the latch know that the searchTask has completed
722                                 complete.countDown();
723                             }
724                         }
725                     );
726                 }
727 
728                 // Wait for the tasks to complete executing,
729                 // failure is an infinite wait...but we wait for 5 seconds
730                 try
731                 {
732                     assertTrue("We waited too long, should have passed", complete.await(5, SECONDS));
733                 }
734                 catch (InterruptedException e)
735                 {
736                     throw new AssertionError("We should not have to wait here, tasks should be reentrant: " + e);
737                 }
738             }
739         });
740 
741         // Test that the reader was refreshed
742         lcon.withReader(
743             new ILuceneConnection.ReaderAction()
744             {
745                 public Object perform(IndexReader reader)
746                 {
747                     assertNotSame(readerRef.get(), reader);
748                     return null;
749                 }
750             }
751         );
752     }
753 
754     public void testBatchUpdatesAreNotVisibleToSearchers() throws Exception
755     {
756         addDocsToIndex(5);
757 
758         final Callable<Integer> countDocs = new Callable<Integer>() {
759             public Integer call() throws Exception
760             {
761                 return (Integer) lcon.withReader(new ILuceneConnection.ReaderAction() {
762                     public Object perform(IndexReader reader)
763                     {
764                         return reader.numDocs();
765                     }
766                 });
767             }
768         };
769 
770         lcon.withBatchUpdate(new ILuceneConnection.BatchUpdateAction() {
771             public void perform() throws Exception
772             {
773                 assertEquals(5, (int) countDocs.call());
774 
775                 lcon.withWriter(new ILuceneConnection.WriterAction()
776                 {
777                     public void perform(IndexWriter writer) throws IOException
778                     {
779                         writer.deleteDocuments(new Term(FIELD_NAME_COUNT, "0"));
780                     }
781                 });
782 
783                 assertEquals(5, (int) countDocs.call());
784             }
785         });
786 
787         assertEquals(4, (int) countDocs.call());
788     }
789 
790     public void testClose() throws Exception
791     {
792         lcon.close();
793         Method[] methods = LuceneConnection.class.getDeclaredMethods();
794         for (Method method : methods)
795         {
796             if (!Modifier.isPublic(method.getModifiers()))
797                 continue;
798             
799             try
800             {
801                 invokeWithDefaultParams(method);
802                 fail(method + " should throw a " + LuceneConnectionClosedException.class.getName() + " because the connection is closed");
803             }
804             catch (InvocationTargetException e)
805             {
806                 assertEquals(method + " didn't throw the correct exception", LuceneConnectionClosedException.class, 
807                     e.getTargetException().getClass());
808             }
809             catch (IllegalArgumentException e)
810             {
811                 throw new RuntimeException("Could not execute method: " + method);
812             }
813         }
814     }
815 
816     private void invokeWithDefaultParams(Method method) throws IllegalAccessException, InvocationTargetException
817     {
818         List<Object> parameters = new ArrayList<Object>();
819         Class<?>[] parameterTypes = method.getParameterTypes();
820         for (Class<?> paramType : parameterTypes)
821         {
822             if (paramType.equals(Integer.TYPE))
823                 parameters.add(0);
824             else
825                 parameters.add(null);
826         }
827         method.invoke(lcon, parameters.toArray(new Object[parameters.size()]));
828     }
829     
830     public void testCantCloseTwice() throws Exception
831     {
832         lcon.close();
833         try
834         {
835             lcon.close();
836             fail("Should throw a " + LuceneConnectionClosedException.class.getName() + " because the connection is closed");
837         }
838         catch (LuceneConnectionClosedException expected) {}
839     }
840 
841     public void testTruncateIndex() throws Exception
842     {
843         final int docs = 100;
844 
845         lcon.withWriter(new ILuceneConnection.WriterAction() {
846             public void perform(IndexWriter writer) throws IOException
847             {
848                 for (int i = 0; i < docs; i++)
849                 {
850                     writer.addDocument(createDocument());
851                 }
852             }
853         });
854         assertEquals(docs, lcon.getNumDocs());
855         
856         lcon.truncateIndex();
857         
858         assertEquals(0, lcon.getNumDocs());
859     }
860     
861     public void testTruncateIndexWithDeletes() throws Exception
862     {
863         lcon.withWriter(new ILuceneConnection.WriterAction() {
864             public void perform(IndexWriter writer) throws IOException
865             {
866                 writer.addDocument(createDocument("0"));
867                 writer.addDocument(createDocument("1"));
868             }
869         });
870         lcon.withWriter(new ILuceneConnection.WriterAction()
871         {
872             public void perform(IndexWriter writer) throws IOException
873             {
874                 writer.deleteDocuments(new Term(FIELD_NAME_COUNT, "0"));
875             }
876         });
877         lcon.withWriter(new ILuceneConnection.WriterAction() {
878             public void perform(IndexWriter writer) throws IOException
879             {
880                 writer.addDocument(createDocument());
881             }
882         });
883         
884         assertEquals(2, lcon.getNumDocs());
885         
886         lcon.truncateIndex();
887         
888         assertEquals(0, lcon.getNumDocs());
889     }
890 
891     public void testTruncateIndexDoesNotAffectCurrentReaders() throws Exception
892     {
893         final int docs = 3;
894         
895         lcon.withWriter(new ILuceneConnection.WriterAction() {
896             public void perform(IndexWriter writer) throws IOException
897             {
898                 for (int i = 0; i < docs; i++)
899                 {
900                     writer.addDocument(createDocument());
901                 }
902             }
903         });
904         assertEquals(docs, lcon.getNumDocs());
905         
906         final AtomicReference<IndexReader> readerRef = new AtomicReference<IndexReader>();
907         lcon.withReader(new ILuceneConnection.ReaderAction() {
908             public Object perform(IndexReader reader)
909             {
910                 readerRef.set(reader);
911                 return null;
912             }
913         });
914 
915         assertEquals("check document count before truncate", docs, readerRef.get().numDocs());
916         
917         lcon.truncateIndex();
918         
919         assertEquals("existing readers should not see truncate", docs, readerRef.get().numDocs());
920         
921         assertEquals(0, lcon.getNumDocs());
922         lcon.withReader(new ILuceneConnection.ReaderAction() {
923             public Object perform(IndexReader reader)
924             {
925                 assertNotSame("reader should have been refreshed after truncate", readerRef.get(), reader);
926                 assertEquals(0, reader.numDocs());
927                 return null;
928             }
929         });
930     }
931 
932     public void testSearcherInitialisation() throws Exception
933     {
934         final AtomicBoolean isSearcherInitialised = new AtomicBoolean();
935         lcon = new LuceneConnection(directory, new StandardAnalyzer(), ILuceneConnection.DEFAULT_CONFIGURATION, new SearcherInitialisation() {
936             public void initialise(IndexSearcher searcher)
937             {
938                 isSearcherInitialised.set(true);
939             }
940         });
941 
942         // Initialization happens per segment. Lets add enough documents to create a new segment
943         lcon.withWriter(new ILuceneConnection.WriterAction()
944         {
945             public void perform(IndexWriter writer) throws IOException
946             {
947                 for (int i = 0; i < ILuceneConnection.DEFAULT_CONFIGURATION.getInteractiveMaxMergeDocs(); i++)
948                 {
949                     writer.addDocument(createDocument());
950                 }
951                 writer.addDocument(createDocument());
952             }
953         });
954 
955         lcon.withReader(new ILuceneConnection.ReaderAction() {
956             public Object perform(IndexReader reader)
957             {
958                 assertTrue(isSearcherInitialised.get());
959                 return null;
960             }
961         });
962         
963         // reset warmed-up status to check that LuceneConnection initialises a new searcher
964         isSearcherInitialised.set(false);
965 
966         lcon.withWriter(new ILuceneConnection.WriterAction() {
967             public void perform(IndexWriter writer) throws IOException
968             {
969                 assertFalse(isSearcherInitialised.get());
970                 for (int i = 0; i < ILuceneConnection.DEFAULT_CONFIGURATION.getInteractiveMaxMergeDocs(); i++)
971                 {
972                     writer.addDocument(createDocument());
973                 }
974                 writer.addDocument(createDocument());
975             }
976         });
977 
978         assertTrue(isSearcherInitialised.get());
979     }
980 
981 
982     public void testWriteDuringWithSearch() throws Exception
983     {
984         final CountDownLatch readerStarted = new CountDownLatch(1);
985         final CountDownLatch readerLatch = new CountDownLatch(1);
986 
987         new Thread(new Runnable() {
988             public void run()
989             {
990                 try
991                 {
992                     readerStarted.await(1, SECONDS);
993                 }
994                 catch (InterruptedException e)
995                 {
996                     throw new RuntimeException(e);
997                 }
998                 lcon.withWriter(NOOP_WRITER_ACTION); // refresh searcher
999                 readerLatch.countDown();
1000             }
1001         }).start();
1002 
1003         lcon.withSearch(new ILuceneConnection.SearcherAction()
1004         {
1005             public void perform(IndexSearcher searcher)
1006             {
1007                 readerStarted.countDown();
1008                 try
1009                 {
1010                     readerLatch.await(1, SECONDS);
1011                 }
1012                 catch (InterruptedException e)
1013                 {
1014                     throw new RuntimeException(e);
1015                 }
1016             }
1017         });
1018     }
1019 
1020     public void testWriteDuringWithReader() throws Exception
1021     {
1022         final CountDownLatch readerStarted = new CountDownLatch(1);
1023         final CountDownLatch readerLatch = new CountDownLatch(1);
1024 
1025         new Thread(new Runnable() {
1026             public void run()
1027             {
1028                 try
1029                 {
1030                     readerStarted.await(1, SECONDS);
1031                 }
1032                 catch (InterruptedException e)
1033                 {
1034                     throw new RuntimeException(e);
1035                 }
1036                 lcon.withWriter(NOOP_WRITER_ACTION); // refresh searcher
1037                 readerLatch.countDown();
1038             }
1039         }).start();
1040 
1041         lcon.withReader(new ILuceneConnection.ReaderAction()
1042         {
1043             public Object perform(IndexReader reader)
1044             {
1045                 readerStarted.countDown();
1046                 try
1047                 {
1048                     readerLatch.await(1, SECONDS);
1049                 }
1050                 catch (InterruptedException e)
1051                 {
1052                     throw new RuntimeException(e);
1053                 }
1054                 return null;
1055             }
1056         });
1057     }
1058 
1059     private Document createDocument(String id)
1060     {
1061         Document d = new Document();
1062         d.add(new Field(FIELD_NAME, FIELD_VALUE, Field.Store.YES, Field.Index.TOKENIZED));
1063         d.add(new Field(FIELD_NAME_COUNT, id, Field.Store.YES, Field.Index.TOKENIZED));
1064         return d;
1065     }
1066 
1067     private Document createDocument()
1068     {
1069         return createDocument(UUID.randomUUID().toString());
1070     }
1071 
1072     private void addDocsToIndex(final int numDocs)
1073     {
1074         lcon.withWriter(new ILuceneConnection.WriterAction()
1075         {
1076             public void perform(IndexWriter writer) throws IOException
1077             {
1078                 for (int i = 0; i < numDocs; ++i)
1079                 {
1080                     writer.addDocument(createDocument(Integer.toString(i)));
1081                 }
1082             }
1083         });
1084     }
1085 }