Add catastrophe field to EvaluationResult so that callers can identify the cause of a catastrophic failure (this is distinct from a crash).

Also clean up catastrophe logic in ParallelEvaluator -- the catastrophic nature of an exception is important only if the build is keep_going, and only if the exception is catastrophic can we have an exception in the first place.

--
MOS_MIGRATED_REVID=111293164
diff --git a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
index e2a6608..54b3cfd 100644
--- a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
+++ b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
@@ -1312,10 +1312,14 @@
       // ErrorInfo could only be null if SchedulerException wrapped an InterruptedException, but
       // that should have been propagated.
       ErrorInfo errorInfo = Preconditions.checkNotNull(e.getErrorInfo(), errorKey);
-      catastrophe = errorInfo.isCatastrophic();
-      if (!catastrophe || !keepGoing) {
+      if (!keepGoing) {
         bubbleErrorInfo = bubbleErrorUp(errorInfo, errorKey, skyKeys, visitor);
       } else {
+        Preconditions.checkState(
+            errorInfo.isCatastrophic(),
+            "Scheduler exception only thrown for catastrophe in keep_going evaluation: %s",
+            e);
+        catastrophe = true;
         // Bubbling the error up requires that graph edges are present for done nodes. This is not
         // always the case in a keepGoing evaluation, since it is assumed that done nodes do not
         // need to be traversed. In this case, we hope the caller is tolerant of a possibly empty
@@ -1507,10 +1511,17 @@
    * {@code skyKeys} are known to be in the DONE state ({@code entry.isDone()} returns true).
    */
   private <T extends SkyValue> EvaluationResult<T> constructResult(
-      @Nullable ValueVisitor visitor, Iterable<SkyKey> skyKeys,
-      Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, boolean catastrophe) {
-    Preconditions.checkState(!keepGoing || catastrophe || bubbleErrorInfo == null,
-        "", skyKeys, bubbleErrorInfo);
+      @Nullable ValueVisitor visitor,
+      Iterable<SkyKey> skyKeys,
+      @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo,
+      boolean catastrophe) {
+    Preconditions.checkState(
+        catastrophe == (keepGoing && bubbleErrorInfo != null),
+        "Catastrophe not consistent with keepGoing mode and bubbleErrorInfo: %s %s %s %s",
+        skyKeys,
+        catastrophe,
+        keepGoing,
+        bubbleErrorInfo);
     EvaluationResult.Builder<T> result = EvaluationResult.builder();
     List<SkyKey> cycleRoots = new ArrayList<>();
     boolean hasError = false;
@@ -1556,6 +1567,20 @@
     Preconditions.checkState(bubbleErrorInfo == null || hasError,
         "If an error bubbled up, some top-level node must be in error", bubbleErrorInfo, skyKeys);
     result.setHasError(hasError);
+    if (catastrophe) {
+      // We may not have a top-level node completed. Inform the caller of the catastrophic exception
+      // that shut down the evaluation so that it has some context.
+      ErrorInfo errorInfo =
+          Preconditions.checkNotNull(
+              Iterables.getOnlyElement(bubbleErrorInfo.values()).getErrorInfo(),
+              "bubbleErrorInfo should have contained element with errorInfo: %s",
+              bubbleErrorInfo);
+      Preconditions.checkState(
+          errorInfo.isCatastrophic(),
+          "bubbleErrorInfo should have contained element with catastrophe: %s",
+          bubbleErrorInfo);
+      result.setCatastrophe(errorInfo.getException());
+    }
     return result.build();
   }