Run idle server GC immediately following a `--nokeep_state_after_build_command` instead of waiting 10 seconds.

This helps to ensure that weakly reachable objects are not resurrected. Using the idle server task avoids blocking command completion on a full GC, which can take multiple seconds.

PiperOrigin-RevId: 550056326
Change-Id: Ia55a2360a196ee0f0dcbb0ac6b6baa11e39e244d
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandResult.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandResult.java
index 77191d7..4d8b3b9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandResult.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandResult.java
@@ -38,21 +38,24 @@
   @Nullable private final ExecRequest execDescription;
   private final ImmutableList<Any> responseExtensions;
   private final boolean shutdown;
+  private final boolean stateKeptAfterBuild;
 
   private BlazeCommandResult(
       DetailedExitCode detailedExitCode,
       @Nullable ExecRequest execDescription,
       boolean shutdown,
-      ImmutableList<Any> responseExtensions) {
+      ImmutableList<Any> responseExtensions,
+      boolean stateKeptAfterBuild) {
     this.detailedExitCode = Preconditions.checkNotNull(detailedExitCode);
     this.execDescription = execDescription;
     this.shutdown = shutdown;
     this.responseExtensions = responseExtensions;
+    this.stateKeptAfterBuild = stateKeptAfterBuild;
   }
 
   private BlazeCommandResult(
       DetailedExitCode detailedExitCode, @Nullable ExecRequest execDescription, boolean shutdown) {
-    this(detailedExitCode, execDescription, shutdown, ImmutableList.of());
+    this(detailedExitCode, execDescription, shutdown, ImmutableList.of(), true);
   }
 
   public ExitCode getExitCode() {
@@ -85,6 +88,11 @@
     return responseExtensions;
   }
 
+  /** Reflects the value of {@link CommonCommandOptions#keepStateAfterBuild}. */
+  public boolean stateKeptAfterBuild() {
+    return stateKeptAfterBuild;
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this)
@@ -92,6 +100,8 @@
         .add("failureDetail", getFailureDetail())
         .add("execDescription", execDescription)
         .add("shutdown", shutdown)
+        .add("responseExtensions", responseExtensions)
+        .add("stateKeptAfterBuild", stateKeptAfterBuild)
         .toString();
   }
 
@@ -116,9 +126,15 @@
   }
 
   public static BlazeCommandResult withResponseExtensions(
-      BlazeCommandResult result, ImmutableList<Any> responseExtensions) {
+      BlazeCommandResult result,
+      ImmutableList<Any> responseExtensions,
+      boolean stateKeptAfterBuild) {
     return new BlazeCommandResult(
-        result.detailedExitCode, result.execDescription, result.shutdown, responseExtensions);
+        result.detailedExitCode,
+        result.execDescription,
+        result.shutdown,
+        responseExtensions,
+        stateKeptAfterBuild);
   }
 
   public static BlazeCommandResult execute(ExecRequest execDescription) {
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 1ec96ab..2e00120 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
@@ -680,7 +680,7 @@
     DebugLoggerConfigurator.flushServerLog();
     storedExitCode.set(null);
     return BlazeCommandResult.withResponseExtensions(
-        finalCommandResult, env.getResponseExtensions());
+        finalCommandResult, env.getResponseExtensions(), commonOptions.keepStateAfterBuild);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/server/CommandManager.java b/src/main/java/com/google/devtools/build/lib/server/CommandManager.java
index 617ec04..60e6551 100644
--- a/src/main/java/com/google/devtools/build/lib/server/CommandManager.java
+++ b/src/main/java/com/google/devtools/build/lib/server/CommandManager.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.GoogleLogger;
 import com.google.devtools.build.lib.server.CommandProtos.CancelRequest;
+import com.google.devtools.build.lib.server.IdleServerTasks.IdleServerCleanupStrategy;
 import com.google.devtools.build.lib.util.ThreadUtils;
 import java.util.Collections;
 import java.util.HashMap;
@@ -43,7 +44,7 @@
   CommandManager(boolean doIdleServerTasks, @Nullable String slowInterruptMessageSuffix) {
     this.doIdleServerTasks = doIdleServerTasks;
     this.slowInterruptMessageSuffix = slowInterruptMessageSuffix;
-    idle();
+    idle(IdleServerCleanupStrategy.DELAYED);
   }
 
   void preemptEligibleCommands() {
@@ -132,11 +133,11 @@
     logger.atInfo().log("Starting command %s on thread %s", command.id, command.thread.getName());
   }
 
-  private void idle() {
+  private void idle(IdleServerCleanupStrategy cleanupStrategy) {
     Preconditions.checkState(idleServerTasks == null);
     if (doIdleServerTasks) {
       idleServerTasks = new IdleServerTasks();
-      idleServerTasks.idle();
+      idleServerTasks.idle(cleanupStrategy);
     }
   }
 
@@ -176,10 +177,11 @@
     interruptWatcherThread.start();
   }
 
-  class RunningCommand implements AutoCloseable {
+  final class RunningCommand implements AutoCloseable {
     private final Thread thread;
     private final String id;
     private final boolean preemptible;
+    private IdleServerCleanupStrategy cleanupStrategy = IdleServerCleanupStrategy.DELAYED;
 
     private RunningCommand(boolean preemptible) {
       thread = Thread.currentThread();
@@ -192,7 +194,7 @@
       synchronized (runningCommandsMap) {
         runningCommandsMap.remove(id);
         if (runningCommandsMap.isEmpty()) {
-          idle();
+          idle(cleanupStrategy);
         }
         runningCommandsMap.notify();
       }
@@ -205,7 +207,12 @@
     }
 
     boolean isPreemptible() {
-      return this.preemptible;
+      return preemptible;
+    }
+
+    /** Requests a manual GC as soon as the server becomes idle. */
+    void requestEagerIdleServerCleanup() {
+      cleanupStrategy = IdleServerCleanupStrategy.EAGER;
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java b/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
index 9f73f5a..8efb5da 100644
--- a/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
@@ -568,6 +568,13 @@
                                 .setCode(Command.Code.INVOCATION_POLICY_PARSE_FAILURE))
                         .build()));
       }
+      if (!result.stateKeptAfterBuild()) {
+        // If state was not kept, GC as soon as the server becomes idle. This ensures that weakly
+        // reachable objects are not "resurrected" on a subsequent command. See b/291641466. Without
+        // this call, a manual GC will only be triggered if the server remains idle for at least 10
+        // seconds before the next command starts.
+        command.requestEagerIdleServerCleanup();
+      }
     } catch (InterruptedException e) {
       result =
           BlazeCommandResult.detailedExitCode(
diff --git a/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java b/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java
index 4d286e3..5186db8 100644
--- a/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java
+++ b/src/main/java/com/google/devtools/build/lib/server/IdleServerTasks.java
@@ -28,25 +28,35 @@
 import java.util.concurrent.TimeUnit;
 
 /**
- * Run cleanup-related tasks during idle periods in the server.
- * idle() and busy() must be called in that order, and only once.
+ * Run cleanup-related tasks during idle periods in the server. idle() and busy() must be called in
+ * that order, and only once.
  */
-class IdleServerTasks {
-  private final ScheduledThreadPoolExecutor executor;
+final class IdleServerTasks {
+
   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
 
+  enum IdleServerCleanupStrategy {
+    EAGER(0),
+    DELAYED(10);
+
+    private final long delaySeconds;
+
+    IdleServerCleanupStrategy(long delaySeconds) {
+      this.delaySeconds = delaySeconds;
+    }
+  }
+
+  private final ScheduledThreadPoolExecutor executor;
+
   /** Must be called from the main thread. */
   public IdleServerTasks() {
-    this.executor = new ScheduledThreadPoolExecutor(
-        1,
-        new ThreadFactoryBuilder().setNameFormat("idle-server-tasks-%d").build());
+    this.executor =
+        new ScheduledThreadPoolExecutor(
+            1, new ThreadFactoryBuilder().setNameFormat("idle-server-tasks-%d").build());
   }
 
-  /**
-   * Called when the server becomes idle. Should not block, but may invoke
-   * new threads.
-   */
-  public void idle() {
+  /** Called when the server becomes idle. Should not block, but may invoke new threads. */
+  public void idle(IdleServerCleanupStrategy cleanupStrategy) {
     Preconditions.checkState(!executor.isShutdown());
 
     @SuppressWarnings("unused")
@@ -66,7 +76,7 @@
                   StringUtilities.prettyPrintBytes(before.getCommitted()),
                   StringUtilities.prettyPrintBytes(after.getCommitted()));
             },
-            10,
+            cleanupStrategy.delaySeconds,
             TimeUnit.SECONDS);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/server/GrpcServerTest.java b/src/test/java/com/google/devtools/build/lib/server/GrpcServerTest.java
index e0593a9..c3fdfb0 100644
--- a/src/test/java/com/google/devtools/build/lib/server/GrpcServerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/server/GrpcServerTest.java
@@ -241,7 +241,8 @@
                 BlazeCommandResult.success(),
                 ImmutableList.of(
                     Any.pack(StringValue.of("foo")),
-                    Any.pack(BytesValue.of(ByteString.copyFromUtf8("bar")))));
+                    Any.pack(BytesValue.of(ByteString.copyFromUtf8("bar")))),
+                true);
           }
         };
     createServer(dispatcher);