View Javadoc

1   package com.atlassian.bonnie;
2   
3   import edu.emory.mathcs.backport.java.util.concurrent.locks.ReadWriteLock;
4   import edu.emory.mathcs.backport.java.util.concurrent.locks.ReentrantReadWriteLock;
5   import org.apache.log4j.Category;
6   import org.apache.log4j.Level;
7   import org.apache.lucene.analysis.Analyzer;
8   import org.apache.lucene.index.IndexReader;
9   import org.apache.lucene.index.IndexUtils;
10  import org.apache.lucene.index.IndexWriter;
11  import org.apache.lucene.search.IndexSearcher;
12  import org.apache.lucene.store.Directory;
13  import org.apache.lucene.store.FSDirectory;
14  
15  import java.io.File;
16  import java.io.FileNotFoundException;
17  import java.io.IOException;
18  
19  /**
20   * TODO reentrancy to/from reader/writer ... allow it?
21   * these operations can occur concurrently, using the same reader (writer closed beforehand):
22   * - withSearch
23   * - withReader
24   * <p/>
25   * these operations will be serialized against all other operatons
26   * - withWriter (closes any readers closed beforehand)
27   * - withReaderAndDeletes (writers closed beforehand, reader+writer closed afterwards)
28   * - optimize (any readers closed beforehand)
29   * - flushWriter (closes readers and writers)
30   * <p/>
31   * also see:
32   * - "What are all possible concurrent Lucene requests?" http://www.jguru.com/faq/view.jsp?EID=913302
33   *
34   * @author Matt Quail
35   */
36  public class LuceneConnection implements ILuceneConnection
37  {
38      public static final Category LOG = Category.getInstance(LuceneConnection.class);
39  
40      public static final int WRITER_DEFAULT = 0x01;
41      public static final int WRITER_INTERACTIVE = 0x02;
42      public static final int WRITER_BATCH = 0x04;
43  
44      public static final String REINDEXING_FILENAME = ".reindexing";
45  
46      protected Analyzer analyzerForIndexing;
47      protected boolean searcherOpensNewReader = true;
48  
49      private File indexDir = null;
50  
51      /**
52       * The directory that contains the lucene index.
53       */
54      private Directory directory = null;
55  
56      private final ReadWriteLock sync = new ReentrantReadWriteLock();
57      private final Object createLock = new Object();
58  
59      private final Configuration configuration;
60  
61      private IndexReader reader = null;
62      private IndexWriter writer = null;
63  
64      private long readerVersion = -1;
65      private boolean alwaysUnlock = true;
66  
67  
68      /**
69       * Default constructor.
70       */
71      public LuceneConnection()
72      {
73          this.configuration = DEFAULT_CONFIGURATION;
74      }
75  
76      /**
77       * Create a new lucene connection, setting the index directory to be the specified location.
78       *
79       * @param location of the index directory.
80       *
81       * @param analyzerForIndexing
82       */
83      public LuceneConnection(File location, Analyzer analyzerForIndexing)
84      {
85          this(location, analyzerForIndexing, DEFAULT_CONFIGURATION);
86      }
87      /**
88       * Create a new lucene connection, setting the index directory to be the specified location.
89       *
90       * @param location of the index directory.
91       * @param analyzerForIndexing
92       * @param configuration of the Lucene IndexWriter settings
93       */
94      public LuceneConnection(File location, Analyzer analyzerForIndexing, Configuration configuration)
95      {
96          this.indexDir = location;
97          this.analyzerForIndexing = analyzerForIndexing;
98          this.configuration = configuration;
99      }
100 
101     /**
102      * Create a new lucene connection, setting the index directory to be the specified directory.
103      *
104      * @param directory used for storing the lucene index.
105      * @param analyzerForIndexing
106      */
107     public LuceneConnection(Directory directory, Analyzer analyzerForIndexing)
108     {
109         this.directory = directory;
110         this.analyzerForIndexing = analyzerForIndexing;
111         this.configuration = DEFAULT_CONFIGURATION;
112     }
113 
114     /**
115      * Create a new lucene connection, setting the index directory to be the specified directory.
116      *
117      * @param directory used for storing the lucene index.
118      * @param analyzerForIndexing
119      * @param configuration of the Lucene IndexWriter settings
120      */
121     public LuceneConnection(Directory directory, Analyzer analyzerForIndexing, Configuration configuration)
122     {
123         this.directory = directory;
124         this.analyzerForIndexing = analyzerForIndexing;
125         this.configuration = configuration;
126     }
127 
128     /*
129      * @see com.atlassian.bonnie.ILuceneConnection#withSearch(com.atlassian.bonnie.LuceneConnection.SearcherAction)
130      */
131     public void withSearch(LuceneConnection.SearcherAction action) throws LuceneException
132     {
133         IndexSearcher searcher = null;
134         grabReadLock();
135         try
136         {
137             searcher = getSearcher();
138             if (action.perform(searcher))
139             {
140                 searcher.close();
141             }
142         }
143         catch (Throwable e)
144         {
145             if (searcher != null)
146             {
147                 try
148                 {
149                     searcher.close();
150                 }
151                 catch (Throwable t)
152                 {
153                     LOG.debug(t);
154                 }
155             }
156             if (e instanceof Error)
157             {
158                 throw (Error) e;
159             }
160             if (e instanceof RuntimeException)
161             {
162                 throw (RuntimeException) e;
163             }
164             throw new LuceneException(e);
165         }
166         finally
167         {
168             releaseReadLock();
169         }
170     }
171 
172     protected IndexSearcher getSearcher() throws IOException
173     {
174         if (searcherOpensNewReader)
175         {
176             return new IndexSearcher(getDirectory());
177         }
178         else
179         {
180             return new IndexSearcher(getReader());
181         }
182     }
183 
184     /*
185      * @see com.atlassian.bonnie.ILuceneConnection#withReader(com.atlassian.bonnie.LuceneConnection.ReaderAction)
186      */
187     public Object withReader(LuceneConnection.ReaderAction action) throws LuceneException
188     {
189         Object result = null;
190         grabReadLock();
191         try
192         {
193             IndexReader reader = getReader();
194             result = action.perform(reader);
195         }
196         catch (IOException e)
197         {
198             throw new LuceneException(e);
199         }
200         finally
201         {
202             releaseReadLock();
203         }
204         return result;
205     }
206 
207     /*
208      * @see com.atlassian.bonnie.ILuceneConnection#withReaderAndDeletes(com.atlassian.bonnie.LuceneConnection.ReaderAction)
209      */
210     public void withReaderAndDeletes(ReaderAction action) throws LuceneException
211     {
212         grabWriteLock();
213         try
214         {
215             unlockIfNeeded();
216             closeWriter();
217             IndexReader reader = getReader();
218             action.perform(reader);
219             closeReader();
220         }
221         catch (IOException e)
222         {
223             //TODO invalidate reader/write on any exception
224             throw new LuceneException(e);
225         }
226         finally
227         {
228             releaseWriteLock();
229         }
230     }
231 
232     /*
233      * @see com.atlassian.bonnie.ILuceneConnection#withWriter(com.atlassian.bonnie.LuceneConnection.WriterAction)
234      */
235     public void withWriter(WriterAction action) throws LuceneException
236     {
237         withWriter(action, WRITER_INTERACTIVE);
238     }
239 
240     public void withWriter(WriterAction action, int flags) throws LuceneException
241     {
242         grabWriteLock();
243         try
244         {
245             unlockIfNeeded();
246             IndexWriter writer = getWriter(flags);
247             action.perform(writer);
248             if (!isReIndexing())
249             {
250                 closeWriter();
251             }
252         }
253         catch (IOException e)
254         {
255             throw new LuceneException(e);
256         }
257         finally
258         {
259             releaseWriteLock();
260         }
261     }
262     
263     public void withDeleteAndWrites(ReaderAction readerAction, WriterAction writerAction) throws LuceneException
264     {
265         grabWriteLock();
266         try
267         {
268             withReaderAndDeletes(readerAction);
269             withWriter(writerAction);
270         }
271         finally
272         {
273         	releaseWriteLock();
274         }
275     }
276 
277     /*
278      * @see com.atlassian.bonnie.ILuceneConnection#withBatchUpdate(com.atlassian.bonnie.LuceneConnection.BatchUpdateAction)
279      */
280     public void withBatchUpdate(BatchUpdateAction action)
281     {
282         grabWriteLock();
283         try
284         {
285             action.perform();
286         }
287         catch (Exception e)
288         {
289             throw new LuceneException(e);
290         }
291         finally
292         {
293             releaseWriteLock();
294         }
295     }
296 
297     /*
298      * @see com.atlassian.bonnie.ILuceneConnection#optimize()
299      */
300     public void optimize() throws LuceneException
301     {
302         grabWriteLock();
303         try
304         {
305             IndexWriter writer = getWriter();
306             writer.optimize();
307             closeWriter();
308         }
309         catch (IOException e)
310         {
311             throw new LuceneException(e);
312         }
313         finally
314         {
315             releaseWriteLock();
316         }
317     }
318 
319     public void setSearcherOpensNewReader(boolean searcherOpensNewReader)
320     {
321         this.searcherOpensNewReader = searcherOpensNewReader;
322     }
323 
324     private IndexWriter createAndConfigureWriter(int flags) throws LuceneException
325     {
326         try
327         {
328             if (LOG.isDebugEnabled())
329             {
330                 LOG.debug(Thread.currentThread().getName() + "## opening writer");
331             }
332             IndexWriter writer = new IndexWriter(getDirectory(), analyzerForIndexing, false);
333             configureWriter(writer, flags);
334             return writer;
335         }
336         catch (IOException e)
337         {
338             throw new LuceneException(e);
339         }
340     }
341 
342     /**
343      * Configures a writer according to our particular requirements.
344      * See http://marc.theaimsgroup.com/?l=lucene-user&m=110235452004799&w=2 for more details
345      */
346     void configureWriter(IndexWriter writer, int flags)
347     {
348         writer.setMaxFieldLength(configuration.getMaxFieldLength());
349         writer.setUseCompoundFile(configuration.isCompoundIndexFileFormat());
350         if (isSet(flags, WRITER_BATCH))
351         {
352             writer.setMergeFactor(configuration.getBatchMergeFactor());
353             writer.setMaxBufferedDocs(configuration.getBatchMaxBufferedDocs());
354             writer.setMaxMergeDocs(configuration.getBatchMaxMergeDocs());
355         }
356         else if (isSet(flags, WRITER_INTERACTIVE))
357         {
358             writer.setMergeFactor(configuration.getInteractiveMergeFactor());
359             writer.setMaxBufferedDocs(configuration.getInteractiveMaxBufferedDocs());
360             writer.setMaxMergeDocs(configuration.getInteractiveMaxMergeDocs());
361         }
362     }
363 
364     private static boolean isSet(int flags, int f)
365     {
366         return (flags & f) > 0;
367     }
368 
369     public void flushWriter() throws LuceneException
370     {
371         grabWriteLock();
372         try
373         {
374             closeReader();
375             closeWriter();
376         }
377         finally
378         {
379             releaseWriteLock();
380         }
381     }
382 
383     protected final IndexReader getReader() throws LuceneException
384     {
385         synchronized (createLock)
386         {
387 //            closeWriter();
388             try
389             {
390                 if (reader == null)
391                 {
392                     createIndexDirectory(false);
393                     LOG.debug(Thread.currentThread().getName() + "##opening reader");
394                     readerVersion = IndexReader.getCurrentVersion(getDirectory());
395                     reader = IndexReader.open(getDirectory());
396                 }
397                 else
398                 {
399                     long v = IndexReader.getCurrentVersion(getDirectory());
400                     if (v != readerVersion)
401                     {
402                         createIndexDirectory(false);
403                         LOG.debug(Thread.currentThread().getName() + "Refreshing reader");
404                         // if current version for directory is difference from existing version in reader, then
405                         // close the reader and open a new one.
406                         reader.close();
407                         reader = IndexReader.open(getDirectory());
408                         readerVersion = v;
409                     }
410                 }
411                 return reader;
412             }
413             catch (IOException e)
414             {
415                 throw new LuceneException(e);
416             }
417         }
418     }
419 
420     protected IndexWriter getWriter() throws LuceneException
421     {
422         return getWriter(WRITER_DEFAULT);
423     }
424 
425     protected IndexWriter getWriter(int flags) throws LuceneException
426     {
427         synchronized (createLock)
428         {
429             createIndexDirectory(false);
430             closeReader();
431             if (writer == null)
432             {
433                 writer = createAndConfigureWriter(flags);
434             }
435             return writer;
436         }
437     }
438 
439     /**
440      * you must have a write-lock to call this method
441      */
442     private void closeWriter()
443     {
444         synchronized (createLock)
445         {
446             if (writer != null)
447             {
448                 try
449                 {
450                     IndexWriter writer = this.writer;
451                     this.writer = null;
452                     if (LOG.isDebugEnabled())
453                     {
454                         LOG.debug(Thread.currentThread().getName() + "##closing writer");
455                     }
456                     writer.close();
457                 }
458                 catch (IOException e)
459                 {
460                     LOG.error(Level.ERROR + "problem closing index writer" + e, e);
461                 }
462             }
463         }
464     }
465 
466     /**
467      * you must have a write-lock to call this method
468      */
469     private void closeReader()
470     {
471         synchronized (createLock)
472         {
473             if (reader != null)
474             {
475                 try
476                 {
477                     IndexReader reader = this.reader;
478                     this.reader = null;
479                     if (LOG.isDebugEnabled())
480                     {
481                         LOG.debug(Thread.currentThread().getName() + "##closing reader");
482                     }
483                     reader.close();
484                 }
485                 catch (IOException e)
486                 {
487                     LOG.error("problem closing index reader" + e, e);
488                 }
489             }
490         }
491     }
492 
493     private void grabReadLock() throws LuceneException
494     {
495         sync.readLock().lock();
496     }
497 
498     private void releaseReadLock()
499     {
500         sync.readLock().unlock();
501     }
502 
503     private void grabWriteLock() throws LuceneException
504     {
505         sync.writeLock().lock();
506     }
507 
508     private void releaseWriteLock()
509     {
510         sync.writeLock().unlock();
511     }
512 
513     /*
514      * @see com.atlassian.bonnie.ILuceneConnection#close()
515      */
516     public void close()
517     {
518         try
519         {
520             grabWriteLock();
521             try
522             {
523                 closeReader();
524                 closeWriter();
525             }
526             finally
527             {
528                 releaseWriteLock();
529             }
530         }
531         catch (LuceneException e)
532         {
533             LOG.error("problem closing lucene connection" + e);
534         }
535     }
536 
537     public boolean isReIndexing() throws IOException
538     {
539         return getDirectory().fileExists(REINDEXING_FILENAME);
540     }
541 
542     /**
543      * Sets reindexing flag. This is NOT a simple setter method. If true,
544      * the index's timestamp is updated and a .reindexing file is created as a flag.
545      * If false, the .reindexing file is deleted and the writer is closed.
546      *
547      * @param b
548      * @throws IOException
549      */
550     public void setReIndexing(boolean b) throws IOException
551     {
552         if (!b)
553         {
554             if (isReIndexing())
555             {
556                 getDirectory().deleteFile(REINDEXING_FILENAME);
557                 closeWriter();
558             }
559         }
560         else if (!isReIndexing())
561         {
562             Directory directory = getDirectory();
563             if (!directory.fileExists(REINDEXING_FILENAME))
564             {
565                 directory.createOutput(REINDEXING_FILENAME).close();
566             }
567             else
568             {
569                 directory.touchFile(REINDEXING_FILENAME);
570             }
571         }
572     }
573 
574     public void recreateIndexDirectory()
575     {
576         createIndexDirectory(true);
577     }
578 
579     public IndexSearcher leakSearcher()
580     {
581         throw new UnsupportedOperationException("Not Implemented. Use withSearch method instead.");
582     }
583 
584     /**
585      * Returns true if the index directory should be created. This implementation will return true if and only if the index
586      * does not already exist.
587      *
588      * @return
589      *
590      * @throws IOException
591      */
592     protected boolean shouldCreateIndexDirectory() throws IOException
593     {
594         return !isIndexCreated();
595     }
596 
597     /**
598      * Create the lucene index. Note: If the index directory already exists AND forceCreate is true, then the existing
599      * index will be deleted and a new one created. If forceCreate is false, then a new index is created iff the index
600      * does not already exist.
601      *
602      * @param forceCreate if you want a new index to be created.
603      */
604     private void createIndexDirectory(boolean forceCreate)
605     {
606         try
607         {
608             if (forceCreate || (shouldCreateIndexDirectory()))
609             {
610                 // this will create a new directory (and erase the old if it exists)
611                 new IndexWriter(getDirectory(), null, true).close();
612             }
613         }
614         catch (IOException e)
615         {
616             throw new LuceneException("Cannot create index directory", e);
617         }
618     }
619 
620     public void truncateIndex() throws IOException
621     {
622         grabWriteLock();
623 
624         try
625         {
626             IndexUtils.truncateIndex(getDirectory());
627         }
628         catch(FileNotFoundException e)
629         {
630             // the index probably doesn't exist or is corrupt. just recreate it then
631             recreateIndexDirectory();
632         }
633         finally
634         {
635             releaseWriteLock();
636         }
637     }
638 
639     /**
640      *
641      * @return
642      * @throws IOException
643      */
644     protected Directory getDirectory() throws IOException
645     {
646         if (directory == null)
647         {
648             File indexDir = getIndexDir();
649             if (!indexDir.exists() && !indexDir.mkdir())
650             {
651                 throw new IOException("Unable to create index directory '" + indexDir.getAbsolutePath() + "'");
652             }
653             directory = FSDirectory.getDirectory(indexDir);
654         }
655         return directory;
656     }
657 
658     public File getIndexDir()
659     {
660         return indexDir;
661     }
662 
663     public void setIndexDir(String location)
664     {
665         this.indexDir = new File(location);
666     }
667 
668     /*
669      * @see com.atlassian.bonnie.ILuceneConnection#getNumDocs()
670      */
671     public int getNumDocs()
672     {
673         int count = -1;
674         try
675         {
676             grabReadLock();
677             count = getReader().numDocs();
678         }
679         finally
680         {
681             releaseReadLock();
682         }
683         return count;
684     }
685 
686     /*
687      * @see com.atlassian.bonnie.ILuceneConnection#isIndexCreated()
688      */
689     public boolean isIndexCreated()
690     {
691         try
692         {
693             return IndexReader.indexExists(getDirectory());
694         }
695         catch (IOException e)
696         {
697             return false;
698         }
699     }
700 
701     public void unlockIfNeeded() throws IOException
702     {
703         if (alwaysUnlock)
704         {
705             IndexReader.unlock(getDirectory());
706         }
707     }
708 
709     public void setAlwaysUnlock(boolean alwaysUnlock)
710     {
711         this.alwaysUnlock = alwaysUnlock;
712     }
713 
714     public class TempIndexWriterData
715     {
716         public final IndexWriter writer;
717         public final Directory dir;
718         public final File tmpIndexDir;
719 
720         public TempIndexWriterData(IndexWriter writer, Directory dir, File tmpIndexDir)
721         {
722             this.writer = writer;
723             this.dir = dir;
724             this.tmpIndexDir = tmpIndexDir;
725         }
726     }
727 }