IndexWriter: Treat java.lang.Error as tragedy (#13277)

Background:
Historically IndexWriter treated OutOfMemoryError special, for defensive
reasons. It was expanded to VirtualMachineError, to try to play it safe
in similar disastrous circumstances.

We should treat any Error as a tragedy, as it isn't an Exception, and it
isn't something a "reasonable" application should catch. IndexWriter
should be reasonable. See #7049 for some of the reasoning.

We can't pretend this will detect any possible scenario that might cause
harm, e.g. a jvm bug might simply miscompile some code and cause silent
corruption. But we should try harder by playing by the rules.

Closes #13275
Closes #7049
This commit is contained in:
Robert Muir 2024-04-23 21:40:22 -04:00 committed by GitHub
parent fb12e09ab5
commit 9af3ef8952
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 64 additions and 34 deletions

View File

@ -183,6 +183,8 @@ Changes in Runtime Behavior
* GITHUB#13293: ConcurrentMergeScheduler now allows up to 50% of the threads of the host to be used * GITHUB#13293: ConcurrentMergeScheduler now allows up to 50% of the threads of the host to be used
for merging. (Adrien Grand) for merging. (Adrien Grand)
* GITHUB#13277: IndexWriter treats any java.lang.Error as tragic. (Robert Muir)
Changes in Backwards Compatibility Policy Changes in Backwards Compatibility Policy
----------------------------------------- -----------------------------------------

View File

@ -153,9 +153,9 @@ import org.apache.lucene.util.Version;
* and it decides when and how to run the merges. The default is {@link ConcurrentMergeScheduler}. * and it decides when and how to run the merges. The default is {@link ConcurrentMergeScheduler}.
* <a id="OOME"></a> * <a id="OOME"></a>
* *
* <p><b>NOTE</b>: if you hit a VirtualMachineError, or disaster strikes during a checkpoint then * <p><b>NOTE</b>: if you hit an Error, or disaster strikes during a checkpoint then IndexWriter
* IndexWriter will close itself. This is a defensive measure in case any internal state (buffered * will close itself. This is a defensive measure in case any internal state (buffered documents,
* documents, deletions, reference counts) were corrupted. Any subsequent calls will throw an * deletions, reference counts) were corrupted. Any subsequent calls will throw an
* AlreadyClosedException. <a id="thread-safety"></a> * AlreadyClosedException. <a id="thread-safety"></a>
* *
* <p><b>NOTE</b>: {@link IndexWriter} instances are completely thread safe, meaning multiple * <p><b>NOTE</b>: {@link IndexWriter} instances are completely thread safe, meaning multiple
@ -704,7 +704,7 @@ public class IndexWriter
infoStream.message("IW", "getReader took " + (System.currentTimeMillis() - tStart) + " ms"); infoStream.message("IW", "getReader took " + (System.currentTimeMillis() - tStart) + " ms");
} }
success2 = true; success2 = true;
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "getReader"); tragicEvent(tragedy, "getReader");
throw tragedy; throw tragedy;
} finally { } finally {
@ -1549,7 +1549,7 @@ public class IndexWriter
final long seqNo = maybeProcessEvents(docWriter.updateDocuments(docs, delNode)); final long seqNo = maybeProcessEvents(docWriter.updateDocuments(docs, delNode));
success = true; success = true;
return seqNo; return seqNo;
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "updateDocuments"); tragicEvent(tragedy, "updateDocuments");
throw tragedy; throw tragedy;
} finally { } finally {
@ -1786,7 +1786,7 @@ public class IndexWriter
ensureOpen(); ensureOpen();
try { try {
return maybeProcessEvents(docWriter.deleteTerms(terms)); return maybeProcessEvents(docWriter.deleteTerms(terms));
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "deleteDocuments(Term..)"); tragicEvent(tragedy, "deleteDocuments(Term..)");
throw tragedy; throw tragedy;
} }
@ -1813,7 +1813,7 @@ public class IndexWriter
try { try {
return maybeProcessEvents(docWriter.deleteQueries(queries)); return maybeProcessEvents(docWriter.deleteQueries(queries));
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "deleteDocuments(Query..)"); tragicEvent(tragedy, "deleteDocuments(Query..)");
throw tragedy; throw tragedy;
} }
@ -1892,7 +1892,7 @@ public class IndexWriter
try { try {
return maybeProcessEvents( return maybeProcessEvents(
docWriter.updateDocValues(new NumericDocValuesUpdate(term, field, value))); docWriter.updateDocValues(new NumericDocValuesUpdate(term, field, value)));
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "updateNumericDocValue"); tragicEvent(tragedy, "updateNumericDocValue");
throw tragedy; throw tragedy;
} }
@ -1922,7 +1922,7 @@ public class IndexWriter
try { try {
return maybeProcessEvents( return maybeProcessEvents(
docWriter.updateDocValues(new BinaryDocValuesUpdate(term, field, value))); docWriter.updateDocValues(new BinaryDocValuesUpdate(term, field, value)));
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "updateBinaryDocValue"); tragicEvent(tragedy, "updateBinaryDocValue");
throw tragedy; throw tragedy;
} }
@ -1944,7 +1944,7 @@ public class IndexWriter
DocValuesUpdate[] dvUpdates = buildDocValuesUpdate(term, updates); DocValuesUpdate[] dvUpdates = buildDocValuesUpdate(term, updates);
try { try {
return maybeProcessEvents(docWriter.updateDocValues(dvUpdates)); return maybeProcessEvents(docWriter.updateDocValues(dvUpdates));
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "updateDocValues"); tragicEvent(tragedy, "updateDocValues");
throw tragedy; throw tragedy;
} }
@ -2577,7 +2577,7 @@ public class IndexWriter
} catch (Throwable t) { } catch (Throwable t) {
throwable.addSuppressed(t); throwable.addSuppressed(t);
} finally { } finally {
if (throwable instanceof VirtualMachineError) { if (throwable instanceof Error) {
try { try {
tragicEvent(throwable, "rollbackInternal"); tragicEvent(throwable, "rollbackInternal");
} catch (Throwable t1) { } catch (Throwable t1) {
@ -2678,7 +2678,7 @@ public class IndexWriter
} }
} }
} }
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "deleteAll"); tragicEvent(tragedy, "deleteAll");
throw tragedy; throw tragedy;
} }
@ -3098,7 +3098,7 @@ public class IndexWriter
successTop = true; successTop = true;
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "addIndexes(Directory...)"); tragicEvent(tragedy, "addIndexes(Directory...)");
throw tragedy; throw tragedy;
} finally { } finally {
@ -3270,7 +3270,7 @@ public class IndexWriter
throw new RuntimeException( throw new RuntimeException(
"failed to successfully merge all provided readers in addIndexes(CodecReader...)"); "failed to successfully merge all provided readers in addIndexes(CodecReader...)");
} }
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "addIndexes(CodecReader...)"); tragicEvent(tragedy, "addIndexes(CodecReader...)");
throw tragedy; throw tragedy;
} }
@ -3623,7 +3623,7 @@ public class IndexWriter
return true; // we wrote a segment return true; // we wrote a segment
} }
return false; return false;
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "flushNextBuffer"); tragicEvent(tragedy, "flushNextBuffer");
throw tragedy; throw tragedy;
} finally { } finally {
@ -3739,7 +3739,7 @@ public class IndexWriter
doAfterFlush(); doAfterFlush();
} }
} }
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "prepareCommit"); tragicEvent(tragedy, "prepareCommit");
throw tragedy; throw tragedy;
} finally { } finally {
@ -4299,7 +4299,7 @@ public class IndexWriter
success = true; success = true;
return anyChanges; return anyChanges;
} }
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "doFlush"); tragicEvent(tragedy, "doFlush");
throw tragedy; throw tragedy;
} finally { } finally {
@ -5693,7 +5693,7 @@ public class IndexWriter
segmentInfos.updateGeneration(toSync); segmentInfos.updateGeneration(toSync);
} }
} }
} catch (VirtualMachineError tragedy) { } catch (Error tragedy) {
tragicEvent(tragedy, "startCommit"); tragicEvent(tragedy, "startCommit");
throw tragedy; throw tragedy;
} }

View File

@ -105,21 +105,20 @@ public final class IOUtils {
} }
/** /**
* Closes all given <code>Closeable</code>s, suppressing all thrown non {@link * Closes all given <code>Closeable</code>s, suppressing all thrown non {@link Error} exceptions.
* VirtualMachineError} exceptions. Even if a {@link VirtualMachineError} is thrown all given * Even if a {@link Error} is thrown all given closeable are closed.
* closeable are closed.
* *
* @see #closeWhileHandlingException(Closeable...) * @see #closeWhileHandlingException(Closeable...)
*/ */
public static void closeWhileHandlingException(Iterable<? extends Closeable> objects) { public static void closeWhileHandlingException(Iterable<? extends Closeable> objects) {
VirtualMachineError firstError = null; Error firstError = null;
Throwable firstThrowable = null; Throwable firstThrowable = null;
for (Closeable object : objects) { for (Closeable object : objects) {
try { try {
if (object != null) { if (object != null) {
object.close(); object.close();
} }
} catch (VirtualMachineError e) { } catch (Error e) {
firstError = useOrSuppress(firstError, e); firstError = useOrSuppress(firstError, e);
} catch (Throwable t) { } catch (Throwable t) {
firstThrowable = useOrSuppress(firstThrowable, t); firstThrowable = useOrSuppress(firstThrowable, t);
@ -128,7 +127,7 @@ public final class IOUtils {
if (firstError != null) { if (firstError != null) {
// we ensure that we bubble up any errors. We can't recover from these but need to make sure // we ensure that we bubble up any errors. We can't recover from these but need to make sure
// they are // they are
// bubbled up. if a non-VMError is thrown we also add the suppressed exceptions to it. // bubbled up. if a non-Error is thrown we also add the suppressed exceptions to it.
if (firstThrowable != null) { if (firstThrowable != null) {
firstError.addSuppressed(firstThrowable); firstError.addSuppressed(firstThrowable);
} }

View File

@ -19,6 +19,7 @@ package org.apache.lucene.index;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOError;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.Arrays; import java.util.Arrays;
@ -53,7 +54,7 @@ import org.apache.lucene.util.IOUtils;
* index corruption is ever created. * index corruption is ever created.
*/ */
@SuppressCodecs("SimpleText") @SuppressCodecs("SimpleText")
public class TestIndexWriterOnVMError extends LuceneTestCase { public class TestIndexWriterOnError extends LuceneTestCase {
// just one thread, serial merge policy, hopefully debuggable // just one thread, serial merge policy, hopefully debuggable
private void doTest(MockDirectoryWrapper.Failure failOn) throws Exception { private void doTest(MockDirectoryWrapper.Failure failOn) throws Exception {
@ -151,7 +152,7 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
"dv2", "dv2",
new BytesRef(Integer.toString(i + 1))); new BytesRef(Integer.toString(i + 1)));
} }
} catch (VirtualMachineError | AlreadyClosedException disaster) { } catch (Error | AlreadyClosedException disaster) {
getTragedy(disaster, iw, exceptionStream); getTragedy(disaster, iw, exceptionStream);
continue STARTOVER; continue STARTOVER;
} }
@ -174,7 +175,7 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
iw.deleteDocuments( iw.deleteDocuments(
new Term("id", Integer.toString(i)), new Term("id", Integer.toString(-i))); new Term("id", Integer.toString(i)), new Term("id", Integer.toString(-i)));
} }
} catch (VirtualMachineError | AlreadyClosedException disaster) { } catch (Error | AlreadyClosedException disaster) {
getTragedy(disaster, iw, exceptionStream); getTragedy(disaster, iw, exceptionStream);
continue STARTOVER; continue STARTOVER;
} }
@ -197,7 +198,7 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
if (DirectoryReader.indexExists(dir)) { if (DirectoryReader.indexExists(dir)) {
TestUtil.checkIndex(dir); TestUtil.checkIndex(dir);
} }
} catch (VirtualMachineError | AlreadyClosedException disaster) { } catch (Error | AlreadyClosedException disaster) {
getTragedy(disaster, iw, exceptionStream); getTragedy(disaster, iw, exceptionStream);
continue STARTOVER; continue STARTOVER;
} }
@ -206,7 +207,7 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
try { try {
iw.close(); iw.close();
} catch (VirtualMachineError | AlreadyClosedException disaster) { } catch (Error | AlreadyClosedException disaster) {
getTragedy(disaster, iw, exceptionStream); getTragedy(disaster, iw, exceptionStream);
continue STARTOVER; continue STARTOVER;
} }
@ -225,15 +226,13 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
} }
} }
private VirtualMachineError getTragedy(Throwable disaster, IndexWriter writer, PrintStream log) { private Error getTragedy(Throwable disaster, IndexWriter writer, PrintStream log) {
Throwable e = disaster; Throwable e = disaster;
if (e instanceof AlreadyClosedException) { if (e instanceof AlreadyClosedException) {
e = e.getCause(); e = e.getCause();
} }
if (e instanceof VirtualMachineError if (e instanceof Error && e.getMessage() != null && e.getMessage().contains("Fake")) {
&& e.getMessage() != null
&& e.getMessage().startsWith("Fake")) {
log.println("\nTEST: got expected fake exc:" + e.getMessage()); log.println("\nTEST: got expected fake exc:" + e.getMessage());
e.printStackTrace(log); e.printStackTrace(log);
// TODO: remove rollback here, and add this assert to ensure "full OOM protection" anywhere IW // TODO: remove rollback here, and add this assert to ensure "full OOM protection" anywhere IW
@ -244,7 +243,7 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(log); t.printStackTrace(log);
} }
return (VirtualMachineError) e; return (Error) e;
} else { } else {
Rethrow.rethrow(disaster); Rethrow.rethrow(disaster);
return null; // dead return null; // dead
@ -281,6 +280,36 @@ public class TestIndexWriterOnVMError extends LuceneTestCase {
}); });
} }
public void testLinkageError() throws Exception {
final Random r = new Random(random().nextLong());
doTest(
new Failure() {
@Override
public void eval(MockDirectoryWrapper dir) throws IOException {
if (r.nextInt(3000) == 0) {
if (callStackContains(IndexWriter.class)) {
throw new LinkageError("Fake LinkageError");
}
}
}
});
}
public void testIOError() throws Exception {
final Random r = new Random(random().nextLong());
doTest(
new Failure() {
@Override
public void eval(MockDirectoryWrapper dir) throws IOException {
if (r.nextInt(3000) == 0) {
if (callStackContains(IndexWriter.class)) {
throw new IOError(new RuntimeException("Fake IOError"));
}
}
}
});
}
@Nightly @Nightly
public void testCheckpoint() throws Exception { public void testCheckpoint() throws Exception {
final Random r = new Random(random().nextLong()); final Random r = new Random(random().nextLong());