RELNOTES[INC]: --fatal_event_bus_exceptions is deprecated and should not be used. Any crashes should be reported so that they can be fixed.

Allow Blaze modules to inject a SubscriberExceptionHandler/uncaught exception handler. The provided handler will be used to handle EventBus exceptions as well as any crashes in async threads.

PiperOrigin-RevId: 296124771
diff --git a/src/main/java/com/google/devtools/build/lib/bugreport/BugReport.java b/src/main/java/com/google/devtools/build/lib/bugreport/BugReport.java
index d4cebc7..f79a554 100644
--- a/src/main/java/com/google/devtools/build/lib/bugreport/BugReport.java
+++ b/src/main/java/com/google/devtools/build/lib/bugreport/BugReport.java
@@ -165,11 +165,11 @@
    *
    * <p>Has no effect if another crash has already been handled by {@link BugReport}.
    */
-  public static void handleCrash(Throwable throwable, String... args) {
-    handleCrash(throwable, /*sendBugReport=*/ true, /*exitCode=*/ null, args);
+  public static RuntimeException handleCrash(Throwable throwable, String... args) {
+    throw handleCrash(throwable, /*sendBugReport=*/ true, /*exitCode=*/ null, args);
   }
 
-  private static void handleCrash(
+  private static RuntimeException handleCrash(
       Throwable throwable, boolean sendBugReport, @Nullable ExitCode exitCode, String... args) {
     ExitCode exitCodeToUse = exitCode == null ? getExitCodeForThrowable(throwable) : exitCode;
     int numericExitCode = exitCodeToUse.getNumericExitCode();
@@ -178,7 +178,10 @@
         if (IN_TEST) {
           unprocessedThrowableInTest = throwable;
         }
-        logCrash(throwable, sendBugReport, args);
+        // Don't try to send a bug report during a crash in a test, it will throw itself.
+        if (!IN_TEST || !sendBugReport) {
+          logCrash(throwable, sendBugReport, args);
+        }
         try {
           if (runtime != null) {
             runtime.cleanUpForCrash(exitCodeToUse);
@@ -209,6 +212,7 @@
 
       Runtime.getRuntime().halt(numericExitCode);
     }
+    throw new IllegalStateException("never get here", throwable);
   }
 
   /** Get exit code corresponding to throwable. */
@@ -292,5 +296,10 @@
     public void sendBugReport(Throwable exception, List<String> args, String... values) {
       BugReport.sendBugReport(exception, args, values);
     }
+
+    @Override
+    public RuntimeException handleCrash(Throwable throwable, String... args) {
+      throw BugReport.handleCrash(throwable, args);
+    }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bugreport/BugReporter.java b/src/main/java/com/google/devtools/build/lib/bugreport/BugReporter.java
index 57dca7d..3fc8cea 100644
--- a/src/main/java/com/google/devtools/build/lib/bugreport/BugReporter.java
+++ b/src/main/java/com/google/devtools/build/lib/bugreport/BugReporter.java
@@ -33,4 +33,7 @@
 
   /** Reports an exception, see {@link BugReport#sendBugReport(Throwable, List, String[])}. */
   void sendBugReport(Throwable exception, List<String> args, String... values);
+
+  /** See {@link BugReport#handleCrash}. */
+  RuntimeException handleCrash(Throwable throwable, String... args);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
index 8ec26a8..1c6d635 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeModule.java
@@ -16,6 +16,8 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.SubscriberExceptionContext;
+import com.google.common.eventbus.SubscriberExceptionHandler;
 import com.google.devtools.build.lib.actions.ExecutorInitException;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.BlazeVersionInfo;
@@ -125,6 +127,18 @@
   }
 
   /**
+   * Returns handler for {@link com.google.common.eventbus.EventBus} subscriber and async thread
+   * exceptions. For async thread exceptions, {@link
+   * SubscriberExceptionHandler#handleException(Throwable, SubscriberExceptionContext)} will be
+   * called with null {@link SubscriberExceptionContext}. If all modules return null, a handler that
+   * crashes on all async exceptions and files bug reports for all EventBus subscriber exceptions
+   * will be used.
+   */
+  public SubscriberExceptionHandler getEventBusAndAsyncExceptionHandler() {
+    return null;
+  }
+
+  /**
    * Called when Bazel starts up after {@link #getStartupOptions}, {@link #globalInit}, and {@link
    * #getFileSystem}.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
index 44bec37..fe5e679 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeRuntime.java
@@ -21,7 +21,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.eventbus.EventBus;
-import com.google.common.eventbus.SubscriberExceptionContext;
 import com.google.common.eventbus.SubscriberExceptionHandler;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.Uninterruptibles;
@@ -531,14 +530,11 @@
   }
 
   /**
-   * Hook method called by the BlazeCommandDispatcher prior to the dispatch of
-   * each command.
+   * Hook method called by the BlazeCommandDispatcher prior to the dispatch of each command.
    *
    * @param options The CommonCommandOptions used by every command.
-   * @throws AbruptExitException if this command is unsuitable to be run as specified
    */
-  void beforeCommand(CommandEnvironment env, CommonCommandOptions options)
-      throws AbruptExitException {
+  void beforeCommand(CommandEnvironment env, CommonCommandOptions options) {
     if (options.memoryProfilePath != null) {
       Path memoryProfilePath = env.getWorkingDirectory().getRelative(options.memoryProfilePath);
       MemoryProfiler.instance()
@@ -798,36 +794,11 @@
   }
 
   /**
-   * An EventBus exception handler that will report the exception to a remote server, if a
-   * handler is registered.
-   */
-  public static final class RemoteExceptionHandler implements SubscriberExceptionHandler {
-    static final String FAILURE_MSG = "Failure in EventBus subscriber";
-
-    @Override
-    public void handleException(Throwable exception, SubscriberExceptionContext context) {
-      logger.log(Level.SEVERE, FAILURE_MSG, exception);
-      LoggingUtil.logToRemote(Level.SEVERE, FAILURE_MSG, exception);
-    }
-  }
-
-  /**
-   * An EventBus exception handler that will call BugReport.handleCrash exiting
-   * the current thread.
-   */
-  public static final class BugReportingExceptionHandler implements SubscriberExceptionHandler {
-    @Override
-    public void handleException(Throwable exception, SubscriberExceptionContext context) {
-      BugReport.handleCrash(exception);
-    }
-  }
-
-  /**
    * Main method for the Blaze server startup. Note: This method logs
    * exceptions to remote servers. Do not add this to a unittest.
    */
   public static void main(Iterable<Class<? extends BlazeModule>> moduleClasses, String[] args) {
-    setupUncaughtHandler(args);
+    setupUncaughtHandlerAtStartup(args);
     List<BlazeModule> modules = createModules(moduleClasses);
     // blaze.cc will put --batch first if the user set it.
     if (args.length >= 1 && args[0].equals("--batch")) {
@@ -1275,6 +1246,34 @@
       throw new AbruptExitException(
           "No module set the default hash function.", ExitCode.BLAZE_INTERNAL_ERROR, e);
     }
+
+    SubscriberExceptionHandler currentHandlerValue = null;
+    for (BlazeModule module : blazeModules) {
+      SubscriberExceptionHandler newHandler = module.getEventBusAndAsyncExceptionHandler();
+      if (newHandler != null) {
+        Preconditions.checkState(
+            currentHandlerValue == null, "Two handlers given. Last module: %s", module);
+        currentHandlerValue = newHandler;
+      }
+    }
+    if (currentHandlerValue == null) {
+      if (startupOptions.fatalEventBusExceptions) {
+        currentHandlerValue = (exception, context) -> BugReport.handleCrash(exception);
+      } else {
+        currentHandlerValue =
+            (exception, context) -> {
+              if (context == null) {
+                BugReport.handleCrash(exception);
+              } else {
+                BugReport.sendBugReport(
+                    exception, ImmutableList.of(), "Failure in EventBus subscriber");
+              }
+            };
+      }
+    }
+    SubscriberExceptionHandler subscriberExceptionHandler = currentHandlerValue;
+    Thread.setDefaultUncaughtExceptionHandler(
+        (thread, throwable) -> subscriberExceptionHandler.handleException(throwable, null));
     Path.setFileSystemForSerialization(fs);
     SubprocessBuilder.setDefaultSubprocessFactory(subprocessFactoryImplementation());
 
@@ -1310,13 +1309,7 @@
             .setStartupOptionsProvider(options)
             .setClock(clock)
             .setAbruptShutdownHandler(abruptShutdownHandler)
-            // TODO(bazel-team): Make BugReportingExceptionHandler the default.
-            // See bug "Make exceptions in EventBus subscribers fatal"
-            .setEventBusExceptionHandler(
-                startupOptions.fatalEventBusExceptions
-                        || !BlazeVersionInfo.instance().isReleasedBlaze()
-                    ? new BlazeRuntime.BugReportingExceptionHandler()
-                    : new BlazeRuntime.RemoteExceptionHandler());
+            .setEventBusExceptionHandler(subscriberExceptionHandler);
 
     if (System.getenv("TEST_TMPDIR") != null
         && System.getenv("NO_CRASH_ON_LOGGING_IN_TEST") == null) {
@@ -1426,10 +1419,10 @@
   }
 
   /**
-   * Make sure async threads cannot be orphaned. This method makes sure bugs are reported to
-   * telemetry and the proper exit code is reported.
+   * Make sure async threads cannot be orphaned at startup. This method makes sure bugs are reported
+   * to telemetry and the proper exit code is reported. Will be overwritten with better handler.
    */
-  private static void setupUncaughtHandler(final String[] args) {
+  private static void setupUncaughtHandlerAtStartup(final String[] args) {
     Thread.setDefaultUncaughtExceptionHandler(
         (thread, throwable) -> BugReport.handleCrash(throwable, args));
   }
@@ -1456,8 +1449,8 @@
    * BlazeDirectories}, and the {@link RuleClassProvider} (except for testing). All other fields
    * have safe default values.
    *
-   * <p>The default behavior of the BlazeRuntime's EventBus is to exit when a subscriber throws
-   * an exception. Please plan appropriately.
+   * <p>The default behavior of the BlazeRuntime's EventBus is to exit the JVM when a subscriber
+   * throws an exception. Please plan appropriately.
    */
   public static class Builder {
     private FileSystem fileSystem;
@@ -1466,7 +1459,8 @@
     private Runnable abruptShutdownHandler;
     private OptionsParsingResult startupOptionsProvider;
     private final List<BlazeModule> blazeModules = new ArrayList<>();
-    private SubscriberExceptionHandler eventBusExceptionHandler = new RemoteExceptionHandler();
+    private SubscriberExceptionHandler eventBusExceptionHandler =
+        (throwable, context) -> BugReport.handleCrash(throwable);
     private UUID instanceId;
     private String productName;
     private ActionKeyContext actionKeyContext;
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java
index 16ccd28..b8c65e9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeServerStartupOptions.java
@@ -308,12 +308,12 @@
   public boolean ignoreAllRcFiles;
 
   @Option(
-    name = "fatal_event_bus_exceptions",
-    defaultValue = "false", // NOTE: only for documentation, value is always passed by the client.
-    documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
-    effectTags = {OptionEffectTag.EAGERNESS_TO_EXIT, OptionEffectTag.LOSES_INCREMENTAL_STATE},
-    help = "Whether or not to exit if an exception is thrown by an internal EventBus handler."
-  )
+      name = "fatal_event_bus_exceptions",
+      defaultValue = "false", // NOTE: only for documentation, value is always passed by the client.
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.EAGERNESS_TO_EXIT, OptionEffectTag.LOSES_INCREMENTAL_STATE},
+      deprecationWarning = "Will be enabled by default and removed soon",
+      help = "Whether or not to exit if an exception is thrown by an internal EventBus handler.")
   public boolean fatalEventBusExceptions;
 
   @Option(