Skip to content

Fix: OOM didn't force Teku to quit - #11241

Open
zilm13 wants to merge 10 commits into
Consensys-Incorporated:masterfrom
zilm13:oom-catchall
Open

Fix: OOM didn't force Teku to quit#11241
zilm13 wants to merge 10 commits into
Consensys-Incorporated:masterfrom
zilm13:oom-catchall

Conversation

@zilm13

@zilm13 zilm13 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR Description

Fixed Issue(s)

partially addresses #11225

Documentation

  • I thought about documentation and added the doc-change-required label to this PR if updates are required.

Changelog

  • I thought about adding a changelog entry, and added one if I deemed necessary.

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 instanceof check.

Adds FatalErrorHandler, which treats OOM (including wrapped causes and suppressed errors) as fatal, triggers shutdown once, and exits via a non-blocking path: graceful System.exit on a daemon thread plus a 90s watchdog that Runtime.halts if shutdown hooks wedge (e.g. stuck Jetty teardown).

Wires that handler into SafeFuture completion/error callbacks, TekuDefaultExceptionHandler, REST DefaultExceptionHandler, and ObservableValue / Subscribers when exceptions are suppressed. OOM exits use ERROR_EXIT_CODE (restart-friendly); existing fatal service/storage paths still use FATAL_EXIT_CODE. Gradle dependency rules allow infrastructure:subscribers to depend on exceptions.

Reviewed by Cursor Bugbot for commit 3946f6e. Bugbot is set up for automated code reviews on this repo. Configure here.

@tbenr

tbenr commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

gpt found the issue i was mentioning: #7166

@tbenr tbenr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some points of discussion

Comment on lines +80 to +81
statusLog.fatalError(failedService, exception);
System.exit(FATAL_EXIT_CODE);
FatalErrorHandler.terminate(FATAL_EXIT_CODE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)));

@tbenr tbenr Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
    }
  });
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, updating

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbenr all feedback addressed in d54affc

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3946f6e. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants