LUCENE-5919: fix IndexWriter exception handling when calling IndexFileDeleter.decRef

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1622279 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael McCandless 2014-09-03 15:26:03 +00:00
parent fbb8dc76e3
commit c84af3d08a
4 changed files with 171 additions and 24 deletions

View File

@ -136,6 +136,11 @@ Bug Fixes
tests (and test iterations). Fix for FaultyIndexInput in particular. tests (and test iterations). Fix for FaultyIndexInput in particular.
(Dawid Weiss) (Dawid Weiss)
* LUCENE-5919: Fix exception handling inside IndexWriter when
deleteFile throws an exception, to not over-decRef index files,
possibly deleting a file that's still in use in the index, leading
to corruption. (Mike McCandless)
Build Build
* LUCENE-5909: Smoke tester now has better command line parsing and * LUCENE-5909: Smoke tester now has better command line parsing and

View File

@ -33,6 +33,7 @@ import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory; import org.apache.lucene.store.Directory;
import org.apache.lucene.store.NoSuchDirectoryException; import org.apache.lucene.store.NoSuchDirectoryException;
import org.apache.lucene.util.CollectionUtil; import org.apache.lucene.util.CollectionUtil;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.InfoStream; import org.apache.lucene.util.InfoStream;
/* /*
@ -367,17 +368,25 @@ final class IndexFileDeleter implements Closeable {
// First decref all files that had been referred to by // First decref all files that had been referred to by
// the now-deleted commits: // the now-deleted commits:
Throwable firstThrowable = null;
for(int i=0;i<size;i++) { for(int i=0;i<size;i++) {
CommitPoint commit = commitsToDelete.get(i); CommitPoint commit = commitsToDelete.get(i);
if (infoStream.isEnabled("IFD")) { if (infoStream.isEnabled("IFD")) {
infoStream.message("IFD", "deleteCommits: now decRef commit \"" + commit.getSegmentsFileName() + "\""); infoStream.message("IFD", "deleteCommits: now decRef commit \"" + commit.getSegmentsFileName() + "\"");
} }
for (final String file : commit.files) { try {
decRef(file); decRef(commit.files);
} catch (Throwable t) {
if (firstThrowable == null) {
firstThrowable = t;
}
} }
} }
commitsToDelete.clear(); commitsToDelete.clear();
// NOTE: does nothing if firstThrowable is null
IOUtils.reThrow(firstThrowable);
// Now compact commits to remove deleted ones (preserving the sort): // Now compact commits to remove deleted ones (preserving the sort):
size = commits.size(); size = commits.size();
int readFrom = 0; int readFrom = 0;
@ -456,8 +465,11 @@ final class IndexFileDeleter implements Closeable {
assert locked(); assert locked();
if (!lastFiles.isEmpty()) { if (!lastFiles.isEmpty()) {
decRef(lastFiles); try {
lastFiles.clear(); decRef(lastFiles);
} finally {
lastFiles.clear();
}
} }
deletePendingFiles(); deletePendingFiles();
@ -554,8 +566,11 @@ final class IndexFileDeleter implements Closeable {
deleteCommits(); deleteCommits();
} else { } else {
// DecRef old files from the last checkpoint, if any: // DecRef old files from the last checkpoint, if any:
decRef(lastFiles); try {
lastFiles.clear(); decRef(lastFiles);
} finally {
lastFiles.clear();
}
// Save files so we can decr on next checkpoint/commit: // Save files so we can decr on next checkpoint/commit:
lastFiles.addAll(segmentInfos.files(directory, false)); lastFiles.addAll(segmentInfos.files(directory, false));
@ -593,10 +608,34 @@ final class IndexFileDeleter implements Closeable {
rc.IncRef(); rc.IncRef();
} }
/** Decrefs all provided files, even on exception; throws first exception hit, if any. */
void decRef(Collection<String> files) throws IOException { void decRef(Collection<String> files) throws IOException {
assert locked(); assert locked();
Throwable firstThrowable = null;
for(final String file : files) { for(final String file : files) {
decRef(file); try {
decRef(file);
} catch (Throwable t) {
if (firstThrowable == null) {
// Save first exception and throw it in the end, but be sure to finish decRef all files
firstThrowable = t;
}
}
}
// NOTE: does nothing if firstThrowable is null
IOUtils.reThrow(firstThrowable);
}
/** Decrefs all provided files, ignoring any exceptions hit; call this if
* you are already handling an exception. */
void decRefWhileHandlingException(Collection<String> files) throws IOException {
assert locked();
for(final String file : files) {
try {
decRef(file);
} catch (Throwable t) {
}
} }
} }
@ -611,16 +650,17 @@ final class IndexFileDeleter implements Closeable {
if (0 == rc.DecRef()) { if (0 == rc.DecRef()) {
// This file is no longer referenced by any past // This file is no longer referenced by any past
// commit points nor by the in-memory SegmentInfos: // commit points nor by the in-memory SegmentInfos:
deleteFile(fileName); try {
refCounts.remove(fileName); deleteFile(fileName);
} finally {
refCounts.remove(fileName);
}
} }
} }
void decRef(SegmentInfos segmentInfos) throws IOException { void decRef(SegmentInfos segmentInfos) throws IOException {
assert locked(); assert locked();
for (final String file : segmentInfos.files(directory, false)) { decRef(segmentInfos.files(directory, false));
decRef(file);
}
} }
public boolean exists(String fileName) { public boolean exists(String fileName) {

View File

@ -2019,9 +2019,12 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
if (pendingCommit != null) { if (pendingCommit != null) {
pendingCommit.rollbackCommit(directory); pendingCommit.rollbackCommit(directory);
deleter.decRef(pendingCommit); try {
pendingCommit = null; deleter.decRef(pendingCommit);
notifyAll(); } finally {
pendingCommit = null;
notifyAll();
}
} }
// Don't bother saving any changes in our segmentInfos // Don't bother saving any changes in our segmentInfos
@ -2074,7 +2077,9 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
try { try {
pendingCommit.rollbackCommit(directory); pendingCommit.rollbackCommit(directory);
deleter.decRef(pendingCommit); deleter.decRef(pendingCommit);
} catch (Throwable t) {} } catch (Throwable t) {
}
pendingCommit = null;
} }
// close all the closeables we can (but important is readerPool and writeLock to prevent leaks) // close all the closeables we can (but important is readerPool and writeLock to prevent leaks)
@ -2848,7 +2853,7 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
if (!success) { if (!success) {
synchronized (this) { synchronized (this) {
if (filesToCommit != null) { if (filesToCommit != null) {
deleter.decRef(filesToCommit); deleter.decRefWhileHandlingException(filesToCommit);
filesToCommit = null; filesToCommit = null;
} }
} }
@ -2979,10 +2984,13 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
deleter.checkpoint(pendingCommit, true); deleter.checkpoint(pendingCommit, true);
} finally { } finally {
// Matches the incRef done in prepareCommit: // Matches the incRef done in prepareCommit:
deleter.decRef(filesToCommit); try {
filesToCommit = null; deleter.decRef(filesToCommit);
pendingCommit = null; } finally {
notifyAll(); filesToCommit = null;
pendingCommit = null;
notifyAll();
}
} }
if (infoStream.isEnabled("IW")) { if (infoStream.isEnabled("IW")) {
@ -4291,8 +4299,11 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
if (infoStream.isEnabled("IW")) { if (infoStream.isEnabled("IW")) {
infoStream.message("IW", " skip startCommit(): no changes pending"); infoStream.message("IW", " skip startCommit(): no changes pending");
} }
deleter.decRef(filesToCommit); try {
filesToCommit = null; deleter.decRef(filesToCommit);
} finally {
filesToCommit = null;
}
return; return;
} }
@ -4363,7 +4374,7 @@ public class IndexWriter implements Closeable, TwoPhaseCommit, Accountable {
} }
// Hit exception // Hit exception
deleter.decRef(filesToCommit); deleter.decRefWhileHandlingException(filesToCommit);
filesToCommit = null; filesToCommit = null;
} }
} }

View File

@ -398,4 +398,95 @@ public class TestIndexFileDeleter extends LuceneTestCase {
dir.close(); dir.close();
} }
// LUCENE-5919
public void testExcInDecRef() throws Exception {
MockDirectoryWrapper dir = newMockDirectory();
// disable slow things: we don't rely upon sleeps here.
dir.setThrottling(MockDirectoryWrapper.Throttling.NEVER);
dir.setUseSlowOpenClosers(false);
final AtomicBoolean doFailExc = new AtomicBoolean();
dir.failOn(new MockDirectoryWrapper.Failure() {
@Override
public void eval(MockDirectoryWrapper dir) throws IOException {
if (doFailExc.get() && random().nextInt(4) == 1) {
Exception e = new Exception();
StackTraceElement stack[] = e.getStackTrace();
for (int i = 0; i < stack.length; i++) {
if (stack[i].getClassName().equals(IndexFileDeleter.class.getName()) && stack[i].getMethodName().equals("decRef")) {
throw new RuntimeException("fake fail");
}
}
}
}
});
IndexWriterConfig iwc = newIndexWriterConfig(new MockAnalyzer(random()));
//iwc.setMergeScheduler(new SerialMergeScheduler());
MergeScheduler ms = iwc.getMergeScheduler();
if (ms instanceof ConcurrentMergeScheduler) {
final ConcurrentMergeScheduler suppressFakeFail = new ConcurrentMergeScheduler() {
@Override
protected void handleMergeException(Throwable exc) {
// suppress only FakeIOException:
if (exc instanceof RuntimeException && exc.getMessage().equals("fake fail")) {
// ok to ignore
} else {
super.handleMergeException(exc);
}
}
};
final ConcurrentMergeScheduler cms = (ConcurrentMergeScheduler) ms;
suppressFakeFail.setMaxMergesAndThreads(cms.getMaxMergeCount(), cms.getMaxThreadCount());
suppressFakeFail.setMergeThreadPriority(cms.getMergeThreadPriority());
iwc.setMergeScheduler(suppressFakeFail);
}
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
// Since we hit exc during merging, a partial
// forceMerge can easily return when there are still
// too many segments in the index:
w.setDoRandomForceMergeAssert(false);
doFailExc.set(true);
int ITERS = atLeast(1000);
for(int iter=0;iter<ITERS;iter++) {
try {
if (random().nextInt(10) == 5) {
w.commit();
} else if (random().nextInt(10) == 7) {
w.getReader().close();
} else {
Document doc = new Document();
doc.add(newTextField("field", "some text", Field.Store.NO));
w.addDocument(doc);
}
} catch (IOException ioe) {
if (ioe.getMessage().contains("background merge hit exception")) {
Throwable cause = ioe.getCause();
if (cause != null && cause instanceof RuntimeException && ((RuntimeException) cause).getMessage().equals("fake fail")) {
// ok
} else {
throw ioe;
}
} else {
throw ioe;
}
} catch (RuntimeException re) {
if (re.getMessage().equals("fake fail")) {
// ok
} else {
throw re;
}
}
}
doFailExc.set(false);
w.close();
dir.close();
}
} }