1 package org.apache.lucene.index;
2
3 /**
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19
20 import java.io.IOException;
21 import java.io.FileNotFoundException;
22
23 import java.util.HashSet;
24 import java.util.Collection;
25 import java.util.ArrayList;
26 import java.util.List;
27
28 import org.apache.lucene.store.Directory;
29 import org.apache.lucene.store.Lock;
30 import org.apache.lucene.store.LockObtainFailedException;
31 import org.apache.lucene.store.FSDirectory;
32
33 /**
34 * IndexReader implementation that has access to a Directory.
35 * Instances that have a SegmentInfos object (i. e. segmentInfos != null)
36 * "own" the directory, which means that they try to acquire a write lock
37 * whenever index modifications are performed.
38 */
39 abstract class DirectoryIndexReader extends IndexReader {
40 protected Directory directory;
41 protected boolean closeDirectory;
42 private IndexDeletionPolicy deletionPolicy;
43
44 private SegmentInfos segmentInfos;
45 private Lock writeLock;
46 private boolean stale;
47 private final HashSet synced = new HashSet();
48
49 /** Used by commit() to record pre-commit state in case
50 * rollback is necessary */
51 private boolean rollbackHasChanges;
52 private SegmentInfos rollbackSegmentInfos;
53
54 protected boolean readOnly;
55
56
57 void init(Directory directory, SegmentInfos segmentInfos, boolean closeDirectory, boolean readOnly)
58 throws IOException {
59 this.directory = directory;
60 this.segmentInfos = segmentInfos;
61 this.closeDirectory = closeDirectory;
62 this.readOnly = readOnly;
63
64 if (!readOnly && segmentInfos != null) {
65 // We assume that this segments_N was previously
66 // properly sync'd:
67 for(int i=0;i<segmentInfos.size();i++) {
68 final SegmentInfo info = segmentInfos.info(i);
69 List files = info.files();
70 for(int j=0;j<files.size();j++)
71 synced.add(files.get(j));
72 }
73 }
74 }
75
76 protected DirectoryIndexReader() {}
77
78 DirectoryIndexReader(Directory directory, SegmentInfos segmentInfos,
79 boolean closeDirectory, boolean readOnly) throws IOException {
80 super();
81 init(directory, segmentInfos, closeDirectory, readOnly);
82 }
83
84 static DirectoryIndexReader open(final Directory directory, final boolean closeDirectory, final IndexDeletionPolicy deletionPolicy) throws CorruptIndexException, IOException {
85 return open(directory, closeDirectory, deletionPolicy, null, false);
86 }
87
88 static DirectoryIndexReader open(final Directory directory, final boolean closeDirectory, final IndexDeletionPolicy deletionPolicy, final IndexCommit commit, final boolean readOnly) throws CorruptIndexException, IOException {
89
90 SegmentInfos.FindSegmentsFile finder = new SegmentInfos.FindSegmentsFile(directory) {
91
92 protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException {
93
94 SegmentInfos infos = new SegmentInfos();
95 infos.read(directory, segmentFileName);
96
97 DirectoryIndexReader reader;
98
99 if (infos.size() == 1) { // index is optimized
100 reader = SegmentReader.get(readOnly, infos, infos.info(0), false);
101 } else if (readOnly) {
102 reader = new ReadOnlyMultiSegmentReader(directory, infos, false);
103 } else {
104 reader = new MultiSegmentReader(directory, infos, false, false);
105 }
106 reader.setDeletionPolicy(deletionPolicy);
107 reader.closeDirectory = closeDirectory;
108 return reader;
109 }
110 };
111
112 DirectoryIndexReader reader = null;
113 try {
114 if (commit == null)
115 reader = (DirectoryIndexReader) finder.run();
116 else {
117 if (directory != commit.getDirectory())
118 throw new IOException("the specified commit does not match the specified Directory");
119 // This can & will directly throw IOException if the
120 // specified commit point has been deleted:
121 reader = (DirectoryIndexReader) finder.doBody(commit.getSegmentsFileName());
122 }
123 } finally {
124 // We passed false above for closeDirectory so that
125 // the directory would not be closed before we were
126 // done retrying, so at this point if we truly failed
127 // to open a reader, which means an exception is being
128 // thrown, then close the directory now:
129 if (reader == null && closeDirectory) {
130 try {
131 directory.close();
132 } catch (IOException ioe) {
133 // suppress, so we keep throwing original failure
134 // from opening the reader
135 }
136 }
137 }
138
139 return reader;
140 }
141
142 public final synchronized IndexReader reopen() throws CorruptIndexException, IOException {
143 ensureOpen();
144
145 if (this.hasChanges || this.isCurrent()) {
146 // this has changes, therefore we have the lock and don't need to reopen
147 // OR: the index in the directory hasn't changed - nothing to do here
148 return this;
149 }
150
151 final SegmentInfos.FindSegmentsFile finder = new SegmentInfos.FindSegmentsFile(directory) {
152
153 protected Object doBody(String segmentFileName) throws CorruptIndexException, IOException {
154 SegmentInfos infos = new SegmentInfos();
155 infos.read(directory, segmentFileName);
156
157 DirectoryIndexReader newReader = doReopen(infos);
158
159 if (DirectoryIndexReader.this != newReader) {
160 newReader.init(directory, infos, closeDirectory, readOnly);
161 newReader.deletionPolicy = deletionPolicy;
162 }
163
164 return newReader;
165 }
166 };
167
168 DirectoryIndexReader reader = null;
169
170 // While trying to reopen, we temporarily mark our
171 // closeDirectory as false. This way any exceptions hit
172 // partway while opening the reader, which is expected
173 // eg if writer is committing, won't close our
174 // directory. We restore this value below:
175 final boolean myCloseDirectory = closeDirectory;
176 closeDirectory = false;
177
178 try {
179 reader = (DirectoryIndexReader) finder.run();
180 } finally {
181 if (myCloseDirectory) {
182 assert directory instanceof FSDirectory;
183 // Restore my closeDirectory
184 closeDirectory = true;
185 if (reader != null && reader != this) {
186 // Success, and a new reader was actually opened
187 reader.closeDirectory = true;
188 // Clone the directory
189 reader.directory = FSDirectory.getDirectory(((FSDirectory) directory).getFile());
190 }
191 }
192 }
193
194 return reader;
195 }
196
197 /**
198 * Re-opens the index using the passed-in SegmentInfos
199 */
200 protected abstract DirectoryIndexReader doReopen(SegmentInfos infos) throws CorruptIndexException, IOException;
201
202 public void setDeletionPolicy(IndexDeletionPolicy deletionPolicy) {
203 this.deletionPolicy = deletionPolicy;
204 }
205
206 /** Returns the directory this index resides in.
207 */
208 public Directory directory() {
209 ensureOpen();
210 return directory;
211 }
212
213 /**
214 * Version number when this IndexReader was opened.
215 */
216 public long getVersion() {
217 ensureOpen();
218 return segmentInfos.getVersion();
219 }
220
221 /**
222 * Check whether this IndexReader is still using the
223 * current (i.e., most recently committed) version of the
224 * index. If a writer has committed any changes to the
225 * index since this reader was opened, this will return
226 * <code>false</code>, in which case you must open a new
227 * IndexReader in order to see the changes. See the
228 * description of the <a href="IndexWriter.html#autoCommit"><code>autoCommit</code></a>
229 * flag which controls when the {@link IndexWriter}
230 * actually commits changes to the index.
231 *
232 * @throws CorruptIndexException if the index is corrupt
233 * @throws IOException if there is a low-level IO error
234 */
235 public boolean isCurrent() throws CorruptIndexException, IOException {
236 ensureOpen();
237 return SegmentInfos.readCurrentVersion(directory) == segmentInfos.getVersion();
238 }
239
240 /**
241 * Checks is the index is optimized (if it has a single segment and no deletions)
242 * @return <code>true</code> if the index is optimized; <code>false</code> otherwise
243 */
244 public boolean isOptimized() {
245 ensureOpen();
246 return segmentInfos.size() == 1 && hasDeletions() == false;
247 }
248
249 protected void doClose() throws IOException {
250 if(closeDirectory)
251 directory.close();
252 }
253
254 /**
255 * Commit changes resulting from delete, undeleteAll, or
256 * setNorm operations
257 *
258 * If an exception is hit, then either no changes or all
259 * changes will have been committed to the index
260 * (transactional semantics).
261 * @throws IOException if there is a low-level IO error
262 */
263 protected void doCommit() throws IOException {
264 if (hasChanges) {
265 if (segmentInfos != null) {
266
267 // Default deleter (for backwards compatibility) is
268 // KeepOnlyLastCommitDeleter:
269 IndexFileDeleter deleter = new IndexFileDeleter(directory,
270 deletionPolicy == null ? new KeepOnlyLastCommitDeletionPolicy() : deletionPolicy,
271 segmentInfos, null, null);
272
273 // Checkpoint the state we are about to change, in
274 // case we have to roll back:
275 startCommit();
276
277 boolean success = false;
278 try {
279 commitChanges();
280
281 // Sync all files we just wrote
282 for(int i=0;i<segmentInfos.size();i++) {
283 final SegmentInfo info = segmentInfos.info(i);
284 final List files = info.files();
285 for(int j=0;j<files.size();j++) {
286 final String fileName = (String) files.get(j);
287 if (!synced.contains(fileName)) {
288 assert directory.fileExists(fileName);
289 directory.sync(fileName);
290 synced.add(fileName);
291 }
292 }
293 }
294
295 segmentInfos.commit(directory);
296 success = true;
297 } finally {
298
299 if (!success) {
300
301 // Rollback changes that were made to
302 // SegmentInfos but failed to get [fully]
303 // committed. This way this reader instance
304 // remains consistent (matched to what's
305 // actually in the index):
306 rollbackCommit();
307
308 // Recompute deletable files & remove them (so
309 // partially written .del files, etc, are
310 // removed):
311 deleter.refresh();
312 }
313 }
314
315 // Have the deleter remove any now unreferenced
316 // files due to this commit:
317 deleter.checkpoint(segmentInfos, true);
318
319 if (writeLock != null) {
320 writeLock.release(); // release write lock
321 writeLock = null;
322 }
323 }
324 else
325 commitChanges();
326 }
327 hasChanges = false;
328 }
329
330 protected abstract void commitChanges() throws IOException;
331
332 /**
333 * Tries to acquire the WriteLock on this directory.
334 * this method is only valid if this IndexReader is directory owner.
335 *
336 * @throws StaleReaderException if the index has changed
337 * since this reader was opened
338 * @throws CorruptIndexException if the index is corrupt
339 * @throws LockObtainFailedException if another writer
340 * has this index open (<code>write.lock</code> could not
341 * be obtained)
342 * @throws IOException if there is a low-level IO error
343 */
344 protected void acquireWriteLock() throws StaleReaderException, CorruptIndexException, LockObtainFailedException, IOException {
345 if (segmentInfos != null) {
346 ensureOpen();
347 if (stale)
348 throw new StaleReaderException("IndexReader out of date and no longer valid for delete, undelete, or setNorm operations");
349
350 if (writeLock == null) {
351 Lock writeLock = directory.makeLock(IndexWriter.WRITE_LOCK_NAME);
352 if (!writeLock.obtain(IndexWriter.WRITE_LOCK_TIMEOUT)) // obtain write lock
353 throw new LockObtainFailedException("Index locked for write: " + writeLock);
354 this.writeLock = writeLock;
355
356 // we have to check whether index has changed since this reader was opened.
357 // if so, this reader is no longer valid for deletion
358 if (SegmentInfos.readCurrentVersion(directory) > segmentInfos.getVersion()) {
359 stale = true;
360 this.writeLock.release();
361 this.writeLock = null;
362 throw new StaleReaderException("IndexReader out of date and no longer valid for delete, undelete, or setNorm operations");
363 }
364 }
365 }
366 }
367
368 /**
369 * Should internally checkpoint state that will change
370 * during commit so that we can rollback if necessary.
371 */
372 void startCommit() {
373 if (segmentInfos != null) {
374 rollbackSegmentInfos = (SegmentInfos) segmentInfos.clone();
375 }
376 rollbackHasChanges = hasChanges;
377 }
378
379 /**
380 * Rolls back state to just before the commit (this is
381 * called by commit() if there is some exception while
382 * committing).
383 */
384 void rollbackCommit() {
385 if (segmentInfos != null) {
386 for(int i=0;i<segmentInfos.size();i++) {
387 // Rollback each segmentInfo. Because the
388 // SegmentReader holds a reference to the
389 // SegmentInfo we can't [easily] just replace
390 // segmentInfos, so we reset it in place instead:
391 segmentInfos.info(i).reset(rollbackSegmentInfos.info(i));
392 }
393 rollbackSegmentInfos = null;
394 }
395
396 hasChanges = rollbackHasChanges;
397 }
398
399 /** Release the write lock, if needed. */
400 protected void finalize() throws Throwable {
401 try {
402 if (writeLock != null) {
403 writeLock.release(); // release write lock
404 writeLock = null;
405 }
406 } finally {
407 super.finalize();
408 }
409 }
410
411 private static class ReaderCommit extends IndexCommit {
412 private String segmentsFileName;
413 Collection files;
414 Directory dir;
415 long generation;
416 long version;
417 final boolean isOptimized;
418
419 ReaderCommit(SegmentInfos infos, Directory dir) throws IOException {
420 segmentsFileName = infos.getCurrentSegmentFileName();
421 this.dir = dir;
422 final int size = infos.size();
423 files = new ArrayList(size);
424 files.add(segmentsFileName);
425 for(int i=0;i<size;i++) {
426 SegmentInfo info = infos.info(i);
427 if (info.dir == dir)
428 files.addAll(info.files());
429 }
430 version = infos.getVersion();
431 generation = infos.getGeneration();
432 isOptimized = infos.size() == 1 && !infos.info(0).hasDeletions();
433 }
434
435 public boolean isOptimized() {
436 return isOptimized;
437 }
438 public String getSegmentsFileName() {
439 return segmentsFileName;
440 }
441 public Collection getFileNames() {
442 return files;
443 }
444 public Directory getDirectory() {
445 return dir;
446 }
447 public long getVersion() {
448 return version;
449 }
450 public long getGeneration() {
451 return generation;
452 }
453 public boolean isDeleted() {
454 return false;
455 }
456 }
457
458 /**
459 * Expert: return the IndexCommit that this reader has
460 * opened.
461 *
462 * <p><b>WARNING</b>: this API is new and experimental and
463 * may suddenly change.</p>
464 */
465 public IndexCommit getIndexCommit() throws IOException {
466 return new ReaderCommit(segmentInfos, directory);
467 }
468
469 /** @see IndexReader#listCommits */
470 public static Collection listCommits(Directory dir) throws IOException {
471
472 final String[] files = dir.list();
473 if (files == null)
474 throw new IOException("cannot read directory " + dir + ": list() returned null");
475
476 Collection commits = new ArrayList();
477
478 SegmentInfos latest = new SegmentInfos();
479 latest.read(dir);
480 final long currentGen = latest.getGeneration();
481
482 commits.add(new ReaderCommit(latest, dir));
483
484 for(int i=0;i<files.length;i++) {
485
486 final String fileName = files[i];
487
488 if (fileName.startsWith(IndexFileNames.SEGMENTS) &&
489 !fileName.equals(IndexFileNames.SEGMENTS_GEN) &&
490 SegmentInfos.generationFromSegmentsFileName(fileName) < currentGen) {
491
492 SegmentInfos sis = new SegmentInfos();
493 try {
494 // IOException allowed to throw there, in case
495 // segments_N is corrupt
496 sis.read(dir, fileName);
497 } catch (FileNotFoundException fnfe) {
498 // LUCENE-948: on NFS (and maybe others), if
499 // you have writers switching back and forth
500 // between machines, it's very likely that the
501 // dir listing will be stale and will claim a
502 // file segments_X exists when in fact it
503 // doesn't. So, we catch this and handle it
504 // as if the file does not exist
505 sis = null;
506 }
507
508 if (sis != null)
509 commits.add(new ReaderCommit(sis, dir));
510 }
511 }
512
513 return commits;
514 }
515 }