1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    * Permission is hereby granted, free of charge, to any person obtaining a copy
5    * of this software and associated documentation files (the "Software"), to deal
6    * in the Software without restriction, including without limitation the rights
7    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    * copies of the Software, and to permit persons to whom the Software is
9    * furnished to do so, subject to the following conditions:
10   *
11   * The above copyright notice and this permission notice shall be included in
12   * all copies or substantial portions of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.search.lucene;
24  
25  import com.liferay.portal.SystemException;
26  import com.liferay.portal.kernel.log.Log;
27  import com.liferay.portal.kernel.log.LogFactoryUtil;
28  import com.liferay.portal.kernel.util.GetterUtil;
29  import com.liferay.portal.model.Company;
30  import com.liferay.portal.model.CompanyConstants;
31  import com.liferay.portal.service.CompanyLocalServiceUtil;
32  import com.liferay.portal.util.PropsKeys;
33  import com.liferay.portal.util.PropsUtil;
34  import com.liferay.portal.util.PropsValues;
35  import com.liferay.util.SystemProperties;
36  
37  import java.io.File;
38  import java.io.IOException;
39  
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.concurrent.Semaphore;
44  
45  import org.apache.lucene.analysis.SimpleAnalyzer;
46  import org.apache.lucene.index.IndexReader;
47  import org.apache.lucene.index.IndexWriter;
48  import org.apache.lucene.index.Term;
49  import org.apache.lucene.store.Directory;
50  import org.apache.lucene.store.FSDirectory;
51  
52  /**
53   * <a href="IndexWriterFactory.java.html"><b><i>View Source</i></b></a>
54   *
55   * <p>
56   * Lucene only allows one IndexWriter to be open at a time. However, multiple
57   * threads can use this single IndexWriter. This class manages a global
58   * IndexWriter and uses reference counting to determine when it can be closed.
59   * </p>
60   *
61   * <p>
62   * To delete documents, IndexReaders are used but cannot delete while another
63   * IndexWriter or IndexReader has the write lock. A semaphore is used to
64   * serialize delete and add operations. If the shared IndexWriter is open,
65   * concurrent add operations are permitted.
66   * </p>
67   *
68   * @author Harry Mark
69   * @author Brian Wing Shun Chan
70   *
71   */
72  public class IndexWriterFactory {
73  
74      public IndexWriterFactory() {
75          if (PropsValues.INDEX_READ_ONLY) {
76              return;
77          }
78  
79          // Create semaphores for all companies
80  
81          try {
82              List<Company> companies = CompanyLocalServiceUtil.getCompanies();
83  
84              for (Company company : companies) {
85                  _lockLookup.put(company.getCompanyId(), new Semaphore(1));
86              }
87  
88              _lockLookup.put(CompanyConstants.SYSTEM, new Semaphore(1));
89          }
90          catch (SystemException se) {
91              _log.error(se);
92          }
93      }
94  
95      public void acquireLock(long companyId, boolean needExclusive)
96          throws InterruptedException {
97  
98          if (PropsValues.INDEX_READ_ONLY) {
99              return;
100         }
101 
102         Semaphore lock = _lockLookup.get(companyId);
103 
104         if (lock != null) {
105 
106             // Exclusive checking is used to prevent greedy IndexWriter sharing.
107             // This registers a need for exclusive lock, and causes IndexWriters
108             // to wait in FIFO order.
109 
110             if (needExclusive) {
111                 synchronized (_lockLookup) {
112                     _needExclusiveLock++;
113                 }
114             }
115 
116             try {
117                 lock.acquire();
118             }
119             finally {
120                 if (needExclusive) {
121                     synchronized (_lockLookup) {
122                         _needExclusiveLock--;
123                     }
124                 }
125             }
126         }
127         else {
128             if (_log.isWarnEnabled()) {
129                 _log.warn("IndexWriterFactory lock not found for " + companyId);
130             }
131         }
132     }
133 
134     public void deleteDocuments(long companyId, Term term)
135         throws InterruptedException, IOException {
136 
137         if (PropsValues.INDEX_READ_ONLY) {
138             return;
139         }
140 
141         try {
142             acquireLock(companyId, true);
143 
144             IndexReader reader = null;
145 
146             try {
147                 reader = IndexReader.open(LuceneUtil.getLuceneDir(companyId));
148 
149                 reader.deleteDocuments(term);
150             }
151             finally {
152                 if (reader != null) {
153                     reader.close();
154                 }
155             }
156         }
157         finally {
158             releaseLock(companyId);
159         }
160     }
161 
162     public IndexWriter getWriter(long companyId, boolean create)
163         throws IOException {
164 
165         if (PropsValues.INDEX_READ_ONLY) {
166             return getReadOnlyIndexWriter();
167         }
168 
169         boolean hasError = false;
170         boolean newWriter = false;
171 
172         try {
173 
174             // If others need an exclusive lock, then wait to acquire lock
175             // before proceeding. This prevents starvation.
176 
177             if (_needExclusiveLock > 0) {
178                 acquireLock(companyId, false);
179                 releaseLock(companyId);
180             }
181 
182             synchronized (this) {
183                 IndexWriterData writerData = _writerLookup.get(companyId);
184 
185                 if (writerData == null) {
186                     newWriter = true;
187 
188                     acquireLock(companyId, false);
189 
190                     IndexWriter writer = new IndexWriter(
191                         LuceneUtil.getLuceneDir(companyId),
192                         LuceneUtil.getAnalyzer(), create);
193 
194                     writer.setMergeFactor(_MERGE_FACTOR);
195 
196                     writerData = new IndexWriterData(companyId, writer, 0);
197 
198                     _writerLookup.put(companyId, writerData);
199                 }
200 
201                 writerData.setCount(writerData.getCount() + 1);
202 
203                 return writerData.getWriter();
204             }
205         }
206         catch (Exception e) {
207             hasError = true;
208 
209             _log.error("Unable to create a new writer", e);
210 
211             throw new IOException("Unable to create a new writer");
212         }
213         finally {
214             if (hasError && newWriter) {
215                 try {
216                     releaseLock(companyId);
217                 }
218                 catch (Exception e) {
219                 }
220             }
221         }
222     }
223 
224     public void releaseLock(long companyId) {
225         if (PropsValues.INDEX_READ_ONLY) {
226             return;
227         }
228 
229         Semaphore lock = _lockLookup.get(companyId);
230 
231         if (lock != null) {
232             lock.release();
233         }
234     }
235 
236     public void write(long companyId) {
237         if (PropsValues.INDEX_READ_ONLY) {
238             return;
239         }
240 
241         IndexWriterData writerData = _writerLookup.get(companyId);
242 
243         if (writerData != null) {
244             decrement(writerData);
245         }
246         else {
247             if (_log.isWarnEnabled()) {
248                 _log.warn("IndexWriterData not found for " + companyId);
249             }
250         }
251     }
252 
253     public void write(IndexWriter writer) throws IOException {
254         if (PropsValues.INDEX_READ_ONLY) {
255             return;
256         }
257 
258         boolean writerFound = false;
259 
260         synchronized (this) {
261             if (!_writerLookup.isEmpty()) {
262                 for (IndexWriterData writerData : _writerLookup.values()) {
263                     if (writerData.getWriter() == writer) {
264                         writerFound = true;
265 
266                         decrement(writerData);
267 
268                         break;
269                     }
270                 }
271             }
272         }
273 
274         if (!writerFound) {
275             try {
276                 _optimizeCount++;
277 
278                 if ((_OPTIMIZE_INTERVAL == 0) ||
279                     (_optimizeCount >= _OPTIMIZE_INTERVAL)) {
280 
281                     writer.optimize();
282 
283                     _optimizeCount = 0;
284                 }
285             }
286             finally {
287                 writer.close();
288             }
289         }
290     }
291 
292     protected void decrement(IndexWriterData writerData) {
293         if (writerData.getCount() > 0) {
294             writerData.setCount(writerData.getCount() - 1);
295 
296             if (writerData.getCount() == 0) {
297                 _writerLookup.remove(writerData.getCompanyId());
298 
299                 try {
300                     IndexWriter writer = writerData.getWriter();
301 
302                     try {
303                         _optimizeCount++;
304 
305                         if ((_OPTIMIZE_INTERVAL == 0) ||
306                             (_optimizeCount >= _OPTIMIZE_INTERVAL)) {
307 
308                             writer.optimize();
309 
310                             _optimizeCount = 0;
311                         }
312                     }
313                     finally {
314                         writer.close();
315                     }
316                 }
317                 catch (Exception e) {
318                     _log.error(e, e);
319                 }
320                 finally {
321                     releaseLock(writerData.getCompanyId());
322                 }
323             }
324         }
325     }
326 
327     protected IndexWriter getReadOnlyIndexWriter() {
328         if (_readOnlyIndexWriter == null) {
329             try {
330                 if (_log.isInfoEnabled()) {
331                     _log.info("Disabling writing to index for this process");
332                 }
333 
334                 _readOnlyIndexWriter = new ReadOnlyIndexWriter(
335                     getReadOnlyLuceneDir(), new SimpleAnalyzer(), true);
336             }
337             catch (IOException ioe) {
338                 throw new RuntimeException(ioe);
339             }
340         }
341 
342         return _readOnlyIndexWriter;
343     }
344 
345     protected Directory getReadOnlyLuceneDir() throws IOException {
346         if (_readOnlyLuceneDir == null) {
347             String tmpDir = SystemProperties.get(SystemProperties.TMP_DIR);
348 
349             File dir = new File(tmpDir + "/liferay/lucene/empty");
350 
351             dir.mkdir();
352 
353             _readOnlyLuceneDir = LuceneUtil.getDirectory(dir.getPath(), false);
354         }
355 
356         return _readOnlyLuceneDir;
357     }
358 
359     private static final int _MERGE_FACTOR = GetterUtil.getInteger(
360         PropsUtil.get(PropsKeys.LUCENE_MERGE_FACTOR));
361 
362     private static final int _OPTIMIZE_INTERVAL = GetterUtil.getInteger(
363         PropsUtil.get(PropsKeys.LUCENE_OPTIMIZE_INTERVAL));
364 
365     private static Log _log = LogFactoryUtil.getLog(IndexWriterFactory.class);
366 
367     private FSDirectory _readOnlyLuceneDir = null;
368     private IndexWriter _readOnlyIndexWriter = null;
369     private Map<Long, Semaphore> _lockLookup = new HashMap<Long, Semaphore>();
370     private Map<Long, IndexWriterData> _writerLookup =
371         new HashMap<Long, IndexWriterData>();
372     private int _needExclusiveLock = 0;
373     private int _optimizeCount = 0;
374 
375 }