Skip to content

Commit

Permalink
Add logging in Exceptions.throwIf[Jvm]Fatal, add isFatal methods
Browse files Browse the repository at this point in the history
This commit improves the discoverability of exceptions that are "fatal"
to Reactor by:
 - adding `Exceptions.isFatal` and `Exceptions.isJvmFatal` methods
 - adding logging to `Exceptions.throwIfFatal` and
 `Exceptions.throwIfJvmFatal` just before a fatal exception is thrown

This should at least help pinpointing such occurrences from the logs,
in cases where the thrown exception bubble all the way up the stack of
a thread with no uncaughtExceptionHandler for example.

Fixes #3111.
  • Loading branch information
simonbasle committed Aug 1, 2022
1 parent 6a89d1f commit 5a2109b
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 17 deletions.
94 changes: 82 additions & 12 deletions reactor-core/src/main/java/reactor/core/Exceptions.java
Expand Up @@ -25,6 +25,8 @@
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

import reactor.core.publisher.Flux;
import reactor.util.Logger;
import reactor.util.Loggers;
import reactor.util.annotation.Nullable;
import reactor.util.retry.Retry;

Expand All @@ -36,6 +38,8 @@
*/
public abstract class Exceptions {

private static final Logger LOGGER = Loggers.getLogger(Exceptions.class);

/**
* A common error message used when a reactive streams source doesn't seem to respect
* backpressure signals, resulting in an operator's internal queue to be full.
Expand Down Expand Up @@ -412,6 +416,70 @@ public static <T> Throwable terminate(AtomicReferenceFieldUpdater<T, Throwable>
return current;
}

/**
* Check if a {@link Throwable} is considered by Reactor as Jvm Fatal and would be thrown
* by both {@link #throwIfFatal(Throwable)} and {@link #throwIfJvmFatal(Throwable)}.
* This is a subset of {@link #isFatal(Throwable)}, namely:
* <ul>
* <li>{@link VirtualMachineError}</li>
* <li>{@link ThreadDeath}</li>
* <li>{@link LinkageError}</li>
* </ul>
* <p>
* Unless wrapped explicitly, such exceptions would always be thrown by operators instead of
* propagation through onError, potentially interrupting progress of Flux/Mono sequences.
* When they occur, the JVM itself is assumed to be in an unrecoverable state, and so is Reactor.
*
* @see #throwIfFatal(Throwable)
* @see #throwIfJvmFatal(Throwable)
* @see #isFatal(Throwable)
* @param t the {@link Throwable} to check
* @return true if the throwable is considered Jvm Fatal
*/
public static boolean isJvmFatal(@Nullable Throwable t) {
if (t instanceof VirtualMachineError ||
t instanceof ThreadDeath ||
t instanceof LinkageError) {
return true;
}
return false;
}

/**
* Check if a {@link Throwable} is considered by Reactor as Fatal and would be thrown by
* {@link #throwIfFatal(Throwable)}.
* <ul>
* <li>{@code BubblingException} (as detectable by {@link #isBubbling(Throwable)})</li>
* <li>{@code ErrorCallbackNotImplemented} (as detectable by {@link #isErrorCallbackNotImplemented(Throwable)})</li>
* <li> {@link #isJvmFatal(Throwable) Jvm Fatal exceptions}
* <ul>
* <li>{@link VirtualMachineError}</li>
* <li>{@link ThreadDeath}</li>
* <li>{@link LinkageError}</li>
* </ul>
* </li>
* </ul>
* <p>
* Unless wrapped explicitly, such exceptions would always be thrown by operators instead of
* propagation through onError, potentially interrupting progress of Flux/Mono sequences.
* When they occur, the assumption is that Reactor is in an unrecoverable state (notably
* because the JVM itself might be in an unrecoverable state).
*
* @see #throwIfFatal(Throwable)
* @see #isJvmFatal(Throwable)
* @param t the {@link Throwable} to check
* @return true if the throwable is considered fatal
*/
public static boolean isFatal(@Nullable Throwable t) {
if (t instanceof BubblingException || t instanceof ErrorCallbackNotImplemented) {
return true;
}
if (isJvmFatal(t)) {
return true;
}
return false;
}

/**
* Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error
* varieties. These varieties are as follows: <ul>
Expand All @@ -422,13 +490,16 @@ public static <T> Throwable terminate(AtomicReferenceFieldUpdater<T, Throwable>
* @param t the exception to evaluate
*/
public static void throwIfFatal(@Nullable Throwable t) {
if (t instanceof BubblingException) {
throw (BubblingException) t;
}
if (t instanceof ErrorCallbackNotImplemented) {
throw (ErrorCallbackNotImplemented) t;
if (t == null) {
return;
}
//we give throwIfJvmFatal an opportunity to detect, log and throw first
throwIfJvmFatal(t);
if (isFatal(t)) {
LOGGER.warn("throwIfFatal detected a fatal exception, throwing", t);
assert t instanceof RuntimeException;
throw (RuntimeException) t;
}
}

/**
Expand All @@ -440,14 +511,13 @@ public static void throwIfFatal(@Nullable Throwable t) {
* @param t the exception to evaluate
*/
public static void throwIfJvmFatal(@Nullable Throwable t) {
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
if (t == null) {
return;
}
if (t instanceof LinkageError) {
throw (LinkageError) t;
if (isJvmFatal(t)) {
LOGGER.warn("throwIfJvmFatal detected a fatal exception, throwing", t);
assert t instanceof Error;
throw (Error) t;
}
}

Expand Down
70 changes: 65 additions & 5 deletions reactor-core/src/test/java/reactor/core/ExceptionsTest.java
Expand Up @@ -156,9 +156,64 @@ public void propagateDoesntWrapRuntimeException() {

//TODO test terminate

@Test
void isFatalNotJvmFatalBubbling() {
Throwable exception = new BubblingException("expected");
assertThat(Exceptions.isFatal(exception))
.as("isFatal(bubbling)")
.isTrue();
assertThat(Exceptions.isJvmFatal(exception))
.as("isJvmFatal(bubbling)")
.isFalse();
}

@Test
void isFatalNotJvmFatalErrorCallback() {
Throwable exception = new ErrorCallbackNotImplemented(new IllegalStateException("expected cause"));
assertThat(Exceptions.isFatal(exception))
.as("isFatal(ErrorCallbackNotImplemented)")
.isTrue();
assertThat(Exceptions.isJvmFatal(exception))
.as("isJvmFatal(ErrorCallbackNotImplemented)")
.isFalse();
}

@Test
void isFatalAndJvmFatalVirtualMachineError() {
Throwable exception = new VirtualMachineError() { };
assertThat(Exceptions.isFatal(exception))
.as("isFatal(VirtualMachineError)")
.isTrue();
assertThat(Exceptions.isJvmFatal(exception))
.as("isJvmFatal(VirtualMachineError)")
.isTrue();
}

@Test
void isFatalAndJvmFatalLinkageError() {
Throwable exception = new LinkageError();
assertThat(Exceptions.isFatal(exception))
.as("isFatal(LinkageError)")
.isTrue();
assertThat(Exceptions.isJvmFatal(exception))
.as("isJvmFatal(LinkageError)")
.isTrue();
}

@Test
void isFatalAndJvmFatalThreadDeath() {
Throwable exception = new ThreadDeath();
assertThat(Exceptions.isFatal(exception))
.as("isFatal(ThreadDeath)")
.isTrue();
assertThat(Exceptions.isJvmFatal(exception))
.as("isJvmFatal(ThreadDeath)")
.isTrue();
}

@Test
public void throwIfFatalThrowsBubbling() {
BubblingException expected = new BubblingException("expected");
BubblingException expected = new BubblingException("expected to be logged");

assertThatExceptionOfType(BubblingException.class)
.isThrownBy(() -> Exceptions.throwIfFatal(expected))
Expand All @@ -167,7 +222,7 @@ public void throwIfFatalThrowsBubbling() {

@Test
public void throwIfFatalThrowsErrorCallbackNotImplemented() {
ErrorCallbackNotImplemented expected = new ErrorCallbackNotImplemented(new IllegalStateException("expected cause"));
ErrorCallbackNotImplemented expected = new ErrorCallbackNotImplemented(new IllegalStateException("expected to be logged"));

assertThatExceptionOfType(ErrorCallbackNotImplemented.class)
.isThrownBy(() -> Exceptions.throwIfFatal(expected))
Expand All @@ -177,9 +232,14 @@ public void throwIfFatalThrowsErrorCallbackNotImplemented() {

@Test
public void throwIfJvmFatal() {
VirtualMachineError fatal1 = new VirtualMachineError() {};
ThreadDeath fatal2 = new ThreadDeath();
LinkageError fatal3 = new LinkageError();
VirtualMachineError fatal1 = new VirtualMachineError("expected to be logged") {};
ThreadDeath fatal2 = new ThreadDeath() {
@Override
public String getMessage() {
return "expected to be logged";
}
};
LinkageError fatal3 = new LinkageError("expected to be logged");

assertThatExceptionOfType(VirtualMachineError.class)
.as("VirtualMachineError")
Expand Down

0 comments on commit 5a2109b

Please sign in to comment.