Fix an issue where a "build" command might hang after it finished, because sandbox directories could not be cleaned up.

--
MOS_MIGRATED_REVID=134286101
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
index 004c8df..de018b1 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java
@@ -26,6 +26,8 @@
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.standalone.StandaloneSpawnStrategy;
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.vfs.Path;
@@ -38,7 +40,10 @@
 /** Helper methods that are shared by the different sandboxing strategies in this package. */
 final class SandboxHelpers {
 
-  static void lazyCleanup(ExecutorService backgroundWorkers, final SandboxRunner runner) {
+  static void lazyCleanup(
+      ExecutorService backgroundWorkers,
+      final EventHandler eventHandler,
+      final SandboxRunner runner) {
     // By deleting the sandbox directory in the background, we avoid having to wait for it to
     // complete before returning from the action, which improves performance.
     backgroundWorkers.execute(
@@ -46,18 +51,15 @@
           @Override
           public void run() {
             try {
-              while (!Thread.currentThread().isInterrupted()) {
-                try {
-                  runner.cleanup();
-                  return;
-                } catch (IOException e2) {
-                  // Sleep & retry.
-                  Thread.sleep(250);
-                }
-              }
-            } catch (InterruptedException e) {
-              // Mark ourselves as interrupted and then exit.
-              Thread.currentThread().interrupt();
+              runner.cleanup();
+            } catch (IOException e) {
+              // Can't do anything except logging here. SandboxModule#afterCommand will try again
+              // and alert the user if cleanup still fails.
+              eventHandler.handle(
+                  Event.warn(
+                      String.format(
+                          "Could not delete sandbox directory after action execution: %s (%s)",
+                          runner.getSandboxPath(), e)));
             }
           }
         });
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
index 53c5c67..df28aaf 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxModule.java
@@ -21,11 +21,13 @@
 import com.google.devtools.build.lib.actions.ActionContextProvider;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent;
-import com.google.devtools.build.lib.concurrent.ExecutorUtil;
+import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.runtime.BlazeModule;
 import com.google.devtools.build.lib.runtime.Command;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.common.options.OptionsBase;
 import java.io.IOException;
 import java.util.concurrent.ExecutorService;
@@ -35,18 +37,18 @@
  * This module provides the Sandbox spawn strategy.
  */
 public final class SandboxModule extends BlazeModule {
-  // Per-server state
-  private ExecutorService backgroundWorkers;
-
   // Per-command state
   private CommandEnvironment env;
   private BuildRequest buildRequest;
+  private ExecutorService backgroundWorkers;
+  private SandboxOptions sandboxOptions;
 
   @Override
   public Iterable<ActionContextProvider> getActionContextProviders() {
     Preconditions.checkNotNull(env);
     Preconditions.checkNotNull(buildRequest);
     Preconditions.checkNotNull(backgroundWorkers);
+    sandboxOptions = buildRequest.getOptions(SandboxOptions.class);
     try {
       return ImmutableList.<ActionContextProvider>of(
           SandboxActionContextProvider.create(env, buildRequest, backgroundWorkers));
@@ -79,13 +81,46 @@
 
   @Override
   public void afterCommand() {
+    // We want to make sure that all sandbox directories are deleted after a command finishes or at
+    // least the user gets notified if some of them can't be deleted. However we can't rely on the
+    // background workers for that, because a) they can't log, and b) if a directory is undeletable,
+    // the Runnable might never finish. So we cancel them and delete the remaining directories here,
+    // where we have more control.
+    backgroundWorkers.shutdownNow();
+    if (sandboxOptions != null && !sandboxOptions.sandboxDebug) {
+      Path sandboxRoot =
+          env.getDirectories()
+              .getOutputBase()
+              .getRelative(env.getRuntime().getProductName() + "-sandbox");
+      if (sandboxRoot.exists()) {
+        try {
+          for (Path child : sandboxRoot.getDirectoryEntries()) {
+            try {
+              FileSystemUtils.deleteTree(child);
+            } catch (IOException e) {
+              env.getReporter()
+                  .handle(
+                      Event.warn(
+                          String.format(
+                              "Could not delete sandbox directory: %s (%s)",
+                              child.getPathString(), e)));
+            }
+          }
+          sandboxRoot.delete();
+        } catch (IOException e) {
+          env.getReporter()
+              .handle(
+                  Event.warn(
+                      String.format(
+                          "Could not delete %s directory: %s", sandboxRoot.getBaseName(), e)));
+        }
+      }
+    }
+
     env = null;
     buildRequest = null;
-
-    // "bazel clean" will also try to delete the sandbox directories, leading to a race condition
-    // if it is run right after a "bazel build". We wait for and shutdown the background worker pool
-    // before continuing to avoid this.
-    ExecutorUtil.interruptibleShutdown(backgroundWorkers);
+    backgroundWorkers = null;
+    sandboxOptions = null;
   }
 
   @Subscribe
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java
index 4bb2b95..dabbf66 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxRunner.java
@@ -122,4 +122,8 @@
       FileSystemUtils.deleteTree(sandboxPath);
     }
   }
+
+  Path getSandboxPath() {
+    return sandboxPath;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
index d02ffc2..b5d787c 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxStrategy.java
@@ -26,6 +26,7 @@
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.buildtool.BuildRequest;
 import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -70,6 +71,7 @@
       SandboxRunner runner,
       AtomicReference<Class<? extends SpawnActionContext>> writeOutputFiles)
       throws ExecException, InterruptedException {
+    EventHandler eventHandler = actionExecutionContext.getExecutor().getEventHandler();
     try {
       runner.run(
           spawn.getArguments(),
@@ -90,17 +92,14 @@
         } catch (IOException e) {
           // Catch the IOException and turn it into an error message, otherwise this might hide an
           // exception thrown during runner.run earlier.
-          actionExecutionContext
-              .getExecutor()
-              .getEventHandler()
-              .handle(
-                  Event.error(
-                      "I/O exception while extracting output artifacts from sandboxed execution: "
-                          + e));
+          eventHandler.handle(
+              Event.error(
+                  "I/O exception while extracting output artifacts from sandboxed execution: "
+                      + e));
         }
       }
       if (!sandboxOptions.sandboxDebug) {
-        SandboxHelpers.lazyCleanup(backgroundWorkers, runner);
+        SandboxHelpers.lazyCleanup(backgroundWorkers, eventHandler, runner);
       }
     }
 
diff --git a/src/test/shell/bazel/bazel_sandboxing_test.sh b/src/test/shell/bazel/bazel_sandboxing_test.sh
index 38b9189..17e3d4f 100755
--- a/src/test/shell/bazel/bazel_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_sandboxing_test.sh
@@ -222,8 +222,7 @@
   bazel build examples/genrule:tools_work &> $TEST_log \
     || fail "Hermetic genrule failed: examples/genrule:tools_work"
   bazel shutdown &> $TEST_log || fail "bazel shutdown failed"
-  ls -la "$(bazel info output_base)/bazel-sandbox"
-  if [[ "$(ls -A "$(bazel info execution_root)"/bazel-sandbox)" ]]; then
+  if [[ "$(ls -la "$(bazel info output_base)/bazel-sandbox")" ]]; then
     fail "Build left files around afterwards"
   fi
 }