Fix: OOM didn't force Teku to quit - #11241
Conversation
|
gpt found the issue i was mentioning: #7166 |
| statusLog.fatalError(failedService, exception); | ||
| System.exit(FATAL_EXIT_CODE); | ||
| FatalErrorHandler.terminate(FATAL_EXIT_CODE); |
There was a problem hiding this comment.
in this context we need to be conservative: logging may throw too.
So:
try {
statusLog.fatalError(subscriberDescription, exception);
} catch (final Throwable t) {
System.err.println("Failed to log fatal error in " + subscriberDescription);
} finally {
FatalErrorHandler.terminate(ERROR_EXIT_CODE);
}| public SafeFuture<T> whenComplete(final BiConsumer<? super T, ? super Throwable> action) { | ||
| return (SafeFuture<T>) super.whenComplete(action); | ||
| return (SafeFuture<T>) | ||
| super.whenComplete((result, error) -> action.accept(result, checkForFatalError(error))); |
There was a problem hiding this comment.
problem is that if the action itself throws it is possible we don't intercept that.
seems like all patterns following the functions\consumers should be wrapped in a try\catch
@Override
public SafeFuture<T> whenComplete(final BiConsumer<? super T, ? super Throwable> action) {
return (SafeFuture<T>)
super.whenComplete(
(value, error) -> {
final Throwable checked = checkForFatalError(error);
try {
action.accept(value, checked);
} catch (final Throwable t) {
checkForFatalError(t);
throw t;
}
});
}Then apply the same try/catch + checkForFatalError(t) pattern to exceptionally, handle, and handleAsync, and switch whenException / whenSuccess to go through whenComplete.
Also add checkForFatalError(t); before result.completeExceptionally(t) in handleComposed.
gpt is also suggesting to have something like:
public final class FatalErrorHandler {
public static void runGuarded(final String context, final Runnable action) {
try {
action.run();
} catch (final Throwable t) {
shutdownIfFatalError(t, context);
throw t;
}
}
public static <T> T callGuarded(final String context, final Supplier<T> action) {
try {
return action.get();
} catch (final Throwable t) {
shutdownIfFatalError(t, context);
throw t;
}
}
}and then use that like:
public SafeFuture<T> whenSuccess(final Runnable action) {
return whenComplete((value, error) -> {
if (error == null) {
FatalErrorHandler.runGuarded("SafeFuture.whenSuccess", action);
}
});
}There was a problem hiding this comment.
makes sense, updating
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3946f6e. Configure here.
| } finally { | ||
| processTerminator.terminate(ExitConstants.ERROR_EXIT_CODE, GRACEFUL_SHUTDOWN_TIMEOUT); | ||
| } | ||
| } |
There was a problem hiding this comment.
OOM check overrides fatal exit code
Medium Severity
SafeFuture now calls shutdownIfFatalError on every exceptional completion, and shutdown always terminates with ERROR_EXIT_CODE. When an OutOfMemoryError is wrapped in FatalServiceFailureException or an unrecoverable DatabaseStorageException, that one-shot shutdown fires first, so TekuDefaultExceptionHandler cannot apply the intended FATAL_EXIT_CODE.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 3946f6e. Configure here.


PR Description
Fixed Issue(s)
partially addresses #11225
Documentation
doc-change-requiredlabel to this PR if updates are required.Changelog
Note
Medium Risk
Changes global process termination and async error handling; incorrect detection could exit on non-OOM errors or miss edge cases, though behavior is heavily tested and shutdown is idempotent.
Overview
Fixes a case where Teku could keep running after heap exhaustion because OutOfMemoryError was swallowed by async futures, suppressed subscriber/REST callbacks, or only detected via a direct
instanceofcheck.Adds
FatalErrorHandler, which treats OOM (including wrapped causes and suppressed errors) as fatal, triggers shutdown once, and exits via a non-blocking path: gracefulSystem.exiton a daemon thread plus a 90s watchdog thatRuntime.halts if shutdown hooks wedge (e.g. stuck Jetty teardown).Wires that handler into
SafeFuturecompletion/error callbacks,TekuDefaultExceptionHandler, RESTDefaultExceptionHandler, andObservableValue/Subscriberswhen exceptions are suppressed. OOM exits useERROR_EXIT_CODE(restart-friendly); existing fatal service/storage paths still useFATAL_EXIT_CODE. Gradle dependency rules allowinfrastructure:subscribersto depend onexceptions.Reviewed by Cursor Bugbot for commit 3946f6e. Bugbot is set up for automated code reviews on this repo. Configure here.