[8.8.0] Don't fail an action due to spawn log exceptions (#30717)

Instead, log the exception (at most once per spawn) and show a warning
at the end of the build.

Speculatively fixes #22920.

Closes #27313.

PiperOrigin-RevId: 825577891
Change-Id: I91d057fca01b0d1f1413ea1c3210bf4a392ebc55

(cherry picked from commit ceaff2dcc7c213b0eaac82a35903075ab71fe089)


8.8.0 adaptation: the release branch has no `internalToUnicode`
conversion for exec paths and mnemonics, passes the real
`inputMetadataProvider` to `logFile`/`logDirectory`, and
`logUnresolvedSymlink` takes only two arguments; the new output-logging
loop was adapted to those signatures. The test-only reformatting hunks
in `SpawnLogContextTestBase` were dropped as they don't apply to the
branch.

Closes #30715
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java b/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
index f62c175..f7471db 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
@@ -112,7 +112,8 @@
                 env.getOptions().getOptions(RemoteOptions.class),
                 env.getRuntime().getFileSystem().getDigestFunction(),
                 env.getXattrProvider(),
-                env.getCommandId());
+                env.getCommandId(),
+                env.getReporter());
       } catch (InterruptedException e) {
         env.getReporter()
             .handle(Event.error("Error while setting up the execution log: " + e.getMessage()));
diff --git a/src/main/java/com/google/devtools/build/lib/exec/BUILD b/src/main/java/com/google/devtools/build/lib/exec/BUILD
index 156a5ae..b3f0f1c 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/exec/BUILD
@@ -284,6 +284,7 @@
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
+        "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/profiler",
         "//src/main/java/com/google/devtools/build/lib/remote/options",
         "//src/main/java/com/google/devtools/build/lib/remote/util:digest_utils",
diff --git a/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java b/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
index 5641ee4..c49db49 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/CompactSpawnLogContext.java
@@ -21,6 +21,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.actions.AbstractAction;
 import com.google.devtools.build.lib.actions.ActionInput;
 import com.google.devtools.build.lib.actions.Artifact;
@@ -38,6 +39,8 @@
 import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
 import com.google.devtools.build.lib.concurrent.ErrorClassifier;
 import com.google.devtools.build.lib.concurrent.NamedForkJoinPool;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
 import com.google.devtools.build.lib.exec.Protos.Digest;
 import com.google.devtools.build.lib.exec.Protos.ExecLogEntry;
 import com.google.devtools.build.lib.exec.Protos.Platform;
@@ -66,12 +69,15 @@
 import java.util.SortedMap;
 import java.util.UUID;
 import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 
 /** A {@link SpawnLogContext} implementation that produces a log in compact format. */
 public class CompactSpawnLogContext extends SpawnLogContext {
 
+  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+
   private static final Comparator<ExecLogEntry.File> EXEC_LOG_ENTRY_FILE_COMPARATOR =
       Comparator.comparing(ExecLogEntry.File::getPath);
 
@@ -146,6 +152,8 @@
   private final DigestHashFunction digestHashFunction;
   private final XattrProvider xattrProvider;
   private final UUID invocationId;
+  private final ExtendedEventHandler reporter;
+  private final AtomicBoolean outputLoggingFailed = new AtomicBoolean(false);
 
   // Maps a key identifying an entry into its ID.
   // Each key is either a NestedSet.Node or the String path of a file, directory, symlink or
@@ -170,7 +178,8 @@
       @Nullable RemoteOptions remoteOptions,
       DigestHashFunction digestHashFunction,
       XattrProvider xattrProvider,
-      UUID invocationId)
+      UUID invocationId,
+      ExtendedEventHandler reporter)
       throws IOException, InterruptedException {
     this.execRoot = execRoot;
     this.workspaceName = workspaceName;
@@ -179,6 +188,7 @@
     this.digestHashFunction = digestHashFunction;
     this.xattrProvider = xattrProvider;
     this.invocationId = invocationId;
+    this.reporter = reporter;
     this.outputStream = getOutputStream(outputPath);
 
     logInvocation();
@@ -236,19 +246,32 @@
       }
       builder.setMnemonic(spawn.getMnemonic());
 
+      boolean warned = false;
       for (ActionInput output : spawn.getOutputFiles()) {
-        Path path = fileSystem.getPath(execRoot.getRelative(output.getExecPath()));
-        if (!output.isDirectory() && !output.isSymlink() && path.isFile()) {
-          builder.addOutputsBuilder().setOutputId(logFile(output, path, inputMetadataProvider));
-        } else if (output.isDirectory() && path.isDirectory()) {
-          builder
-              .addOutputsBuilder()
-              .setOutputId(logDirectory(output, path, inputMetadataProvider));
-        } else if (output.isSymlink() && path.isSymbolicLink()) {
-          builder.addOutputsBuilder().setOutputId(logUnresolvedSymlink(output, path));
-        } else {
-          builder.addOutputsBuilder().setInvalidOutputPath(output.getExecPathString());
+        var path = fileSystem.getPath(execRoot.getRelative(output.getExecPath()));
+        var outputBuilder = ExecLogEntry.Output.newBuilder();
+        try {
+          if (!output.isDirectory() && !output.isSymlink() && path.isFile()) {
+            outputBuilder.setOutputId(logFile(output, path, inputMetadataProvider));
+          } else if (output.isDirectory() && path.isDirectory()) {
+            outputBuilder.setOutputId(logDirectory(output, path, inputMetadataProvider));
+          } else if (output.isSymlink() && path.isSymbolicLink()) {
+            outputBuilder.setOutputId(logUnresolvedSymlink(output, path));
+          } else {
+            outputBuilder.setInvalidOutputPath(output.getExecPathString());
+          }
+        } catch (IOException e) {
+          if (!warned) {
+            outputLoggingFailed.set(true);
+            warned = true;
+            logger.atInfo().withCause(e).log(
+                "Failed to log outputs of spawn with mnemonic %s and primary output %s",
+                spawn.getMnemonic(),
+                Iterables.getFirst(spawn.getOutputFiles(), /* not reached */ null));
+          }
+          outputBuilder.setInvalidOutputPath(output.getExecPathString());
         }
+        builder.addOutputs(outputBuilder);
       }
 
       builder.setExitCode(result.exitCode());
@@ -727,6 +750,12 @@
 
   @Override
   public void close() throws IOException {
+    if (outputLoggingFailed.get()) {
+      reporter.handle(
+          Event.warn(
+              "The compact execution log is incomplete because some outputs could not be read."
+                  + " Refer to the server log file for details."));
+    }
     outputStream.close();
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/exec/BUILD b/src/test/java/com/google/devtools/build/lib/exec/BUILD
index 0cb9810..b086aad 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/exec/BUILD
@@ -97,6 +97,7 @@
         "//src/test/java/com/google/devtools/build/lib/analysis/util",
         "//src/test/java/com/google/devtools/build/lib/exec/util",
         "//src/test/java/com/google/devtools/build/lib/testutil",
+        "//src/test/java/com/google/devtools/build/lib/testutil:JunitUtils",
         "//src/test/java/com/google/devtools/build/lib/testutil:TestConstants",
         "//third_party:guava",
         "//third_party:jsr305",
diff --git a/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java b/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
index 7eb6bd1..ce2881f 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/CompactSpawnLogContextTest.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.exec;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertContainsEvent;
 import static com.google.devtools.build.lib.testutil.TestConstants.PRODUCT_NAME;
 import static com.google.devtools.build.lib.testutil.TestConstants.WORKSPACE_NAME;
 
@@ -239,6 +240,54 @@
             .build());
   }
 
+  @Test
+  public void testUnreadableOutputs(@TestParameter OutputsMode outputsMode) throws Exception {
+    Artifact readableFile = ActionsTestUtil.createArtifact(outputDir, "readable");
+    Artifact unreadableFile = ActionsTestUtil.createArtifact(outputDir, "unreadable");
+    Artifact unreadableFileDir =
+        ActionsTestUtil.createTreeArtifactWithGeneratingAction(outputDir, "unreadableFileDir");
+
+    writeFile(readableFile.getPath(), "xyz");
+    // Make the files unreadable.
+    writeFile(unreadableFile.getPath(), "abc");
+    unreadableFile.getPath().setReadable(false);
+    writeFile(unreadableFileDir.getPath().getChild("file"), "def");
+    unreadableFileDir.getPath().getChild("file").setReadable(false);
+
+    SpawnBuilder spawn =
+        defaultSpawnBuilder().withOutputs(readableFile, unreadableFile, unreadableFileDir);
+
+    SpawnLogContext context = createSpawnLogContext();
+
+    context.logSpawn(
+        spawn.build(),
+        createInputMetadataProvider(),
+        createInputMap(),
+        outputsMode.getActionFileSystem(fs),
+        defaultTimeout(),
+        defaultSpawnResult());
+
+    closeAndAssertLog(
+        context,
+        defaultSpawnExecBuilder()
+            .addActualOutputs(
+                File.newBuilder()
+                    .setPath(PRODUCT_NAME + "-out/k8-fastbuild/bin/readable")
+                    .setDigest(getDigest("xyz"))
+                    .setIsTool(false))
+            .addListedOutputs(PRODUCT_NAME + "-out/k8-fastbuild/bin/readable")
+            .addListedOutputs(PRODUCT_NAME + "-out/k8-fastbuild/bin/unreadable")
+            .addListedOutputs(PRODUCT_NAME + "-out/k8-fastbuild/bin/unreadableFileDir")
+            .build());
+
+    assertContainsEvent(
+        storedEventHandler.getEvents(),
+        "The compact execution log is incomplete because some outputs could not be read. Refer"
+            + " to the server log file for details.");
+    assertThat(storedEventHandler.getEvents()).hasSize(1);
+    assertThat(storedEventHandler.getPosts()).isEmpty();
+  }
+
   @Override
   protected SpawnLogContext createSpawnLogContext(ImmutableMap<String, String> platformProperties)
       throws IOException, InterruptedException {
@@ -253,7 +302,8 @@
         remoteOptions,
         DigestHashFunction.SHA256,
         SyscallCache.NO_CACHE,
-        UUID.fromString("00000000-0000-0000-0000-000000000000"));
+        UUID.fromString("00000000-0000-0000-0000-000000000000"),
+        storedEventHandler);
   }
 
   @Override
diff --git a/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java b/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
index 6150808..d6cfaa6 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/SpawnLogContextTestBase.java
@@ -63,6 +63,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.exec.Protos.Digest;
 import com.google.devtools.build.lib.exec.Protos.EnvironmentVariable;
 import com.google.devtools.build.lib.exec.Protos.File;
@@ -117,6 +118,7 @@
   protected ArtifactRoot externalSourceRoot;
   protected ArtifactRoot externalOutputDir;
   protected BuildConfigurationValue configuration;
+  protected StoredEventHandler storedEventHandler;
 
   @TestParameter public boolean siblingRepositoryLayout;
 
@@ -161,6 +163,7 @@
         ArtifactRoot.asExternalSourceRoot(
             Root.fromPath(externalRoot.getChild(externalRepo.getName())));
     externalOutputDir = configuration.getBinDirectory(externalRepo);
+    storedEventHandler = new StoredEventHandler();
   }
 
   // A fake action filesystem that provides a fast digest, but refuses to compute it from the