mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-25 01:19:02 +00:00
* Fix Infinite Loops in ExceptionsHelper#unwrap * Keep track of all seen exceptions and break out on loops * Closes #42340
This commit is contained in:
parent
39fef8379b
commit
99a44a04f7
server/src
main/java/org/elasticsearch
test/java/org/elasticsearch
@ -38,12 +38,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class ExceptionsHelper {
|
public final class ExceptionsHelper {
|
||||||
@ -185,22 +187,14 @@ public final class ExceptionsHelper {
|
|||||||
* @return Corruption indicating exception if one is found, otherwise {@code null}
|
* @return Corruption indicating exception if one is found, otherwise {@code null}
|
||||||
*/
|
*/
|
||||||
public static IOException unwrapCorruption(Throwable t) {
|
public static IOException unwrapCorruption(Throwable t) {
|
||||||
if (t != null) {
|
return t == null ? null : ExceptionsHelper.<IOException>unwrapCausesAndSuppressed(t, cause -> {
|
||||||
do {
|
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
|
||||||
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
|
if (clazz.isInstance(cause)) {
|
||||||
if (clazz.isInstance(t)) {
|
return true;
|
||||||
return (IOException) t;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (Throwable suppressed : t.getSuppressed()) {
|
}
|
||||||
IOException corruptionException = unwrapCorruption(suppressed);
|
return false;
|
||||||
if (corruptionException != null) {
|
}).orElse(null);
|
||||||
return corruptionException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while ((t = t.getCause()) != null);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -213,7 +207,11 @@ public final class ExceptionsHelper {
|
|||||||
*/
|
*/
|
||||||
public static Throwable unwrap(Throwable t, Class<?>... clazzes) {
|
public static Throwable unwrap(Throwable t, Class<?>... clazzes) {
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
|
final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
do {
|
do {
|
||||||
|
if (seen.add(t) == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
for (Class<?> clazz : clazzes) {
|
for (Class<?> clazz : clazzes) {
|
||||||
if (clazz.isInstance(t)) {
|
if (clazz.isInstance(t)) {
|
||||||
return t;
|
return t;
|
||||||
@ -246,33 +244,22 @@ public final class ExceptionsHelper {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final int MAX_ITERATIONS = 1024;
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T extends Throwable> Optional<T> unwrapCausesAndSuppressed(Throwable cause, Predicate<Throwable> predicate) {
|
||||||
/**
|
if (predicate.test(cause)) {
|
||||||
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
|
return Optional.of((T) cause);
|
||||||
*
|
|
||||||
* @param cause the root throwable
|
|
||||||
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
|
|
||||||
*/
|
|
||||||
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
|
|
||||||
// early terminate if the cause is already an error
|
|
||||||
if (cause instanceof Error) {
|
|
||||||
return Optional.of((Error) cause);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Queue<Throwable> queue = new LinkedList<>();
|
final Queue<Throwable> queue = new LinkedList<>();
|
||||||
queue.add(cause);
|
queue.add(cause);
|
||||||
int iterations = 0;
|
final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
while (queue.isEmpty() == false) {
|
while (queue.isEmpty() == false) {
|
||||||
iterations++;
|
|
||||||
// this is a guard against deeply nested or circular chains of exceptions
|
|
||||||
if (iterations > MAX_ITERATIONS) {
|
|
||||||
logger.warn("giving up looking for fatal errors", cause);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
final Throwable current = queue.remove();
|
final Throwable current = queue.remove();
|
||||||
if (current instanceof Error) {
|
if (seen.add(current) == false) {
|
||||||
return Optional.of((Error) current);
|
continue;
|
||||||
|
}
|
||||||
|
if (predicate.test(current)) {
|
||||||
|
return Optional.of((T) current);
|
||||||
}
|
}
|
||||||
Collections.addAll(queue, current.getSuppressed());
|
Collections.addAll(queue, current.getSuppressed());
|
||||||
if (current.getCause() != null) {
|
if (current.getCause() != null) {
|
||||||
@ -283,21 +270,24 @@ public final class ExceptionsHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See {@link #maybeError(Throwable, Logger)}. Uses the class-local logger.
|
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
|
||||||
|
*
|
||||||
|
* @param cause the root throwable
|
||||||
|
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
|
||||||
*/
|
*/
|
||||||
public static Optional<Error> maybeError(final Throwable cause) {
|
public static Optional<Error> maybeError(final Throwable cause) {
|
||||||
return maybeError(cause, logger);
|
return unwrapCausesAndSuppressed(cause, t -> t instanceof Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be
|
* If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be
|
||||||
* caught and bubbles up to the uncaught exception handler. Note that the cause tree is examined for any {@link Error}. See
|
* caught and bubbles up to the uncaught exception handler. Note that the cause tree is examined for any {@link Error}. See
|
||||||
* {@link #maybeError(Throwable, Logger)} for the semantics.
|
* {@link #maybeError(Throwable)} for the semantics.
|
||||||
*
|
*
|
||||||
* @param throwable the throwable to possibly throw on another thread
|
* @param throwable the throwable to possibly throw on another thread
|
||||||
*/
|
*/
|
||||||
public static void maybeDieOnAnotherThread(final Throwable throwable) {
|
public static void maybeDieOnAnotherThread(final Throwable throwable) {
|
||||||
ExceptionsHelper.maybeError(throwable, logger).ifPresent(error -> {
|
ExceptionsHelper.maybeError(throwable).ifPresent(error -> {
|
||||||
/*
|
/*
|
||||||
* Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, sometimes the stack
|
* Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, sometimes the stack
|
||||||
* contains statements that catch any throwable (e.g., Netty, and the JDK futures framework). This means that a rethrow here
|
* contains statements that catch any throwable (e.g., Netty, and the JDK futures framework). This means that a rethrow here
|
||||||
|
@ -1142,7 +1142,7 @@ public abstract class Engine implements Closeable {
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings("finally")
|
@SuppressWarnings("finally")
|
||||||
private void maybeDie(final String maybeMessage, final Throwable maybeFatal) {
|
private void maybeDie(final String maybeMessage, final Throwable maybeFatal) {
|
||||||
ExceptionsHelper.maybeError(maybeFatal, logger).ifPresent(error -> {
|
ExceptionsHelper.maybeError(maybeFatal).ifPresent(error -> {
|
||||||
try {
|
try {
|
||||||
logger.error(maybeMessage, error);
|
logger.error(maybeMessage, error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -35,9 +35,9 @@ import org.elasticsearch.search.SearchShardTarget;
|
|||||||
import org.elasticsearch.test.ESTestCase;
|
import org.elasticsearch.test.ESTestCase;
|
||||||
import org.elasticsearch.transport.RemoteClusterAware;
|
import org.elasticsearch.transport.RemoteClusterAware;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.elasticsearch.ExceptionsHelper.MAX_ITERATIONS;
|
|
||||||
import static org.elasticsearch.ExceptionsHelper.maybeError;
|
import static org.elasticsearch.ExceptionsHelper.maybeError;
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
@ -81,20 +81,14 @@ public class ExceptionsHelperTests extends ESTestCase {
|
|||||||
if (fatal) {
|
if (fatal) {
|
||||||
assertError(cause, error);
|
assertError(cause, error);
|
||||||
} else {
|
} else {
|
||||||
assertFalse(maybeError(cause, logger).isPresent());
|
assertFalse(maybeError(cause).isPresent());
|
||||||
}
|
}
|
||||||
|
|
||||||
assertFalse(maybeError(new Exception(new DecoderException()), logger).isPresent());
|
assertFalse(maybeError(new Exception(new DecoderException())).isPresent());
|
||||||
|
|
||||||
Throwable chain = outOfMemoryError;
|
|
||||||
for (int i = 0; i < MAX_ITERATIONS; i++) {
|
|
||||||
chain = new Exception(chain);
|
|
||||||
}
|
|
||||||
assertFalse(maybeError(chain, logger).isPresent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertError(final Throwable cause, final Error error) {
|
private void assertError(final Throwable cause, final Error error) {
|
||||||
final Optional<Error> maybeError = maybeError(cause, logger);
|
final Optional<Error> maybeError = maybeError(cause);
|
||||||
assertTrue(maybeError.isPresent());
|
assertTrue(maybeError.isPresent());
|
||||||
assertThat(maybeError.get(), equalTo(error));
|
assertThat(maybeError.get(), equalTo(error));
|
||||||
}
|
}
|
||||||
@ -211,4 +205,29 @@ public class ExceptionsHelperTests extends ESTestCase {
|
|||||||
withSuppressedException.addSuppressed(new RuntimeException());
|
withSuppressedException.addSuppressed(new RuntimeException());
|
||||||
assertThat(ExceptionsHelper.unwrapCorruption(withSuppressedException), nullValue());
|
assertThat(ExceptionsHelper.unwrapCorruption(withSuppressedException), nullValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testSuppressedCycle() {
|
||||||
|
RuntimeException e1 = new RuntimeException();
|
||||||
|
RuntimeException e2 = new RuntimeException();
|
||||||
|
e1.addSuppressed(e2);
|
||||||
|
e2.addSuppressed(e1);
|
||||||
|
ExceptionsHelper.unwrapCorruption(e1);
|
||||||
|
|
||||||
|
final CorruptIndexException corruptIndexException = new CorruptIndexException("corrupt", "resource");
|
||||||
|
RuntimeException e3 = new RuntimeException(corruptIndexException);
|
||||||
|
e3.addSuppressed(e1);
|
||||||
|
assertThat(ExceptionsHelper.unwrapCorruption(e3), equalTo(corruptIndexException));
|
||||||
|
|
||||||
|
RuntimeException e4 = new RuntimeException(e1);
|
||||||
|
e4.addSuppressed(corruptIndexException);
|
||||||
|
assertThat(ExceptionsHelper.unwrapCorruption(e4), equalTo(corruptIndexException));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCauseCycle() {
|
||||||
|
RuntimeException e1 = new RuntimeException();
|
||||||
|
RuntimeException e2 = new RuntimeException(e1);
|
||||||
|
e1.initCause(e2);
|
||||||
|
ExceptionsHelper.unwrap(e1, IOException.class);
|
||||||
|
ExceptionsHelper.unwrapCorruption(e1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user