Experimental flag to skip dynamic execution until there has been a successful build. This allows doing a first clean build in situations where dynamic execution interferes too much.

PiperOrigin-RevId: 403955415
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModule.java b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModule.java
index 3f876d9..db9b775 100644
--- a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModule.java
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModule.java
@@ -19,11 +19,16 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.eventbus.Subscribe;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.SpawnStrategy;
 import com.google.devtools.build.lib.actions.Spawns;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
 import com.google.devtools.build.lib.concurrent.ExecutorUtil;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.exec.ExecutionPolicy;
 import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
 import com.google.devtools.build.lib.runtime.BlazeModule;
@@ -42,13 +47,18 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-/**
- * {@link BlazeModule} providing support for dynamic spawn execution and scheduling.
- */
+/** {@link BlazeModule} providing support for dynamic spawn execution and scheduling. */
 public class DynamicExecutionModule extends BlazeModule {
 
   private ExecutorService executorService;
 
+  /**
+   * If true, this is the first build since this server started (excluding failed builds). This
+   * allows turning off dynamic execution for an initial build, avoiding dynamic execution on most
+   * clean builds.
+   */
+  private static boolean firstBuild = true;
+
   public DynamicExecutionModule() {}
 
   @VisibleForTesting
@@ -125,13 +135,17 @@
       SpawnStrategyRegistry.Builder registryBuilder, CommandEnvironment env)
       throws AbruptExitException {
     registerSpawnStrategies(
-        registryBuilder, env.getOptions().getOptions(DynamicExecutionOptions.class));
+        registryBuilder,
+        env.getOptions().getOptions(DynamicExecutionOptions.class),
+        env.getReporter());
   }
 
   // CommandEnvironment is difficult to access in tests, so use this method for testing.
   @VisibleForTesting
   final void registerSpawnStrategies(
-      SpawnStrategyRegistry.Builder registryBuilder, DynamicExecutionOptions options)
+      SpawnStrategyRegistry.Builder registryBuilder,
+      DynamicExecutionOptions options,
+      Reporter reporter)
       throws AbruptExitException {
     if (!options.internalSpawnScheduler) {
       return;
@@ -144,8 +158,12 @@
             this::getExecutionPolicy,
             this::getPostProcessingSpawnForLocalExecution);
     registryBuilder.registerStrategy(strategy, "dynamic", "dynamic_worker");
-
-    registryBuilder.addDynamicLocalStrategies(getLocalStrategies(options));
+    if (firstBuild && options.skipFirstBuild) {
+      reporter.handle(
+          Event.info("Disabling dynamic execution until we have seen a successful build"));
+    } else {
+      registryBuilder.addDynamicLocalStrategies(getLocalStrategies(options));
+    }
     registryBuilder.addDynamicRemoteStrategies(getRemoteStrategies(options));
   }
 
@@ -196,6 +214,14 @@
     return Optional.empty();
   }
 
+  @Subscribe
+  public void buildCompleteEvent(BuildCompleteEvent event) {
+    BuildResult result = event.getResult();
+    if (result.getSuccess()) {
+      firstBuild = false;
+    }
+  }
+
   @Override
   public void afterCommand() {
     ExecutorUtil.interruptibleShutdown(executorService);
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionOptions.java
index 19d844e..2a11c13 100644
--- a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicExecutionOptions.java
@@ -22,9 +22,7 @@
 import java.util.List;
 import java.util.Map;
 
-/**
- * Options related to dynamic spawn execution.
- */
+/** Options related to dynamic spawn execution. */
 public class DynamicExecutionOptions extends OptionsBase {
 
   @Option(
@@ -91,22 +89,20 @@
   public String dynamicWorkerStrategy;
 
   @Option(
-    name = "experimental_local_execution_delay",
-    documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-    effectTags = {OptionEffectTag.UNKNOWN},
-    defaultValue = "1000",
-    help =
-        "How many milliseconds should local execution be delayed, if remote execution was faster"
-            + " during a build at least once?"
-  )
+      name = "experimental_local_execution_delay",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      defaultValue = "1000",
+      help =
+          "How many milliseconds should local execution be delayed, if remote execution was faster"
+              + " during a build at least once?")
   public int localExecutionDelay;
 
   @Option(
-    name = "experimental_debug_spawn_scheduler",
-    documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
-    effectTags = {OptionEffectTag.UNKNOWN},
-    defaultValue = "false"
-  )
+      name = "experimental_debug_spawn_scheduler",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      defaultValue = "false")
   public boolean debugSpawnScheduler;
 
   @Option(
@@ -130,4 +126,14 @@
               + "execution info if --experimental_require_availability_info=true. No-op if "
               + "--experimental_require_availability_info=false.")
   public List<String> availabilityInfoExempt;
+
+  @Option(
+      name = "experimental_dynamic_skip_first_build",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      defaultValue = "false",
+      help =
+          "If set, dynamic execution is turned off until there has been at least one successful"
+              + " build.")
+  public boolean skipFirstBuild;
 }
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/BUILD b/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
index 471587f..494192b 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
@@ -44,6 +44,7 @@
         "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
         "//src/main/java/com/google/devtools/build/lib/bugreport",
         "//src/main/java/com/google/devtools/build/lib/dynamic",
+        "//src/main/java/com/google/devtools/build/lib/events",
         "//src/main/java/com/google/devtools/build/lib/exec:blaze_executor",
         "//src/main/java/com/google/devtools/build/lib/exec:execution_options",
         "//src/main/java/com/google/devtools/build/lib/exec:module_action_context_registry",
@@ -71,10 +72,24 @@
     size = "small",
     srcs = ["DynamicExecutionModuleTest.java"],
     deps = [
+        "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/actions:artifacts",
+        "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
         "//src/main/java/com/google/devtools/build/lib/dynamic",
+        "//src/main/java/com/google/devtools/build/lib/events",
+        "//src/main/java/com/google/devtools/build/lib/exec:spawn_strategy_registry",
         "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
+        "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
+        "//src/main/java/com/google/devtools/build/lib/util:exit_code",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:failure_details_java_proto",
+        "//src/test/java/com/google/devtools/build/lib/actions/util",
+        "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
+        "//src/test/java/com/google/devtools/build/lib/vfs/util",
         "//third_party:guava",
+        "//third_party:jsr305",
         "//third_party:junit4",
         "//third_party:truth",
     ],
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModuleTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModuleTest.java
index 17a8c19..9ab87bc 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModuleTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicExecutionModuleTest.java
@@ -15,15 +15,47 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ActionContext.ActionContextRegistry;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactRoot;
+import com.google.devtools.build.lib.actions.BaseSpawn;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode;
+import com.google.devtools.build.lib.actions.EmptyRunfilesSupplier;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnContinuation;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction;
+import com.google.devtools.build.lib.buildtool.BuildResult;
+import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Root;
+import com.google.devtools.build.lib.vfs.util.FileSystems;
 import com.google.devtools.common.options.Converters;
 import com.google.devtools.common.options.OptionsParsingException;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
+import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -34,10 +66,12 @@
 public class DynamicExecutionModuleTest {
   private DynamicExecutionModule module;
   private DynamicExecutionOptions options;
+  private Path testRoot;
 
   @Before
-  public void setUp() {
-    module = new DynamicExecutionModule();
+  public void setUp() throws IOException {
+    testRoot = TestUtils.createUniqueTmpDir(FileSystems.getNativeFileSystem());
+    module = new DynamicExecutionModule(Executors.newCachedThreadPool());
     options = new DynamicExecutionOptions();
     options.dynamicWorkerStrategy = ""; // default
     options.dynamicLocalStrategy = Collections.emptyList(); // default
@@ -78,6 +112,96 @@
   }
 
   @Test
+  public void testRegisterSpawnStrategies_unsetsLocalStrategiesOnFirstBuild()
+      throws AbruptExitException, OptionsParsingException, UserExecException {
+    options.skipFirstBuild = true;
+    options.internalSpawnScheduler = true;
+    options.dynamicLocalStrategy = parseStrategiesToOptions("Foo=local", "local");
+    options.dynamicRemoteStrategy = parseStrategiesToOptions("Foo=remote", "remote");
+    SpawnStrategyRegistry.Builder registryBuilder = new SpawnStrategyRegistry.Builder();
+    registerStrategy(registryBuilder, "local");
+    registerStrategy(registryBuilder, "remote");
+    // "dynamic" strategy will  be added by registerSpawnStrategies().
+
+    Reporter reporter = new Reporter(new EventBus());
+    Spawn spawn = newCustomSpawn("Foo", ImmutableMap.of());
+
+    // First build skips dynamic execution
+    module.registerSpawnStrategies(registryBuilder, options, reporter);
+    assertThat(registryBuilder.build().getDynamicSpawnActionContexts(spawn, DynamicMode.LOCAL))
+        .isEmpty();
+
+    // Pretend that the build failed
+    BuildResult buildResult = new BuildResult(1L);
+    buildResult.setDetailedExitCode(
+        DetailedExitCode.of(ExitCode.ANALYSIS_FAILURE, FailureDetail.newBuilder().build()));
+    module.buildCompleteEvent(new BuildCompleteEvent(buildResult));
+
+    // Second build, after no successful builds, skips dynamic execution.
+    module.registerSpawnStrategies(registryBuilder, options, reporter);
+    assertThat(registryBuilder.build().getDynamicSpawnActionContexts(spawn, DynamicMode.LOCAL))
+        .isEmpty();
+
+    // Pretend that the build succeeded
+    buildResult.setDetailedExitCode(DetailedExitCode.success());
+    module.buildCompleteEvent(new BuildCompleteEvent(buildResult));
+
+    // Third build - since the second succeeded, we shouldn't skip any more.
+    module.registerSpawnStrategies(registryBuilder, options, reporter);
+    assertThat(registryBuilder.build().getDynamicSpawnActionContexts(spawn, DynamicMode.LOCAL))
+        .isNotEmpty();
+  }
+
+  private void registerStrategy(SpawnStrategyRegistry.Builder registryBuilder, String name) {
+    registryBuilder.registerStrategy(
+        new SandboxedSpawnStrategy() {
+          @Override
+          public ImmutableList<SpawnResult> exec(
+              Spawn spawn,
+              ActionExecutionContext actionExecutionContext,
+              @Nullable StopConcurrentSpawns stopConcurrentSpawns) {
+            return ImmutableList.of();
+          }
+
+          @Override
+          public ImmutableList<SpawnResult> exec(
+              Spawn spawn, ActionExecutionContext actionExecutionContext) {
+            return ImmutableList.of();
+          }
+
+          @Override
+          public SpawnContinuation beginExecution(
+              Spawn spawn, ActionExecutionContext actionExecutionContext)
+              throws InterruptedException {
+            return SandboxedSpawnStrategy.super.beginExecution(spawn, actionExecutionContext);
+          }
+
+          @Override
+          public boolean canExec(Spawn spawn, ActionContextRegistry actionContextRegistry) {
+            return false;
+          }
+
+          @Override
+          public boolean canExecWithLegacyFallback(
+              Spawn spawn, ActionContextRegistry actionContextRegistry) {
+            return SandboxedSpawnStrategy.super.canExecWithLegacyFallback(
+                spawn, actionContextRegistry);
+          }
+
+          @Override
+          public void usedContext(ActionContextRegistry actionContextRegistry) {
+            SandboxedSpawnStrategy.super.usedContext(actionContextRegistry);
+          }
+
+          @Override
+          public String toString() {
+            return "Test strategy \"" + name + "\"";
+          }
+        },
+        name);
+  }
+
+  @Test
   public void testGetLocalStrategies_canMixSpecificsAndGenericOptions()
       throws AbruptExitException, OptionsParsingException {
     options.dynamicLocalStrategy = parseStrategiesToOptions("Foo=local,worker", "worker");
@@ -102,4 +226,38 @@
     }
     return result;
   }
+
+  /** Constructs a new spawn with a custom mnemonic and execution info. */
+  Spawn newCustomSpawn(String mnemonic, ImmutableMap<String, String> executionInfo) {
+    Artifact inputArtifact =
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "input.txt");
+    Artifact outputArtifact =
+        ActionsTestUtil.createArtifact(
+            ArtifactRoot.asSourceRoot(Root.fromPath(testRoot)), "output.txt");
+
+    ActionExecutionMetadata action =
+        new NullActionWithMnemonic(mnemonic, ImmutableList.of(inputArtifact), outputArtifact);
+    return new BaseSpawn(
+        ImmutableList.of(),
+        ImmutableMap.of(),
+        executionInfo,
+        EmptyRunfilesSupplier.INSTANCE,
+        action,
+        ResourceSet.create(1, 0, 0));
+  }
+
+  private static class NullActionWithMnemonic extends NullAction {
+    private final String mnemonic;
+
+    private NullActionWithMnemonic(String mnemonic, List<Artifact> inputs, Artifact... outputs) {
+      super(inputs, outputs);
+      this.mnemonic = mnemonic;
+    }
+
+    @Override
+    public String getMnemonic() {
+      return mnemonic;
+    }
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
index 0bae4cf..b6ae3d7 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import com.google.common.eventbus.EventBus;
 import com.google.common.flogger.GoogleLogger;
 import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.devtools.build.lib.actions.ActionContext;
@@ -46,6 +47,7 @@
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil.NullAction;
 import com.google.devtools.build.lib.bugreport.BugReporter;
+import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.exec.BlazeExecutor;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
@@ -328,7 +330,8 @@
     }
 
     DynamicExecutionModule dynamicExecutionModule = new DynamicExecutionModule(executorService);
-    dynamicExecutionModule.registerSpawnStrategies(spawnStrategyRegistryBuilder, options);
+    dynamicExecutionModule.registerSpawnStrategies(
+        spawnStrategyRegistryBuilder, options, new Reporter(new EventBus()));
 
     SpawnStrategyRegistry spawnStrategyRegistry = spawnStrategyRegistryBuilder.build();
 
diff --git a/src/test/shell/integration/execution_strategies_test.sh b/src/test/shell/integration/execution_strategies_test.sh
index abc29bd..8fd2359 100755
--- a/src/test/shell/integration/execution_strategies_test.sh
+++ b/src/test/shell/integration/execution_strategies_test.sh
@@ -93,6 +93,22 @@
   expect_not_log "\"FooBar\" = "
 }
 
+function test_dynamic_strategy_skip_first() {
+  # Remote strategy is "worker" to make the test work both internally and externally.
+  # Since nothing is built, the actual strategy doesn't matter.
+  bazel build --internal_spawn_scheduler --spawn_strategy=dynamic \
+    --dynamic_remote_strategy=worker \
+    --dynamic_local_strategy=standalone \
+    --experimental_dynamic_skip_first_build &> $TEST_log || fail
+  expect_log "Disabling dynamic execution"
+
+  bazel build --internal_spawn_scheduler --spawn_strategy=dynamic \
+    --dynamic_remote_strategy=worker \
+    --dynamic_local_strategy=standalone \
+    --experimental_dynamic_skip_first_build &> $TEST_log || fail
+  expect_not_log "Disabling dynamic execution"
+}
+
 # Runs a build, waits for the given dir and file to appear, and then kills
 # Bazel to check what happens with said files.
 function build_and_interrupt() {