Clean up bug reports for unexpected exceptions and do some general clean-up of SkyframeErrorProcessor. Severity of most unexpected exceptions is reduced to "logUnexpected". However, places where we encounter undetailed exceptions have been promoted to "sendBugReport" since they indicate a failure to attribute responsibility, which could affect downstream systems.

PiperOrigin-RevId: 438867002
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 6fbb571..d184f44 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
@@ -135,6 +135,20 @@
     }
   }
 
+  /** See {@link #logUnexpected(String, Object...)}. */
+  @FormatMethod
+  public static void logUnexpected(Exception e, @FormatString String message, Object... args) {
+    if (SHOULD_NOT_SEND_BUG_REPORT_BECAUSE_IN_TEST) {
+      sendBugReport(new IllegalStateException(String.format(message, args), e));
+    } else {
+      logger
+          .atWarning()
+          .atMostEvery(50, MILLISECONDS)
+          .withCause(e)
+          .logVarargs("Unexpected state: " + message, args);
+    }
+  }
+
   /**
    * Convenience method for {@link #sendBugReport(Throwable)}, sending a bug report with a default
    * {@link IllegalStateException}.
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 a91d10b..cb7e22a 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
@@ -37,6 +37,12 @@
     BugReport.logUnexpected(message, args);
   }
 
+  /** See {@link BugReport#logUnexpected}. */
+  @FormatMethod
+  default void logUnexpected(Exception e, @FormatString String message, Object... args) {
+    BugReport.logUnexpected(e, message, args);
+  }
+
   /** Reports an exception, see {@link BugReport#sendBugReport(String, Object...)}. */
   @FormatMethod
   default void sendBugReport(@FormatString String message, Object... args) {
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
index e24c049..957a74a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -1302,11 +1302,9 @@
       Label actionLabel,
       BugReporter bugReporter) {
     if (!input.isSourceArtifact()) {
-      bugReporter.sendBugReport(
-          new IllegalStateException(
-              String.format(
-                  "Unexpected exit code %s for generated artifact %s (%s)",
-                  detailedExitCode, input, actionLabel)));
+      bugReporter.logUnexpected(
+          "Unexpected exit code %s for generated artifact %s (%s)",
+          detailedExitCode, input, actionLabel);
     }
     return new LabelCause(
         MoreObjects.firstNonNull(input.getOwner(), actionLabel), detailedExitCode);
@@ -1533,9 +1531,8 @@
 
     private void handleSourceArtifactExceptionFromSkykey(SkyKey key, SourceArtifactException e) {
       if (!(key instanceof Artifact) || !((Artifact) key).isSourceArtifact()) {
-        bugReporter.sendBugReport(
-            new IllegalStateException(
-                "Unexpected SourceArtifactException for key: " + key + ", " + action, e));
+        bugReporter.logUnexpected(
+            e, "Unexpected SourceArtifactException for key: %s, %s", key, action.prettyPrint());
         missingArtifactCauses.add(
             new LabelCause(action.getOwner().getLabel(), e.getDetailedExitCode()));
         return;
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index fd0ef11..28d35b0 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -2258,7 +2258,6 @@
         "//src/main/java/com/google/devtools/build/lib/packages",
         "//src/main/java/com/google/devtools/build/lib/pkgcache",
         "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
-        "//src/main/java/com/google/devtools/build/lib/util:execution_detailed_exit_code_helper",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/protobuf:failure_details_java_proto",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
index 18bc499..95e955e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CompletionFunction.java
@@ -245,9 +245,8 @@
         }
       } catch (SourceArtifactException e) {
         if (!input.isSourceArtifact()) {
-          bugReporter.sendBugReport(
-              new IllegalStateException(
-                  "Non-source artifact had SourceArtifactException: " + input, e));
+          bugReporter.logUnexpected(
+              e, "Non-source artifact had SourceArtifactException: %s", input);
         }
         handleSourceFileError(input, e.getDetailedExitCode(), rootCausesBuilder, env, value, key);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeErrorProcessor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeErrorProcessor.java
index c86fe05..f1474b2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeErrorProcessor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeErrorProcessor.java
@@ -55,7 +55,6 @@
 import com.google.devtools.build.lib.skyframe.AspectKeyCreator.TopLevelAspectsKey;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.DetailedExitCode.DetailedExitCodeComparator;
-import com.google.devtools.build.lib.util.ExecutionDetailedExitCodeHelper;
 import com.google.devtools.build.skyframe.CycleInfo;
 import com.google.devtools.build.skyframe.CyclesReporter;
 import com.google.devtools.build.skyframe.ErrorInfo;
@@ -327,9 +326,7 @@
     }
 
     if (includeExecutionPhase && detailedExitCode == null) {
-      detailedExitCode =
-          ExecutionDetailedExitCodeHelper.createDetailedExitCodeForUndetailedExecutionCause(
-              result, undetailedCause);
+      detailedExitCode = createDetailedExitCodeForUndetailedExecutionCause(result, undetailedCause);
     }
 
     return ErrorProcessingResult.create(
@@ -410,7 +407,7 @@
       return;
     }
 
-    sendBugReportUnexpectedExceptionOrigin(errorInfo, key, walkableGraph, cause);
+    logUnexpectedExceptionOrigin(errorInfo, key, walkableGraph, cause);
   }
 
   private static void assertValidAnalysisOrExecutionException(
@@ -428,14 +425,14 @@
       return;
     }
 
-    sendBugReportUnexpectedExceptionOrigin(errorInfo, key, walkableGraph, cause);
+    logUnexpectedExceptionOrigin(errorInfo, key, walkableGraph, cause);
   }
 
   /**
-   * Walk the graph to find a path to the lowest-level node that threw unexpected exception and send
-   * a BugReport.
+   * Walk the graph to find a path to the lowest-level node that threw unexpected exception and log
+   * it.
    */
-  private static void sendBugReportUnexpectedExceptionOrigin(
+  private static void logUnexpectedExceptionOrigin(
       ErrorInfo errorInfo, SkyKey key, WalkableGraph walkableGraph, Throwable cause)
       throws InterruptedException {
     List<SkyKey> path = new ArrayList<>();
@@ -463,9 +460,7 @@
         }
       } while (foundDep);
     } finally {
-      BugReport.sendBugReport(
-          new IllegalStateException(
-              "Unexpected analysis error: " + key + " -> " + errorInfo + ", (" + path + ")"));
+      BugReport.logUnexpected("Unexpected analysis error: %s -> %s, (%s)", key, errorInfo, path);
     }
   }
 
@@ -531,10 +526,7 @@
         // during evaluation (otherwise, it wouldn't have bothered to find a cycle). So the best
         // we can do is throw a generic build failure exception, since we've already reported the
         // cycles above.
-        throw new BuildFailedException(
-            null,
-            ExecutionDetailedExitCodeHelper.createDetailedExecutionExitCode(
-                "cycle found during execution", Execution.Code.CYCLE));
+        throw new BuildFailedException(null, CYCLE_CODE);
       } else {
         rethrow(exception, bugReporter, result);
       }
@@ -564,6 +556,10 @@
           logger.atWarning().withCause(cause).log(
               "Non-action-execution/input-error exception for %s", error);
         }
+      } else if (!error.getValue().getCycleInfo().isEmpty()) {
+        detailedExitCode =
+            DetailedExitCodeComparator.chooseMoreImportantWithFirstIfTie(
+                detailedExitCode, CYCLE_CODE);
       } else {
         undetailedCause = cause;
       }
@@ -571,8 +567,7 @@
     if (detailedExitCode != null) {
       return detailedExitCode;
     }
-    return ExecutionDetailedExitCodeHelper.createDetailedExitCodeForUndetailedExecutionCause(
-        result, undetailedCause);
+    return createDetailedExitCodeForUndetailedExecutionCause(result, undetailedCause);
   }
 
   /**
@@ -580,7 +575,7 @@
    * At the moment, we don't expect any analysis failure to be catastrophic.
    */
   @VisibleForTesting
-  public static void rethrow(
+  static void rethrow(
       Throwable cause, BugReporter bugReporter, EvaluationResult<?> resultForDebugging)
       throws BuildFailedException, TestExecException {
     Throwables.throwIfUnchecked(cause);
@@ -607,9 +602,23 @@
     if (cause instanceof InputFileErrorException) {
       throw (InputFileErrorException) cause;
     }
+
     // We encountered an exception we don't think we should have encountered. This can indicate
     // an exception-processing bug in our code, such as lower level exceptions not being properly
     // handled, or in our expectations in this method.
+
+    if (cause instanceof DetailedException) {
+      // The exception escaped Skyframe error bubbling, but its failure detail can still be used.
+      bugReporter.logUnexpected(
+          (Exception) cause,
+          "action terminated with unexpected exception with result %s",
+          resultForDebugging);
+      throw new BuildFailedException(
+          cause.getMessage(), ((DetailedException) cause).getDetailedExitCode());
+    }
+
+    // An undetailed exception means we may incorrectly attribute responsibility for the failure:
+    // we need to fix that.
     bugReporter.sendBugReport(
         new IllegalStateException(
             "action terminated with unexpected exception with result " + resultForDebugging,
@@ -617,11 +626,42 @@
     String message =
         "Unexpected exception, please file an issue with the Bazel team: " + cause.getMessage();
     throw new BuildFailedException(
-        message,
-        DetailedExitCode.of(
-            FailureDetail.newBuilder()
-                .setMessage(message)
-                .setExecution(Execution.newBuilder().setCode(Execution.Code.UNEXPECTED_EXCEPTION))
-                .build()));
+        message, createDetailedExecutionExitCode(message, UNKONW_EXECUTION));
+  }
+
+  private static DetailedExitCode createDetailedExitCodeForUndetailedExecutionCause(
+      EvaluationResult<?> result, Throwable undetailedCause) {
+    // TODO(b/227660368): These warning logs should be a bug report, but tests currently fail.
+    if (undetailedCause == null) {
+      logger.atWarning().log("No exceptions found despite error in %s", result);
+      return createDetailedExecutionExitCode(
+          "keep_going execution failed without an action failure",
+          Execution.Code.NON_ACTION_EXECUTION_FAILURE);
+    }
+    logger.atWarning().withCause(undetailedCause).log("No detailed exception found in %s", result);
+    return createDetailedExecutionExitCode(
+        "keep_going execution failed without an action failure: "
+            + undetailedCause.getMessage()
+            + " ("
+            + undetailedCause.getClass().getSimpleName()
+            + ")",
+        Execution.Code.NON_ACTION_EXECUTION_FAILURE);
+  }
+
+  private static final DetailedExitCode CYCLE_CODE =
+      createDetailedExecutionExitCode("cycle found during execution", Execution.Code.CYCLE);
+  private static final Execution UNKONW_EXECUTION =
+      Execution.newBuilder().setCode(Execution.Code.UNEXPECTED_EXCEPTION).build();
+
+  private static DetailedExitCode createDetailedExecutionExitCode(
+      String message, Execution.Code detailedCode) {
+    return createDetailedExecutionExitCode(
+        message, Execution.newBuilder().setCode(detailedCode).build());
+  }
+
+  private static DetailedExitCode createDetailedExecutionExitCode(
+      String message, Execution execution) {
+    return DetailedExitCode.of(
+        FailureDetail.newBuilder().setMessage(message).setExecution(execution).build());
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/util/BUILD b/src/main/java/com/google/devtools/build/lib/util/BUILD
index 6b675e8..fc621a8 100644
--- a/src/main/java/com/google/devtools/build/lib/util/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/util/BUILD
@@ -29,17 +29,6 @@
 )
 
 java_library(
-    name = "execution_detailed_exit_code_helper",
-    srcs = ["ExecutionDetailedExitCodeHelper.java"],
-    deps = [
-        ":detailed_exit_code",
-        "//src/main/java/com/google/devtools/build/skyframe",
-        "//src/main/protobuf:failure_details_java_proto",
-        "//third_party:flogger",
-    ],
-)
-
-java_library(
     name = "os",
     srcs = ["OS.java"],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/util/ExecutionDetailedExitCodeHelper.java b/src/main/java/com/google/devtools/build/lib/util/ExecutionDetailedExitCodeHelper.java
deleted file mode 100644
index f3c107b..0000000
--- a/src/main/java/com/google/devtools/build/lib/util/ExecutionDetailedExitCodeHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2021 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.util;
-
-import com.google.common.flogger.GoogleLogger;
-import com.google.devtools.build.lib.server.FailureDetails.Execution;
-import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
-import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
-import com.google.devtools.build.skyframe.EvaluationResult;
-
-/** A collection of helper methods around execution-related DetailedExitCode. */
-public final class ExecutionDetailedExitCodeHelper {
-  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
-
-  public static DetailedExitCode createDetailedExitCodeForUndetailedExecutionCause(
-      EvaluationResult<?> result, Throwable undetailedCause) {
-    if (undetailedCause == null) {
-      logger.atWarning().log("No exceptions found despite error in %s", result);
-      return createDetailedExecutionExitCode(
-          "keep_going execution failed without an action failure",
-          Code.NON_ACTION_EXECUTION_FAILURE);
-    }
-    logger.atWarning().withCause(undetailedCause).log("No detailed exception found in %s", result);
-    return createDetailedExecutionExitCode(
-        "keep_going execution failed without an action failure: "
-            + undetailedCause.getMessage()
-            + " ("
-            + undetailedCause.getClass().getSimpleName()
-            + ")",
-        Code.NON_ACTION_EXECUTION_FAILURE);
-  }
-
-  public static DetailedExitCode createDetailedExecutionExitCode(
-      String message, Code detailedCode) {
-    return DetailedExitCode.of(
-        FailureDetail.newBuilder()
-            .setMessage(message)
-            .setExecution(Execution.newBuilder().setCode(detailedCode))
-            .build());
-  }
-
-  private ExecutionDetailedExitCodeHelper() {}
-}
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
index 1e5b735..cca6f3b 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BUILD
@@ -98,6 +98,7 @@
         "//src/test/java/com/google/devtools/build/lib/testutil:TestConstants",
         "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
         "//src/test/java/com/google/devtools/build/lib/vfs/util",
+        "//third_party:error_prone_annotations",
         "//third_party:guava",
         "//third_party:guava-testlib",
         "//third_party:jsr305",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
index 071fd93..6bcae4a 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/util/BuildIntegrationTestCase.java
@@ -120,6 +120,7 @@
 import com.google.devtools.build.lib.worker.WorkerModule;
 import com.google.devtools.common.options.OptionsBase;
 import com.google.devtools.common.options.OptionsParser;
+import com.google.errorprone.annotations.FormatMethod;
 import java.io.IOException;
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.util.ArrayList;
@@ -993,6 +994,18 @@
       exceptions.add(exception);
     }
 
+    @FormatMethod
+    @Override
+    public void logUnexpected(String message, Object... args) {
+      sendBugReport(new IllegalStateException(String.format(message, args)));
+    }
+
+    @FormatMethod
+    @Override
+    public void logUnexpected(Exception e, String message, Object... args) {
+      sendBugReport(new IllegalStateException(String.format(message, args), e));
+    }
+
     @Override
     public void handleCrash(Crash crash, CrashContext ctx) {
       // Unexpected: try to crash JVM.
diff --git a/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
index 7b44260..87519c3 100644
--- a/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
+++ b/src/test/java/com/google/devtools/build/lib/testutil/MoreAsserts.java
@@ -205,6 +205,16 @@
         expectedExitCode, exitCode, recordingOutErr.outAsLatin1(), recordingOutErr.errAsLatin1());
   }
 
+  public static void assertEqualWithStdoutAndErr(
+      Object expected, Object actual, String stdout, String stderr) {
+    if (!expected.equals(actual)) {
+      fail(
+          String.format(
+              "expected <%s> but was <%s> and stdout was <%s> and stderr was <%s>",
+              expected, actual, stdout, stderr));
+    }
+  }
+
   public static void assertStdoutContainsString(String expected, String stdout, String stderr) {
     if (!stdout.contains(expected)) {
       fail("expected stdout to contain string <" + expected + "> but stdout was <"