Reordering of DynamicSpawnStrategy:
- Move Branch, LocalBranch, RemoteBranch to their own files.
- Move exec{Local,Remote} to the branch classes - they still need to be static since they may be called before Branch objects get called.
- Move branchState() into Branch as an instance method.
- Move getSuffixedFileOutErr into Branch, it's only used there.
- Move remaining methods in DynamicSpawnStrategy around so they follow the logical flow: canExec() and its helper methods, followed by exec() and its helper methods, then branch termination handling.
This should not change functionality.
PiperOrigin-RevId: 421105510
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/Branch.java b/src/main/java/com/google/devtools/build/lib/dynamic/Branch.java
new file mode 100644
index 0000000..951d0fb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/Branch.java
@@ -0,0 +1,197 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.dynamic;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.GoogleLogger;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Wraps the execution of a function that is supposed to execute a spawn via a strategy and only
+ * updates the stdout/stderr files if this spawn succeeds.
+ */
+abstract class Branch implements Callable<ImmutableList<SpawnResult>> {
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+
+ /**
+ * True if this branch is still starting up, i.e. didn't get to the inner part of {@link
+ * #callImpl(ActionExecutionContext)} yet.
+ */
+ protected final AtomicBoolean starting = new AtomicBoolean(true);
+ /** The {@link Spawn} this branch is running. */
+ protected final Spawn spawn;
+ /**
+ * The {@link SettableFuture} with the results from running the spawn. Must not be null if
+ * execution succeeded.
+ */
+ protected final SettableFuture<ImmutableList<SpawnResult>> future = SettableFuture.create();
+
+ protected final AtomicReference<DynamicMode> strategyThatCancelled;
+ /** Semaphore that indicates whether this branch is done, i.e. either completed or cancelled. */
+ protected final Semaphore done = new Semaphore(0);
+
+ protected final DynamicExecutionOptions options;
+ private final DynamicMode mode;
+ protected final ActionExecutionContext context;
+
+ /**
+ * Creates a new branch of dynamic execution.
+ *
+ * @param mode the dynamic mode that this branch represents (e.g. {@link DynamicMode#REMOTE}).
+ * Used to qualify temporary files.
+ * @param context the action execution context given to the dynamic strategy, used to obtain the
+ * final location of the stdout/stderr
+ */
+ Branch(
+ DynamicMode mode,
+ ActionExecutionContext context,
+ Spawn spawn,
+ AtomicReference<DynamicMode> strategyThatCancelled,
+ DynamicExecutionOptions options) {
+ this.mode = mode;
+ this.context = context;
+ this.spawn = spawn;
+ this.strategyThatCancelled = strategyThatCancelled;
+ this.options = options;
+ }
+
+ boolean isDone() {
+ return future.isDone();
+ }
+
+ /** Returns the {@code Semaphore} indicating whether this branch is done. */
+ Semaphore getDoneSemaphore() {
+ return done;
+ }
+
+ /** Returns whether this branch has already been cancelled. */
+ boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ /** Cancels this branch. Equivalent to {@code Future.cancel(true)}. */
+ boolean cancel() {
+ return future.cancel(true);
+ }
+
+ /** Gets the results from this branch, when available. Behaves like {@link Future#get()} */
+ ImmutableList<SpawnResult> getResults() throws ExecutionException, InterruptedException {
+ return future.get();
+ }
+
+ public Spawn getSpawn() {
+ return spawn;
+ }
+
+ /** Returns a human-readable description of what we can tell about the state of this Future. */
+ String branchState() {
+ return (isCancelled() ? "cancelled" : "not cancelled")
+ + " and "
+ + (isDone() ? "done" : "not done");
+ }
+
+ /**
+ * Moves a set of stdout/stderr files over another one. Errors during the move are logged and
+ * swallowed.
+ *
+ * @param from the source location
+ * @param to the target location
+ */
+ private static void moveFileOutErr(FileOutErr from, FileOutErr to) {
+ try {
+ if (from.getOutputPath().exists()) {
+ Files.move(from.getOutputPath().getPathFile(), to.getOutputPath().getPathFile());
+ }
+ if (from.getErrorPath().exists()) {
+ Files.move(from.getErrorPath().getPathFile(), to.getErrorPath().getPathFile());
+ }
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log("Could not move action logs from execution");
+ }
+ }
+
+ private static FileOutErr getSuffixedFileOutErr(FileOutErr fileOutErr, String suffix) {
+ Path outDir = checkNotNull(fileOutErr.getOutputPath().getParentDirectory());
+ String outBaseName = fileOutErr.getOutputPath().getBaseName();
+ Path errDir = checkNotNull(fileOutErr.getErrorPath().getParentDirectory());
+ String errBaseName = fileOutErr.getErrorPath().getBaseName();
+ return new FileOutErr(
+ outDir.getChild(outBaseName + suffix), errDir.getChild(errBaseName + suffix));
+ }
+
+ /**
+ * Hook to execute a spawn using an arbitrary strategy.
+ *
+ * @param context the action execution context where the spawn can write its stdout/stderr. The
+ * location of these files is specific to this branch.
+ * @return the spawn results if execution was successful
+ * @throws InterruptedException if the branch was cancelled or an interrupt was caught
+ * @throws ExecException if the spawn execution fails
+ */
+ abstract ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
+ throws InterruptedException, ExecException;
+
+ /**
+ * Executes the {@link #callImpl} hook and handles stdout/stderr.
+ *
+ * @return the spawn results if execution was successful
+ * @throws InterruptedException if the branch was cancelled or an interrupt was caught
+ * @throws ExecException if the spawn execution fails
+ */
+ @Override
+ public final ImmutableList<SpawnResult> call() throws InterruptedException, ExecException {
+ FileOutErr fileOutErr = getSuffixedFileOutErr(context.getFileOutErr(), "." + mode.name());
+
+ ImmutableList<SpawnResult> results = null;
+ ExecException exception = null;
+ try {
+ results = callImpl(context.withFileOutErr(fileOutErr));
+ } catch (ExecException e) {
+ exception = e;
+ } finally {
+ try {
+ fileOutErr.close();
+ } catch (IOException ignored) {
+ // Nothing we can do here.
+ }
+ }
+
+ moveFileOutErr(fileOutErr, context.getFileOutErr());
+
+ if (exception != null) {
+ throw exception;
+ } else {
+ checkNotNull(results);
+ return results;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
index fa587f1..471a05a 100644
--- a/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategy.java
@@ -13,7 +13,6 @@
// limitations under the License.
package com.google.devtools.build.lib.dynamic;
-import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode.LOCAL;
import static com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode.REMOTE;
@@ -22,7 +21,6 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
-import com.google.common.io.Files;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
@@ -36,7 +34,6 @@
import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnResult;
-import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.SpawnStrategy;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.dynamic.DynamicExecutionModule.IgnoreFailureCheck;
@@ -46,18 +43,13 @@
import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution;
import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
-import com.google.devtools.build.lib.util.io.FileOutErr;
-import com.google.devtools.build.lib.vfs.Path;
import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;
-import java.io.IOException;
import java.util.List;
import java.util.Optional;
-import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -137,84 +129,389 @@
this.ignoreFailureCheck = ignoreFailureCheck;
}
- /**
- * Cancels and waits for a branch (a spawn execution) to terminate.
- *
- * <p>This is intended to be used as the body of the {@link
- * SandboxedSpawnStrategy.StopConcurrentSpawns} lambda passed to the spawn runners. Each strategy
- * may call this at most once.
- *
- * @param otherBranch The other branch, the one that should be cancelled.
- * @param cancellingBranch The branch that is performing the cancellation.
- * @param cancellingStrategy identifier of the strategy that is performing the cancellation. Used
- * to prevent cross-cancellations and to check that the same strategy doesn't issue the
- * cancellation twice.
- * @param strategyThatCancelled name of the first strategy that executed this method, or a null
- * reference if this is the first time this method is called. If not null, we expect the value
- * referenced by this to be different than {@code cancellingStrategy}, or else we have a bug.
- * @param options The options for dynamic execution.
- * @param context The context of this action execution.
- * @throws InterruptedException if we get interrupted for any reason trying to cancel the future
- * @throws DynamicInterruptedException if we lost a race against another strategy trying to cancel
- * us
- */
- private static void stopBranch(
- Branch otherBranch,
- Branch cancellingBranch,
- DynamicMode cancellingStrategy,
- AtomicReference<DynamicMode> strategyThatCancelled,
- DynamicExecutionOptions options,
- ActionExecutionContext context)
- throws InterruptedException {
- if (cancellingBranch.isCancelled()) {
- // TODO(b/173020239): Determine why stopBranch() can be called when cancellingBranch is
- // cancelled.
- throw new DynamicInterruptedException(
- String.format(
- "Execution of %s strategy stopped because it was cancelled but not interrupted",
- cancellingStrategy));
- }
- // This multi-step, unlocked access to "strategyThatCancelled" is valid because, for a given
- // value of "cancellingStrategy", we do not expect concurrent calls to this method. (If there
- // are, we are in big trouble.)
- DynamicMode current = strategyThatCancelled.get();
- if (cancellingStrategy.equals(current)) {
- throw new AssertionError("stopBranch called more than once by " + cancellingStrategy);
- } else {
- // Protect against the two branches from cancelling each other. The first branch to set the
- // reference to its own identifier wins and is allowed to issue the cancellation; the other
- // branch just has to give up execution.
- if (strategyThatCancelled.compareAndSet(null, cancellingStrategy)) {
- if (options.debugSpawnScheduler) {
- context
- .getEventHandler()
- .handle(
- Event.info(
- String.format(
- "%s action finished %sly and was %s",
- cancellingBranch.getSpawn().getMnemonic(),
- strategyThatCancelled.get(),
- cancellingBranch.isCancelled() ? "cancelled" : "not cancelled")));
- }
+ @Override
+ public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
+ ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
+ DynamicStrategyRegistry dynamicStrategyRegistry =
+ actionContextRegistry.getContext(DynamicStrategyRegistry.class);
- if (!otherBranch.cancel()) {
- // This can happen if the other branch is local under local_lockfree and has returned
- // its result but not yet cancelled this branch, or if the other branch was already
- // cancelled for other reasons. In the latter case, we are good to continue.
- if (!otherBranch.isCancelled()) {
- throw new DynamicInterruptedException(
- String.format(
- "Execution of %s strategy stopped because %s strategy could not be cancelled",
- cancellingStrategy, cancellingStrategy.other()));
+ return canExecLocal(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry)
+ || canExecRemote(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry);
+ }
+
+ private boolean canExecLocal(
+ Spawn spawn,
+ ExecutionPolicy mainSpawnExecutionPolicy,
+ ActionContext.ActionContextRegistry actionContextRegistry,
+ DynamicStrategyRegistry dynamicStrategyRegistry) {
+ if (!canExecLocalSpawn(
+ spawn, mainSpawnExecutionPolicy, actionContextRegistry, dynamicStrategyRegistry)) {
+ return false;
+ }
+ // Present if there is a extra local spawn. Unset if not.
+ Optional<Boolean> canLocalSpawn =
+ getExtraSpawnForLocalExecution
+ .apply(spawn)
+ .map(
+ extraSpawn ->
+ canExecLocalSpawn(
+ extraSpawn,
+ getExecutionPolicy.apply(extraSpawn),
+ actionContextRegistry,
+ dynamicStrategyRegistry));
+ return canLocalSpawn.orElse(true);
+ }
+
+ private static boolean canExecLocalSpawn(
+ Spawn spawn,
+ ExecutionPolicy executionPolicy,
+ ActionContext.ActionContextRegistry actionContextRegistry,
+ DynamicStrategyRegistry dynamicStrategyRegistry) {
+ if (!executionPolicy.canRunLocally()) {
+ return false;
+ }
+ List<SandboxedSpawnStrategy> localStrategies =
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, LOCAL);
+ return localStrategies.stream()
+ .anyMatch(
+ s ->
+ (s.canExec(spawn, actionContextRegistry)
+ || s.canExecWithLegacyFallback(spawn, actionContextRegistry)));
+ }
+
+ private static boolean canExecRemote(
+ Spawn spawn,
+ ExecutionPolicy executionPolicy,
+ ActionContext.ActionContextRegistry actionContextRegistry,
+ DynamicStrategyRegistry dynamicStrategyRegistry) {
+ if (!executionPolicy.canRunRemotely()) {
+ return false;
+ }
+ List<SandboxedSpawnStrategy> remoteStrategies =
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(
+ spawn, DynamicStrategyRegistry.DynamicMode.REMOTE);
+ return remoteStrategies.stream().anyMatch(s -> s.canExec(spawn, actionContextRegistry));
+ }
+
+ @Override
+ public ImmutableList<SpawnResult> exec(
+ final Spawn spawn, final ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException {
+ DynamicSpawnStrategy.verifyAvailabilityInfo(options, spawn);
+ ImmutableList<SpawnResult> nonDynamicResults =
+ maybeExecuteNonDynamically(spawn, actionExecutionContext);
+ if (nonDynamicResults != null) {
+ return nonDynamicResults;
+ }
+
+ // True if we got the threads we need for actual dynamic execution.
+ boolean gotThreads = false;
+ try {
+ if (threadLimiter.tryAcquire()) {
+ gotThreads = true;
+ } else {
+ // If there are no threads available for dynamic execution because we're limited
+ // to the number of CPUs, we can just execute remotely.
+ ImmutableList<SpawnResult> spawnResults =
+ RemoteBranch.runRemotely(spawn, actionExecutionContext, null);
+ for (SpawnResult r : spawnResults) {
+ if (r.isCacheHit()) {
+ delayLocalExecution.set(true);
+ break;
}
}
- otherBranch.getDoneSemaphore().acquire();
- } else {
- throw new DynamicInterruptedException(
- String.format(
- "Execution of %s strategy stopped because %s strategy finished first",
- cancellingStrategy, strategyThatCancelled.get()));
+ return spawnResults;
}
+
+ // Extra logging to debug b/194373457
+ logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
+ "Spawn %s dynamically executed both ways", spawn.getResourceOwner().describe());
+ debugLog("Dynamic execution of %s beginning%n", spawn.getResourceOwner().prettyPrint());
+ // else both can exec. Fallthrough to below.
+
+ // Semaphores to track termination of each branch. These are necessary to wait for the branch
+ // to finish its own cleanup (e.g. terminating subprocesses) once it has been cancelled.
+
+ AtomicReference<DynamicMode> strategyThatCancelled = new AtomicReference<>(null);
+
+ LocalBranch localBranch =
+ new LocalBranch(
+ actionExecutionContext,
+ spawn,
+ strategyThatCancelled,
+ options,
+ ignoreFailureCheck,
+ getExtraSpawnForLocalExecution,
+ delayLocalExecution);
+ RemoteBranch remoteBranch =
+ new RemoteBranch(
+ actionExecutionContext,
+ spawn,
+ strategyThatCancelled,
+ options,
+ ignoreFailureCheck,
+ delayLocalExecution);
+
+ SettableFuture<ImmutableList<SpawnResult>> localFuture =
+ localBranch.prepareFuture(remoteBranch);
+ SettableFuture<ImmutableList<SpawnResult>> remoteFuture =
+ remoteBranch.prepareFuture(localBranch);
+ localFuture.setFuture(executorService.submit(localBranch));
+ remoteFuture.setFuture(executorService.submit(remoteBranch));
+
+ try {
+ return waitBranches(localBranch, remoteBranch, spawn, options, actionExecutionContext);
+ } finally {
+ checkState(localBranch.isDone());
+ checkState(remoteBranch.isDone());
+ logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
+ "Dynamic execution of %s ended with local %s, remote %s%n",
+ spawn.getResourceOwner().prettyPrint(),
+ localBranch.isCancelled() ? "cancelled" : "done",
+ remoteBranch.isCancelled() ? "cancelled" : "done");
+ debugLog(
+ "Dynamic execution of %s ended with local %s, remote %s%n",
+ spawn.getResourceOwner().prettyPrint(),
+ localBranch.isCancelled() ? "cancelled" : "done",
+ remoteBranch.isCancelled() ? "cancelled" : "done");
+ }
+ } finally {
+ if (gotThreads) {
+ threadLimiter.release();
+ }
+ }
+ }
+
+ /**
+ * Checks if the given spawn has the right execution requirements to indicate whether it can
+ * succeed when running remotely and/or locally depending on the Xcode versions it needs.
+ *
+ * @param options the dynamic execution options that configure this check
+ * @param spawn the spawn to validate
+ * @throws ExecException if the spawn does not contain the expected execution requirements
+ */
+ static void verifyAvailabilityInfo(DynamicExecutionOptions options, Spawn spawn)
+ throws ExecException {
+ if (options.requireAvailabilityInfo
+ && !options.availabilityInfoExempt.contains(spawn.getMnemonic())) {
+ if (spawn.getExecutionInfo().containsKey(ExecutionRequirements.REQUIRES_DARWIN)
+ && !spawn.getExecutionInfo().containsKey(ExecutionRequirements.REQUIREMENTS_SET)) {
+ String message =
+ String.format(
+ "The following spawn was missing Xcode-related execution requirements. Please"
+ + " let the Bazel team know if you encounter this issue. You can work around"
+ + " this error by passing --experimental_require_availability_info=false --"
+ + " at your own risk! This may cause some actions to be executed on the"
+ + " wrong platform, which can result in build failures.\n"
+ + "Failing spawn: mnemonic = %s\n"
+ + "tool files = %s\n"
+ + "execution platform = %s\n"
+ + "execution info = %s\n",
+ spawn.getMnemonic(),
+ spawn.getToolFiles(),
+ spawn.getExecutionPlatform(),
+ spawn.getExecutionInfo());
+
+ FailureDetail detail =
+ FailureDetail.newBuilder()
+ .setMessage(message)
+ .setDynamicExecution(
+ DynamicExecution.newBuilder().setCode(Code.XCODE_RELATED_PREREQ_UNMET))
+ .build();
+ throw new EnvironmentalExecException(detail);
+ }
+ }
+ }
+
+ /**
+ * Checks if this action should be executed dynamically, and if not executes it locally or
+ * remotely as applicable, or throws an exception if it cannot be executed at all.
+ *
+ * @param spawn Spawn in the process of being executed.
+ * @param actionExecutionContext Execution context
+ * @return Results from execution if the action was executed (possibly empty) or null if this
+ * action can be executed dynamically.
+ * @throws ExecException If we tried to execute and executed failed.
+ * @throws InterruptedException If we tried to execute and got interrupted.
+ */
+ @Nullable
+ private ImmutableList<SpawnResult> maybeExecuteNonDynamically(
+ Spawn spawn, ActionExecutionContext actionExecutionContext)
+ throws ExecException, InterruptedException {
+ ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
+
+ DynamicStrategyRegistry dynamicStrategyRegistry =
+ actionExecutionContext.getContext(DynamicStrategyRegistry.class);
+ boolean localCanExec =
+ canExecLocal(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
+
+ boolean remoteCanExec =
+ canExecRemote(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
+
+ if (!localCanExec && !remoteCanExec) {
+ FailureDetail failure =
+ FailureDetail.newBuilder()
+ .setMessage(
+ getNoCanExecFailureMessage(
+ spawn, executionPolicy.canRunLocally(), executionPolicy.canRunRemotely()))
+ .setDynamicExecution(
+ DynamicExecution.newBuilder().setCode(Code.NO_USABLE_STRATEGY_FOUND).build())
+ .setSpawn(
+ FailureDetails.Spawn.newBuilder()
+ .setCode(FailureDetails.Spawn.Code.NO_USABLE_STRATEGY_FOUND)
+ .build())
+ .build();
+ debugLog(
+ "Dynamic execution of %s can be done neither locally nor remotely%n",
+ spawn.getResourceOwner().prettyPrint());
+ throw new UserExecException(failure);
+ } else if (!localCanExec && remoteCanExec) {
+ // Extra logging to debug b/194373457
+ logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
+ "Dynamic execution of %s can only be done remotely: Local execution policy %s it, "
+ + "local strategies are %s.%n",
+ spawn.getResourceOwner().prettyPrint(),
+ executionPolicy.canRunLocally() ? "allows" : "forbids",
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, DynamicMode.LOCAL));
+ debugLog(
+ "Dynamic execution of %s can only be done remotely: Local execution policy %s it, "
+ + "local strategies are %s.%n",
+ spawn.getResourceOwner().prettyPrint(),
+ executionPolicy.canRunLocally() ? "allows" : "forbids",
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, DynamicMode.LOCAL));
+ return RemoteBranch.runRemotely(spawn, actionExecutionContext, null);
+ } else if (localCanExec && !remoteCanExec) {
+ // Extra logging to debug b/194373457
+ logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
+ "Dynamic execution of %s can only be done locally: Remote execution policy %s it, "
+ + "remote strategies are %s.%n",
+ spawn.getResourceOwner().prettyPrint(),
+ executionPolicy.canRunRemotely() ? "allows" : "forbids",
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, DynamicMode.REMOTE));
+ debugLog(
+ "Dynamic execution of %s can only be done locally: Remote execution policy %s it, "
+ + "remote strategies are %s.%n",
+ spawn.getResourceOwner().prettyPrint(),
+ executionPolicy.canRunRemotely() ? "allows" : "forbids",
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, DynamicMode.REMOTE));
+ return LocalBranch.runLocally(
+ spawn, actionExecutionContext, null, getExtraSpawnForLocalExecution);
+ } else if (options.skipFirstBuild && firstBuild) {
+ if (!skipBuildWarningShown) {
+ skipBuildWarningShown = true;
+ actionExecutionContext
+ .getEventHandler()
+ .handle(
+ Event.info(
+ "Disabling dynamic execution until we have seen a successful build, see"
+ + " --experimental_dynamic_skip_first_build."));
+ }
+ return RemoteBranch.runRemotely(spawn, actionExecutionContext, null);
+ }
+ return null;
+ }
+
+ /**
+ * Returns an error string for being unable to execute locally and/or remotely the given execution
+ * state.
+ *
+ * <p>Usage note, this method is only to be called after an impossible condition is already
+ * detected by the caller, as all this does is give an error string to put in the exception.
+ *
+ * @param spawn The action that needs to be executed.
+ * @param localAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
+ * trying local execution.
+ * @param remoteAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
+ * trying remote execution.
+ */
+ private static String getNoCanExecFailureMessage(
+ Spawn spawn,
+ boolean localAllowedBySpawnExecutionPolicy,
+ boolean remoteAllowedBySpawnExecutionPolicy) {
+ // TODO(b/188387840): Can't use Spawn.toString() here because tests report FakeOwner instances
+ // as the resource owner, and those cause toStrings to throw if no primary output.
+ // TODO(b/188402092): Even if the above is fixed, we still don't want to use Spawn.toString()
+ // until the mnemonic is included in the output unconditionally. Too useful for the error
+ // message.
+ if (!localAllowedBySpawnExecutionPolicy && !remoteAllowedBySpawnExecutionPolicy) {
+ return "Neither local nor remote execution allowed for action " + spawn.getMnemonic();
+ } else if (!remoteAllowedBySpawnExecutionPolicy) {
+ return "No usable dynamic_local_strategy found (and remote execution disabled) for action "
+ + spawn.getMnemonic();
+ } else if (!localAllowedBySpawnExecutionPolicy) {
+ return "No usable dynamic_remote_strategy found (and local execution disabled) for action "
+ + spawn.getMnemonic();
+ } else {
+ return "No usable dynamic_local_strategy or dynamic_remote_strategy found for action "
+ + spawn.getMnemonic();
+ }
+ }
+
+ /**
+ * Waits for the two branches of a spawn's execution to complete.
+ *
+ * <p>This guarantees that the two branches are stopped both on successful termination and on an
+ * exception.
+ *
+ * @param localBranch the future running the local side of the spawn. This future must cancel
+ * {@code remoteBranch} at some point during its successful execution to guarantee
+ * termination. If we encounter an execution error, or if we are interrupted, then we handle
+ * such cancellation here.
+ * @param remoteBranch the future running the remote side of the spawn. Same restrictions apply as
+ * in {@code localBranch}, but in the symmetric direction.
+ * @param options the options relevant for dynamic execution
+ * @param context execution context object
+ * @return the result of the branch that terminates first
+ * @throws ExecException the execution error of the spawn that terminated first
+ * @throws InterruptedException if we get interrupted while waiting for completion
+ */
+ @VisibleForTesting
+ static ImmutableList<SpawnResult> waitBranches(
+ LocalBranch localBranch,
+ RemoteBranch remoteBranch,
+ Spawn spawn,
+ DynamicExecutionOptions options,
+ ActionExecutionContext context)
+ throws ExecException, InterruptedException {
+ ImmutableList<SpawnResult> localResult;
+ try {
+ localResult = waitBranch(localBranch, options, LOCAL, context);
+ } catch (ExecException | InterruptedException | RuntimeException e) {
+ if (options.debugSpawnScheduler) {
+ context
+ .getEventHandler()
+ .handle(
+ Event.info(
+ String.format(
+ "Cancelling remote branch of %s after local exception %s",
+ spawn.getResourceOwner().prettyPrint(), e.getMessage())));
+ }
+ remoteBranch.cancel();
+ throw e;
+ }
+
+ ImmutableList<SpawnResult> remoteResult = waitBranch(remoteBranch, options, REMOTE, context);
+
+ if (remoteResult != null && localResult != null) {
+ throw new AssertionError(
+ String.format(
+ "Neither branch of %s cancelled the other one. Local was %s and remote was %s.",
+ spawn.getResourceOwner().getPrimaryOutput().prettyPrint(),
+ localBranch.branchState(),
+ remoteBranch.branchState()));
+ } else if (localResult != null) {
+ return localResult;
+ } else if (remoteResult != null) {
+ return remoteResult;
+ } else {
+ // TODO(b/173153395): Sometimes gets thrown for currently unknown reasons.
+ // (sometimes happens in relation to the whole dynamic execution being cancelled)
+ throw new AssertionError(
+ String.format(
+ "Neither branch of %s completed. Local was %s and remote was %s.",
+ spawn.getResourceOwner().getPrimaryOutput().prettyPrint(),
+ localBranch.branchState(),
+ remoteBranch.branchState()));
}
}
@@ -297,221 +594,86 @@
}
/**
- * Waits for the two branches of a spawn's execution to complete.
+ * Cancels and waits for a branch (a spawn execution) to terminate.
*
- * <p>This guarantees that the two branches are stopped both on successful termination and on an
- * exception.
+ * <p>This is intended to be used as the body of the {@link
+ * SandboxedSpawnStrategy.StopConcurrentSpawns} lambda passed to the spawn runners. Each strategy
+ * may call this at most once.
*
- * @param localBranch the future running the local side of the spawn. This future must cancel
- * {@code remoteBranch} at some point during its successful execution to guarantee
- * termination. If we encounter an execution error, or if we are interrupted, then we handle
- * such cancellation here.
- * @param remoteBranch the future running the remote side of the spawn. Same restrictions apply as
- * in {@code localBranch}, but in the symmetric direction.
- * @param options the options relevant for dynamic execution
- * @param context execution context object
- * @return the result of the branch that terminates first
- * @throws ExecException the execution error of the spawn that terminated first
- * @throws InterruptedException if we get interrupted while waiting for completion
+ * @param otherBranch The other branch, the one that should be cancelled.
+ * @param cancellingBranch The branch that is performing the cancellation.
+ * @param cancellingStrategy identifier of the strategy that is performing the cancellation. Used
+ * to prevent cross-cancellations and to check that the same strategy doesn't issue the
+ * cancellation twice.
+ * @param strategyThatCancelled name of the first strategy that executed this method, or a null
+ * reference if this is the first time this method is called. If not null, we expect the value
+ * referenced by this to be different than {@code cancellingStrategy}, or else we have a bug.
+ * @param options The options for dynamic execution.
+ * @param context The context of this action execution.
+ * @throws InterruptedException if we get interrupted for any reason trying to cancel the future
+ * @throws DynamicInterruptedException if we lost a race against another strategy trying to cancel
+ * us
*/
- @VisibleForTesting
- static ImmutableList<SpawnResult> waitBranches(
- LocalBranch localBranch,
- RemoteBranch remoteBranch,
- Spawn spawn,
+ static void stopBranch(
+ Branch otherBranch,
+ Branch cancellingBranch,
+ DynamicMode cancellingStrategy,
+ AtomicReference<DynamicMode> strategyThatCancelled,
DynamicExecutionOptions options,
ActionExecutionContext context)
- throws ExecException, InterruptedException {
- ImmutableList<SpawnResult> localResult;
- try {
- localResult = waitBranch(localBranch, options, LOCAL, context);
- } catch (ExecException | InterruptedException | RuntimeException e) {
- if (options.debugSpawnScheduler) {
- context
- .getEventHandler()
- .handle(
- Event.info(
- String.format(
- "Cancelling remote branch of %s after local exception %s",
- spawn.getResourceOwner().prettyPrint(), e.getMessage())));
- }
- remoteBranch.cancel();
- throw e;
- }
-
- ImmutableList<SpawnResult> remoteResult = waitBranch(remoteBranch, options, REMOTE, context);
-
- if (remoteResult != null && localResult != null) {
- throw new AssertionError(
+ throws InterruptedException {
+ if (cancellingBranch.isCancelled()) {
+ // TODO(b/173020239): Determine why stopBranch() can be called when cancellingBranch is
+ // cancelled.
+ throw new DynamicInterruptedException(
String.format(
- "Neither branch of %s cancelled the other one. Local was %s and remote was %s.",
- spawn.getResourceOwner().getPrimaryOutput().prettyPrint(),
- branchState(localBranch),
- branchState(remoteBranch)));
- } else if (localResult != null) {
- return localResult;
- } else if (remoteResult != null) {
- return remoteResult;
+ "Execution of %s strategy stopped because it was cancelled but not interrupted",
+ cancellingStrategy));
+ }
+ // This multi-step, unlocked access to "strategyThatCancelled" is valid because, for a given
+ // value of "cancellingStrategy", we do not expect concurrent calls to this method. (If there
+ // are, we are in big trouble.)
+ DynamicMode current = strategyThatCancelled.get();
+ if (cancellingStrategy.equals(current)) {
+ throw new AssertionError("stopBranch called more than once by " + cancellingStrategy);
} else {
- // TODO(b/173153395): Sometimes gets thrown for currently unknown reasons.
- // (sometimes happens in relation to the whole dynamic execution being cancelled)
- throw new AssertionError(
- String.format(
- "Neither branch of %s completed. Local was %s and remote was %s.",
- spawn.getResourceOwner().getPrimaryOutput().prettyPrint(),
- branchState(localBranch),
- branchState(remoteBranch)));
- }
- }
+ // Protect against the two branches from cancelling each other. The first branch to set the
+ // reference to its own identifier wins and is allowed to issue the cancellation; the other
+ // branch just has to give up execution.
+ if (strategyThatCancelled.compareAndSet(null, cancellingStrategy)) {
+ if (options.debugSpawnScheduler) {
+ context
+ .getEventHandler()
+ .handle(
+ Event.info(
+ String.format(
+ "%s action finished %sly and was %s",
+ cancellingBranch.getSpawn().getMnemonic(),
+ strategyThatCancelled.get(),
+ cancellingBranch.isCancelled() ? "cancelled" : "not cancelled")));
+ }
- /** Returns a human-readable description of what we can tell about the state of this Future. */
- private static String branchState(Branch branch) {
- return (branch.isCancelled() ? "cancelled" : "not cancelled")
- + " and "
- + (branch.isDone() ? "done" : "not done");
- }
-
- /**
- * Checks if the given spawn has the right execution requirements to indicate whether it can
- * succeed when running remotely and/or locally depending on the Xcode versions it needs.
- *
- * @param options the dynamic execution options that configure this check
- * @param spawn the spawn to validate
- * @throws ExecException if the spawn does not contain the expected execution requirements
- */
- static void verifyAvailabilityInfo(DynamicExecutionOptions options, Spawn spawn)
- throws ExecException {
- if (options.requireAvailabilityInfo
- && !options.availabilityInfoExempt.contains(spawn.getMnemonic())) {
- if (spawn.getExecutionInfo().containsKey(ExecutionRequirements.REQUIRES_DARWIN)
- && !spawn.getExecutionInfo().containsKey(ExecutionRequirements.REQUIREMENTS_SET)) {
- String message =
+ if (!otherBranch.cancel()) {
+ // This can happen if the other branch is local under local_lockfree and has returned
+ // its result but not yet cancelled this branch, or if the other branch was already
+ // cancelled for other reasons. In the latter case, we are good to continue.
+ if (!otherBranch.isCancelled()) {
+ throw new DynamicInterruptedException(
+ String.format(
+ "Execution of %s strategy stopped because %s strategy could not be cancelled",
+ cancellingStrategy, cancellingStrategy.other()));
+ }
+ }
+ otherBranch.getDoneSemaphore().acquire();
+ } else {
+ throw new DynamicInterruptedException(
String.format(
- "The following spawn was missing Xcode-related execution requirements. Please"
- + " let the Bazel team know if you encounter this issue. You can work around"
- + " this error by passing --experimental_require_availability_info=false --"
- + " at your own risk! This may cause some actions to be executed on the"
- + " wrong platform, which can result in build failures.\n"
- + "Failing spawn: mnemonic = %s\n"
- + "tool files = %s\n"
- + "execution platform = %s\n"
- + "execution info = %s\n",
- spawn.getMnemonic(),
- spawn.getToolFiles(),
- spawn.getExecutionPlatform(),
- spawn.getExecutionInfo());
-
- FailureDetail detail =
- FailureDetail.newBuilder()
- .setMessage(message)
- .setDynamicExecution(
- DynamicExecution.newBuilder().setCode(Code.XCODE_RELATED_PREREQ_UNMET))
- .build();
- throw new EnvironmentalExecException(detail);
+ "Execution of %s strategy stopped because %s strategy finished first",
+ cancellingStrategy, strategyThatCancelled.get()));
}
}
}
- private static boolean canExecLocalSpawn(
- Spawn spawn,
- ExecutionPolicy executionPolicy,
- ActionContext.ActionContextRegistry actionContextRegistry,
- DynamicStrategyRegistry dynamicStrategyRegistry) {
- if (!executionPolicy.canRunLocally()) {
- return false;
- }
- List<SandboxedSpawnStrategy> localStrategies =
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, LOCAL);
- return localStrategies.stream()
- .anyMatch(
- s ->
- (s.canExec(spawn, actionContextRegistry)
- || s.canExecWithLegacyFallback(spawn, actionContextRegistry)));
- }
-
- private boolean canExecLocal(
- Spawn spawn,
- ExecutionPolicy mainSpawnExecutionPolicy,
- ActionContext.ActionContextRegistry actionContextRegistry,
- DynamicStrategyRegistry dynamicStrategyRegistry) {
- if (!canExecLocalSpawn(
- spawn, mainSpawnExecutionPolicy, actionContextRegistry, dynamicStrategyRegistry)) {
- return false;
- }
- // Present if there is a extra local spawn. Unset if not.
- Optional<Boolean> canLocalSpawn =
- getExtraSpawnForLocalExecution
- .apply(spawn)
- .map(
- extraSpawn ->
- canExecLocalSpawn(
- extraSpawn,
- getExecutionPolicy.apply(extraSpawn),
- actionContextRegistry,
- dynamicStrategyRegistry));
- return canLocalSpawn.orElse(true);
- }
-
- private static boolean canExecRemote(
- Spawn spawn,
- ExecutionPolicy executionPolicy,
- ActionContext.ActionContextRegistry actionContextRegistry,
- DynamicStrategyRegistry dynamicStrategyRegistry) {
- if (!executionPolicy.canRunRemotely()) {
- return false;
- }
- List<SandboxedSpawnStrategy> remoteStrategies =
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.REMOTE);
- return remoteStrategies.stream().anyMatch(s -> s.canExec(spawn, actionContextRegistry));
- }
-
- @Override
- public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
- ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
- DynamicStrategyRegistry dynamicStrategyRegistry =
- actionContextRegistry.getContext(DynamicStrategyRegistry.class);
-
- return canExecLocal(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry)
- || canExecRemote(spawn, executionPolicy, actionContextRegistry, dynamicStrategyRegistry);
- }
-
- /**
- * Returns an error string for being unable to execute locally and/or remotely the given execution
- * state.
- *
- * <p>Usage note, this method is only to be called after an impossible condition is already
- * detected by the caller, as all this does is give an error string to put in the exception.
- *
- * @param spawn The action that needs to be executed
- * @param localAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
- * trying local execution.
- * @param remoteAllowedBySpawnExecutionPolicy whether the execution policy for this spawn allows
- * trying remote execution.
- */
- private static String getNoCanExecFailureMessage(
- Spawn spawn,
- boolean localAllowedBySpawnExecutionPolicy,
- boolean remoteAllowedBySpawnExecutionPolicy) {
- // TODO(b/188387840): Can't use Spawn.toString() here because tests report FakeOwner instances
- // as the resource owner, and those cause toStrings to throw if no primary output.
- // TODO(b/188402092): Even if the above is fixed, we still don't want to use Spawn.toString()
- // until the mnemonic is included in the output unconditionally. Too useful for the error
- // message.
- if (!localAllowedBySpawnExecutionPolicy && !remoteAllowedBySpawnExecutionPolicy) {
- return "Neither local nor remote execution allowed for action " + spawn.getMnemonic();
- } else if (!remoteAllowedBySpawnExecutionPolicy) {
- return "No usable dynamic_local_strategy found (and remote execution disabled) for action "
- + spawn.getMnemonic();
- } else if (!localAllowedBySpawnExecutionPolicy) {
- return "No usable dynamic_remote_strategy found (and local execution disabled) for action "
- + spawn.getMnemonic();
- } else {
- return "No usable dynamic_local_strategy or dynamic_remote_strategy found for action "
- + spawn.getMnemonic();
- }
- }
-
@FormatMethod
private void stepLog(
Level level, @Nullable Throwable cause, @FormatString String fmt, Object... args) {
@@ -526,650 +688,12 @@
}
@Override
- public ImmutableList<SpawnResult> exec(
- final Spawn spawn, final ActionExecutionContext actionExecutionContext)
- throws ExecException, InterruptedException {
- DynamicSpawnStrategy.verifyAvailabilityInfo(options, spawn);
- ExecutionPolicy executionPolicy = getExecutionPolicy.apply(spawn);
-
- DynamicStrategyRegistry dynamicStrategyRegistry =
- actionExecutionContext.getContext(DynamicStrategyRegistry.class);
- boolean localCanExec =
- canExecLocal(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
-
- boolean remoteCanExec =
- canExecRemote(spawn, executionPolicy, actionExecutionContext, dynamicStrategyRegistry);
-
- if (!localCanExec && !remoteCanExec) {
- FailureDetail failure =
- FailureDetail.newBuilder()
- .setMessage(
- getNoCanExecFailureMessage(
- spawn, executionPolicy.canRunLocally(), executionPolicy.canRunRemotely()))
- .setDynamicExecution(
- DynamicExecution.newBuilder().setCode(Code.NO_USABLE_STRATEGY_FOUND).build())
- .setSpawn(
- FailureDetails.Spawn.newBuilder()
- .setCode(FailureDetails.Spawn.Code.NO_USABLE_STRATEGY_FOUND)
- .build())
- .build();
- debugLog(
- "Dynamic execution of %s can be done neither locally nor remotely%n",
- spawn.getResourceOwner().prettyPrint());
- throw new UserExecException(failure);
- } else if (!localCanExec && remoteCanExec) {
- // Extra logging to debug b/194373457
- logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
- "Dynamic execution of %s can only be done remotely: Local execution policy %s it, "
- + "local strategies are %s.%n",
- spawn.getResourceOwner().prettyPrint(),
- executionPolicy.canRunLocally() ? "allows" : "forbids",
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.LOCAL));
- debugLog(
- "Dynamic execution of %s can only be done remotely: Local execution policy %s it, "
- + "local strategies are %s.%n",
- spawn.getResourceOwner().prettyPrint(),
- executionPolicy.canRunLocally() ? "allows" : "forbids",
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.LOCAL));
- return runRemotely(spawn, actionExecutionContext, null);
- } else if (localCanExec && !remoteCanExec) {
- // Extra logging to debug b/194373457
- logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
- "Dynamic execution of %s can only be done locally: Remote execution policy %s it, "
- + "remote strategies are %s.%n",
- spawn.getResourceOwner().prettyPrint(),
- executionPolicy.canRunRemotely() ? "allows" : "forbids",
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.REMOTE));
- debugLog(
- "Dynamic execution of %s can only be done locally: Remote execution policy %s it, "
- + "remote strategies are %s.%n",
- spawn.getResourceOwner().prettyPrint(),
- executionPolicy.canRunRemotely() ? "allows" : "forbids",
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.REMOTE));
- return runLocally(spawn, actionExecutionContext, null, getExtraSpawnForLocalExecution);
- } else if (options.skipFirstBuild && firstBuild) {
- if (!skipBuildWarningShown) {
- skipBuildWarningShown = true;
- actionExecutionContext
- .getEventHandler()
- .handle(
- Event.info(
- "Disabling dynamic execution until we have seen a successful build, see"
- + " --experimental_dynamic_skip_first_build."));
- }
- return runRemotely(spawn, actionExecutionContext, null);
- }
-
- // True if we got the threads we need for actual dynamic execution.
- boolean gotThreads = false;
- try {
- if (threadLimiter.tryAcquire()) {
- gotThreads = true;
- } else {
- // If there are no threads available for dynamic execution because we're limited
- // to the number of CPUs, we can just execute remotely.
- ImmutableList<SpawnResult> spawnResults = runRemotely(spawn, actionExecutionContext, null);
- for (SpawnResult r : spawnResults) {
- if (r.isCacheHit()) {
- delayLocalExecution.set(true);
- break;
- }
- }
- return spawnResults;
- }
-
- // Extra logging to debug b/194373457
- logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
- "Spawn %s dynamically executed both ways", spawn.getResourceOwner().describe());
- debugLog("Dynamic execution of %s beginning%n", spawn.getResourceOwner().prettyPrint());
- // else both can exec. Fallthrough to below.
-
- // Semaphores to track termination of each branch. These are necessary to wait for the branch
- // to finish its own cleanup (e.g. terminating subprocesses) once it has been cancelled.
-
- AtomicReference<DynamicMode> strategyThatCancelled = new AtomicReference<>(null);
-
- LocalBranch localBranch =
- new LocalBranch(
- actionExecutionContext,
- spawn,
- strategyThatCancelled,
- options,
- ignoreFailureCheck,
- getExtraSpawnForLocalExecution,
- delayLocalExecution);
- RemoteBranch remoteBranch =
- new RemoteBranch(
- actionExecutionContext,
- spawn,
- strategyThatCancelled,
- options,
- ignoreFailureCheck,
- delayLocalExecution);
-
- SettableFuture<ImmutableList<SpawnResult>> localFuture =
- localBranch.prepareFuture(remoteBranch);
- SettableFuture<ImmutableList<SpawnResult>> remoteFuture =
- remoteBranch.prepareFuture(localBranch);
- localFuture.setFuture(executorService.submit(localBranch));
- remoteFuture.setFuture(executorService.submit(remoteBranch));
-
- try {
- return waitBranches(localBranch, remoteBranch, spawn, options, actionExecutionContext);
- } finally {
- checkState(localBranch.isDone());
- checkState(remoteBranch.isDone());
- logger.atInfo().atMostEvery(1, TimeUnit.SECONDS).log(
- "Dynamic execution of %s ended with local %s, remote %s%n",
- spawn.getResourceOwner().prettyPrint(),
- localBranch.isCancelled() ? "cancelled" : "done",
- remoteBranch.isCancelled() ? "cancelled" : "done");
- debugLog(
- "Dynamic execution of %s ended with local %s, remote %s%n",
- spawn.getResourceOwner().prettyPrint(),
- localBranch.isCancelled() ? "cancelled" : "done",
- remoteBranch.isCancelled() ? "cancelled" : "done");
- }
- } finally {
- if (gotThreads) {
- threadLimiter.release();
- }
- }
- }
-
- @Override
public void usedContext(ActionContext.ActionContextRegistry actionContextRegistry) {
actionContextRegistry
.getContext(DynamicStrategyRegistry.class)
.notifyUsedDynamic(actionContextRegistry);
}
- private static FileOutErr getSuffixedFileOutErr(FileOutErr fileOutErr, String suffix) {
- Path outDir = checkNotNull(fileOutErr.getOutputPath().getParentDirectory());
- String outBaseName = fileOutErr.getOutputPath().getBaseName();
- Path errDir = checkNotNull(fileOutErr.getErrorPath().getParentDirectory());
- String errBaseName = fileOutErr.getErrorPath().getBaseName();
- return new FileOutErr(
- outDir.getChild(outBaseName + suffix), errDir.getChild(errBaseName + suffix));
- }
-
- /**
- * Try to run the given spawn locally.
- *
- * <p>Precondition: At least one {@code dynamic_local_strategy} returns {@code true} from its
- * {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
- */
- private static ImmutableList<SpawnResult> runLocally(
- Spawn spawn,
- ActionExecutionContext actionExecutionContext,
- @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns,
- Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution)
- throws ExecException, InterruptedException {
- ImmutableList<SpawnResult> spawnResult =
- runSpawnLocally(spawn, actionExecutionContext, stopConcurrentSpawns);
- if (spawnResult.stream().anyMatch(result -> result.status() != Status.SUCCESS)) {
- return spawnResult;
- }
-
- Optional<Spawn> extraSpawn = getExtraSpawnForLocalExecution.apply(spawn);
- if (!extraSpawn.isPresent()) {
- return spawnResult;
- }
-
- // The remote branch was already cancelled -- we are holding the output lock during the
- // execution of the extra spawn.
- ImmutableList<SpawnResult> extraSpawnResult =
- runSpawnLocally(extraSpawn.get(), actionExecutionContext, null);
- return ImmutableList.<SpawnResult>builderWithExpectedSize(
- spawnResult.size() + extraSpawnResult.size())
- .addAll(spawnResult)
- .addAll(extraSpawnResult)
- .build();
- }
-
- private static ImmutableList<SpawnResult> runSpawnLocally(
- Spawn spawn,
- ActionExecutionContext actionExecutionContext,
- @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
- throws ExecException, InterruptedException {
- DynamicStrategyRegistry dynamicStrategyRegistry =
- actionExecutionContext.getContext(DynamicStrategyRegistry.class);
-
- for (SandboxedSpawnStrategy strategy :
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, LOCAL)) {
- if (strategy.canExec(spawn, actionExecutionContext)
- || strategy.canExecWithLegacyFallback(spawn, actionExecutionContext)) {
- ImmutableList<SpawnResult> results =
- strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
- if (results == null) {
- logger.atWarning().log(
- "Local strategy %s for %s target %s returned null, which it shouldn't do.",
- strategy, spawn.getMnemonic(), spawn.getResourceOwner().prettyPrint());
- }
- return results;
- }
- }
- throw new AssertionError("canExec passed but no usable local strategy for action " + spawn);
- }
-
- /**
- * Try to run the given spawn remotely.
- *
- * <p>Precondition: At least one {@code dynamic_remote_strategy} returns {@code true} from its
- * {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
- */
- private static ImmutableList<SpawnResult> runRemotely(
- Spawn spawn,
- ActionExecutionContext actionExecutionContext,
- @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
- throws ExecException, InterruptedException {
- DynamicStrategyRegistry dynamicStrategyRegistry =
- actionExecutionContext.getContext(DynamicStrategyRegistry.class);
-
- for (SandboxedSpawnStrategy strategy :
- dynamicStrategyRegistry.getDynamicSpawnActionContexts(
- spawn, DynamicStrategyRegistry.DynamicMode.REMOTE)) {
- if (strategy.canExec(spawn, actionExecutionContext)) {
- ImmutableList<SpawnResult> results =
- strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
- if (results == null) {
- actionExecutionContext
- .getEventHandler()
- .handle(
- Event.warn(
- String.format(
- "Remote strategy %s for %s target %s returned null, which it shouldn't"
- + " do.",
- strategy, spawn.getMnemonic(), spawn.getResourceOwner().prettyPrint())));
- }
- return results;
- }
- }
- throw new AssertionError("canExec passed but no usable remote strategy for action " + spawn);
- }
-
- /**
- * Wraps the execution of a function that is supposed to execute a spawn via a strategy and only
- * updates the stdout/stderr files if this spawn succeeds.
- */
- private abstract static class Branch implements Callable<ImmutableList<SpawnResult>> {
- /**
- * True if this branch is still starting up, i.e. didn't get to the inner part of {@link
- * #callImpl(ActionExecutionContext)} yet.
- */
- protected final AtomicBoolean starting = new AtomicBoolean(true);
- /** The {@link Spawn} this branch is running. */
- protected final Spawn spawn;
- /**
- * The {@link SettableFuture} with the results from running the spawn. Must not be null if
- * execution succeeded.
- */
- protected final SettableFuture<ImmutableList<SpawnResult>> future = SettableFuture.create();
-
- protected final AtomicReference<DynamicMode> strategyThatCancelled;
- /** Semaphore that indicates whether this branch is done, i.e. either completed or cancelled. */
- protected final Semaphore done = new Semaphore(0);
-
- protected final DynamicExecutionOptions options;
- private final DynamicStrategyRegistry.DynamicMode mode;
- protected final ActionExecutionContext context;
-
- /**
- * Creates a new branch of dynamic execution.
- *
- * @param mode the dynamic mode that this branch represents (e.g. {@link
- * DynamicStrategyRegistry.DynamicMode#REMOTE}). Used to qualify temporary files.
- * @param context the action execution context given to the dynamic strategy, used to obtain the
- * final location of the stdout/stderr
- */
- Branch(
- DynamicStrategyRegistry.DynamicMode mode,
- ActionExecutionContext context,
- Spawn spawn,
- AtomicReference<DynamicMode> strategyThatCancelled,
- DynamicExecutionOptions options) {
- this.mode = mode;
- this.context = context;
- this.spawn = spawn;
- this.strategyThatCancelled = strategyThatCancelled;
- this.options = options;
- }
-
- boolean isDone() {
- return future.isDone();
- }
-
- /** Returns the {@code Semaphore} indicating whether this branch is done. */
- Semaphore getDoneSemaphore() {
- return done;
- }
-
- /** Returns whether this branch has already been cancelled. */
- boolean isCancelled() {
- return future.isCancelled();
- }
-
- /** Cancels this branch. Equivalent to {@code Future.cancel(true)}. */
- boolean cancel() {
- return future.cancel(true);
- }
-
- /** Gets the results from this branch, when available. Behaves like {@link Future#get()} */
- ImmutableList<SpawnResult> getResults() throws ExecutionException, InterruptedException {
- return future.get();
- }
-
- public Spawn getSpawn() {
- return spawn;
- }
-
- /**
- * Moves a set of stdout/stderr files over another one. Errors during the move are logged and
- * swallowed.
- *
- * @param from the source location
- * @param to the target location
- */
- private static void moveFileOutErr(FileOutErr from, FileOutErr to) {
- try {
- if (from.getOutputPath().exists()) {
- Files.move(from.getOutputPath().getPathFile(), to.getOutputPath().getPathFile());
- }
- if (from.getErrorPath().exists()) {
- Files.move(from.getErrorPath().getPathFile(), to.getErrorPath().getPathFile());
- }
- } catch (IOException e) {
- logger.atWarning().withCause(e).log("Could not move action logs from execution");
- }
- }
-
- /**
- * Hook to execute a spawn using an arbitrary strategy.
- *
- * @param context the action execution context where the spawn can write its stdout/stderr. The
- * location of these files is specific to this branch.
- * @return the spawn results if execution was successful
- * @throws InterruptedException if the branch was cancelled or an interrupt was caught
- * @throws ExecException if the spawn execution fails
- */
- abstract ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
- throws InterruptedException, ExecException;
-
- /**
- * Executes the {@link #callImpl} hook and handles stdout/stderr.
- *
- * @return the spawn results if execution was successful
- * @throws InterruptedException if the branch was cancelled or an interrupt was caught
- * @throws ExecException if the spawn execution fails
- */
- @Override
- public final ImmutableList<SpawnResult> call() throws InterruptedException, ExecException {
- FileOutErr fileOutErr = getSuffixedFileOutErr(context.getFileOutErr(), "." + mode.name());
-
- ImmutableList<SpawnResult> results = null;
- ExecException exception = null;
- try {
- results = callImpl(context.withFileOutErr(fileOutErr));
- } catch (ExecException e) {
- exception = e;
- } finally {
- try {
- fileOutErr.close();
- } catch (IOException ignored) {
- // Nothing we can do here.
- }
- }
-
- moveFileOutErr(fileOutErr, context.getFileOutErr());
-
- if (exception != null) {
- throw exception;
- } else {
- checkNotNull(results);
- return results;
- }
- }
- }
-
- /**
- * The local version of a Branch. On top of normal Branch things, this handles delaying after
- * remote cache hits and passing the extra-spawn function.
- */
- @VisibleForTesting
- static class LocalBranch extends Branch {
- private RemoteBranch remoteBranch;
- private final IgnoreFailureCheck ignoreFailureCheck;
- private final Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution;
- private final AtomicBoolean delayLocalExecution;
-
- public LocalBranch(
- ActionExecutionContext actionExecutionContext,
- Spawn spawn,
- AtomicReference<DynamicMode> strategyThatCancelled,
- DynamicExecutionOptions options,
- IgnoreFailureCheck ignoreFailureCheck,
- Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution,
- AtomicBoolean delayLocalExecution) {
- super(DynamicMode.LOCAL, actionExecutionContext, spawn, strategyThatCancelled, options);
- this.ignoreFailureCheck = ignoreFailureCheck;
- this.getExtraSpawnForLocalExecution = getExtraSpawnForLocalExecution;
- this.delayLocalExecution = delayLocalExecution;
- }
-
- /** Sets up the {@link Future} used in the local branch to know what remote branch to cancel. */
- protected SettableFuture<ImmutableList<SpawnResult>> prepareFuture(RemoteBranch remoteBranch) {
- this.remoteBranch = remoteBranch;
- future.addListener(
- () -> {
- if (starting.compareAndSet(true, false)) {
- // If the local branch got cancelled before even starting, we release its semaphore
- // for it.
- done.release();
- }
- if (!future.isCancelled()) {
- remoteBranch.cancel();
- }
- },
- MoreExecutors.directExecutor());
- return future;
- }
-
- @Override
- ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
- throws InterruptedException, ExecException {
- try {
- if (!starting.compareAndSet(true, false)) {
- // If we ever get here, it's because we were cancelled early and the listener
- // ran first. Just make sure that's the case.
- checkState(Thread.interrupted());
- throw new InterruptedException();
- }
- if (delayLocalExecution.get()) {
- Thread.sleep(options.localExecutionDelay);
- }
- return runLocally(
- spawn,
- context,
- (exitCode, errorMessage, outErr) -> {
- maybeIgnoreFailure(ignoreFailureCheck, exitCode, errorMessage, outErr);
- stopBranch(remoteBranch, this, LOCAL, strategyThatCancelled, options, this.context);
- },
- getExtraSpawnForLocalExecution);
- } catch (DynamicInterruptedException e) {
- if (options.debugSpawnScheduler) {
- logger.atInfo().log(
- "Local branch of %s self-cancelling with %s: '%s'",
- spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
- }
- // This exception can be thrown due to races in stopBranch(), in which case
- // the branch that lost the race may not have been cancelled yet. Cancel it here
- // to prevent the listener from cross-cancelling.
- cancel();
- throw e;
- } catch (
- @SuppressWarnings("InterruptedExceptionSwallowed")
- Throwable e) {
- if (options.debugSpawnScheduler) {
- logger.atInfo().log(
- "Local branch of %s failed with %s: '%s'",
- spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
- }
- throw e;
- } finally {
- done.release();
- }
- }
-
- /**
- * Called when execution failed, to check if we should allow the other branch to continue
- * instead of failing.
- *
- * @throws DynamicInterruptedException if this failure can be ignored in favor of the result of
- * the other branch.
- */
- protected void maybeIgnoreFailure(
- IgnoreFailureCheck ignoreFailureCheck, int exitCode, String errorMessage, FileOutErr outErr)
- throws DynamicInterruptedException {
- if (exitCode == 0 || ignoreFailureCheck == null) {
- return;
- }
- synchronized (spawn) {
- if (ignoreFailureCheck.canIgnoreFailure(spawn, exitCode, errorMessage, outErr, true)) {
- throw new DynamicInterruptedException(
- String.format(
- "Local branch of %s cancelling self in favor of remote.",
- spawn.getResourceOwner().prettyPrint()));
- }
- }
- }
- }
-
- /**
- * The remove version of Branch. On top of the usual stop handles setting {@link
- * #delayLocalExecution} when getting a cache hit.
- */
- @VisibleForTesting
- static class RemoteBranch extends Branch {
- private LocalBranch localBranch;
- private final IgnoreFailureCheck ignoreFailureCheck;
- private final AtomicBoolean delayLocalExecution;
-
- public RemoteBranch(
- ActionExecutionContext actionExecutionContext,
- Spawn spawn,
- AtomicReference<DynamicMode> strategyThatCancelled,
- DynamicExecutionOptions options,
- IgnoreFailureCheck ignoreFailureCheck,
- AtomicBoolean delayLocalExecution) {
- super(DynamicMode.REMOTE, actionExecutionContext, spawn, strategyThatCancelled, options);
- this.ignoreFailureCheck = ignoreFailureCheck;
- this.delayLocalExecution = delayLocalExecution;
- }
-
- /** Sets up the future for this branch, once the other branch is available. */
- public SettableFuture<ImmutableList<SpawnResult>> prepareFuture(LocalBranch localBranch) {
- this.localBranch = localBranch;
- future.addListener(
- () -> {
- if (starting.compareAndSet(true, false)) {
- // If the remote branch got cancelled before even starting, we release its semaphore
- // for it.
- done.release();
- }
- if (!future.isCancelled()) {
- localBranch.cancel();
- }
- },
- MoreExecutors.directExecutor());
- return future;
- }
-
- @Override
- public ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
- throws InterruptedException, ExecException {
- if (localBranch == null) {
- throw new IllegalStateException("Initialize not called");
- }
- try {
- if (!starting.compareAndSet(true, false)) {
- // If we ever get here, it's because we were cancelled early and the listener
- // ran first. Just make sure that's the case.
- checkState(Thread.interrupted());
- throw new InterruptedException();
- }
- ImmutableList<SpawnResult> spawnResults =
- runRemotely(
- spawn,
- context,
- (exitCode, errorMessage, outErr) -> {
- maybeIgnoreFailure(exitCode, errorMessage, outErr);
- stopBranch(
- localBranch,
- this,
- DynamicMode.REMOTE,
- strategyThatCancelled,
- options,
- this.context);
- });
- for (SpawnResult r : spawnResults) {
- if (r.isCacheHit()) {
- delayLocalExecution.set(true);
- break;
- }
- }
- return spawnResults;
- } catch (DynamicInterruptedException e) {
- if (options.debugSpawnScheduler) {
- logger.atInfo().log(
- "Remote branch of %s self-cancelling with %s: '%s'",
- spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
- }
- // This exception can be thrown due to races in stopBranch(), in which case
- // the branch that lost the race may not have been cancelled yet. Cancel it here
- // to prevent the listener from cross-cancelling.
- future.cancel(true);
- throw e;
- } catch (
- @SuppressWarnings("InterruptedExceptionSwallowed")
- Throwable e) {
- if (options.debugSpawnScheduler) {
- logger.atInfo().log(
- "Remote branch of %s failed with %s: '%s'",
- spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
- }
- throw e;
- } finally {
- done.release();
- }
- }
-
- /**
- * Called when execution failed, to check if we should allow the other branch to continue
- * instead of failing.
- *
- * @throws DynamicInterruptedException if this failure can be ignored in favor of the result of
- * the other branch.
- */
- protected synchronized void maybeIgnoreFailure(
- int exitCode, String errorMessage, FileOutErr outErr) throws DynamicInterruptedException {
- if (exitCode == 0 || ignoreFailureCheck == null) {
- return;
- }
- synchronized (spawn) {
- if (ignoreFailureCheck.canIgnoreFailure(spawn, exitCode, errorMessage, outErr, false)) {
- throw new DynamicInterruptedException(
- String.format(
- "Remote branch of %s cancelling self in favor of local.",
- spawn.getResourceOwner().prettyPrint()));
- }
- }
- }
- }
-
@Override
public String toString() {
return "dynamic";
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/LocalBranch.java b/src/main/java/com/google/devtools/build/lib/dynamic/LocalBranch.java
new file mode 100644
index 0000000..59f4a7f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/LocalBranch.java
@@ -0,0 +1,216 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.dynamic;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode.LOCAL;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.GoogleLogger;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.SpawnResult.Status;
+import com.google.devtools.build.lib.actions.SpawnStrategy;
+import com.google.devtools.build.lib.dynamic.DynamicExecutionModule.IgnoreFailureCheck;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import java.util.Optional;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+/**
+ * The local version of a Branch. On top of normal Branch things, this handles delaying after remote
+ * cache hits and passing the extra-spawn function.
+ */
+@VisibleForTesting
+class LocalBranch extends Branch {
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+
+ private RemoteBranch remoteBranch;
+ private final IgnoreFailureCheck ignoreFailureCheck;
+ private final Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution;
+ private final AtomicBoolean delayLocalExecution;
+
+ public LocalBranch(
+ ActionExecutionContext actionExecutionContext,
+ Spawn spawn,
+ AtomicReference<DynamicMode> strategyThatCancelled,
+ DynamicExecutionOptions options,
+ IgnoreFailureCheck ignoreFailureCheck,
+ Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution,
+ AtomicBoolean delayLocalExecution) {
+ super(DynamicMode.LOCAL, actionExecutionContext, spawn, strategyThatCancelled, options);
+ this.ignoreFailureCheck = ignoreFailureCheck;
+ this.getExtraSpawnForLocalExecution = getExtraSpawnForLocalExecution;
+ this.delayLocalExecution = delayLocalExecution;
+ }
+
+ /**
+ * Try to run the given spawn locally.
+ *
+ * <p>Precondition: At least one {@code dynamic_local_strategy} returns {@code true} from its
+ * {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
+ */
+ static ImmutableList<SpawnResult> runLocally(
+ Spawn spawn,
+ ActionExecutionContext actionExecutionContext,
+ @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns,
+ Function<Spawn, Optional<Spawn>> getExtraSpawnForLocalExecution)
+ throws ExecException, InterruptedException {
+ ImmutableList<SpawnResult> spawnResult =
+ runSpawnLocally(spawn, actionExecutionContext, stopConcurrentSpawns);
+ if (spawnResult.stream().anyMatch(result -> result.status() != Status.SUCCESS)) {
+ return spawnResult;
+ }
+
+ Optional<Spawn> extraSpawn = getExtraSpawnForLocalExecution.apply(spawn);
+ if (!extraSpawn.isPresent()) {
+ return spawnResult;
+ }
+
+ // The remote branch was already cancelled -- we are holding the output lock during the
+ // execution of the extra spawn.
+ ImmutableList<SpawnResult> extraSpawnResult =
+ runSpawnLocally(extraSpawn.get(), actionExecutionContext, null);
+ return ImmutableList.<SpawnResult>builderWithExpectedSize(
+ spawnResult.size() + extraSpawnResult.size())
+ .addAll(spawnResult)
+ .addAll(extraSpawnResult)
+ .build();
+ }
+
+ private static ImmutableList<SpawnResult> runSpawnLocally(
+ Spawn spawn,
+ ActionExecutionContext actionExecutionContext,
+ @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
+ throws ExecException, InterruptedException {
+ DynamicStrategyRegistry dynamicStrategyRegistry =
+ actionExecutionContext.getContext(DynamicStrategyRegistry.class);
+
+ for (SandboxedSpawnStrategy strategy :
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, LOCAL)) {
+ if (strategy.canExec(spawn, actionExecutionContext)
+ || strategy.canExecWithLegacyFallback(spawn, actionExecutionContext)) {
+ ImmutableList<SpawnResult> results =
+ strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
+ if (results == null) {
+ logger.atWarning().log(
+ "Local strategy %s for %s target %s returned null, which it shouldn't do.",
+ strategy, spawn.getMnemonic(), spawn.getResourceOwner().prettyPrint());
+ }
+ return results;
+ }
+ }
+ throw new AssertionError("canExec passed but no usable local strategy for action " + spawn);
+ }
+
+ /** Sets up the {@link Future} used in the local branch to know what remote branch to cancel. */
+ protected SettableFuture<ImmutableList<SpawnResult>> prepareFuture(RemoteBranch remoteBranch) {
+ // TODO(b/203094728): Maybe generify this method and move it up.
+ this.remoteBranch = remoteBranch;
+ future.addListener(
+ () -> {
+ if (starting.compareAndSet(true, false)) {
+ // If the local branch got cancelled before even starting, we release its semaphore
+ // for it.
+ done.release();
+ }
+ if (!future.isCancelled()) {
+ remoteBranch.cancel();
+ }
+ },
+ MoreExecutors.directExecutor());
+ return future;
+ }
+
+ @Override
+ ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
+ throws InterruptedException, ExecException {
+ try {
+ if (!starting.compareAndSet(true, false)) {
+ // If we ever get here, it's because we were cancelled early and the listener
+ // ran first. Just make sure that's the case.
+ checkState(Thread.interrupted());
+ throw new InterruptedException();
+ }
+ if (delayLocalExecution.get()) {
+ Thread.sleep(options.localExecutionDelay);
+ }
+ return runLocally(
+ spawn,
+ context,
+ (exitCode, errorMessage, outErr) -> {
+ maybeIgnoreFailure(exitCode, errorMessage, outErr);
+ DynamicSpawnStrategy.stopBranch(
+ remoteBranch, this, LOCAL, strategyThatCancelled, options, this.context);
+ },
+ getExtraSpawnForLocalExecution);
+ } catch (DynamicInterruptedException e) {
+ if (options.debugSpawnScheduler) {
+ logger.atInfo().log(
+ "Local branch of %s self-cancelling with %s: '%s'",
+ spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
+ }
+ // This exception can be thrown due to races in stopBranch(), in which case
+ // the branch that lost the race may not have been cancelled yet. Cancel it here
+ // to prevent the listener from cross-cancelling.
+ cancel();
+ throw e;
+ } catch (
+ @SuppressWarnings("InterruptedExceptionSwallowed")
+ Throwable e) {
+ if (options.debugSpawnScheduler) {
+ logger.atInfo().log(
+ "Local branch of %s failed with %s: '%s'",
+ spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
+ }
+ throw e;
+ } finally {
+ done.release();
+ }
+ }
+
+ /**
+ * Called when execution failed, to check if we should allow the other branch to continue instead
+ * of failing.
+ *
+ * @throws DynamicInterruptedException if this failure can be ignored in favor of the result of
+ * the other branch.
+ */
+ protected void maybeIgnoreFailure(int exitCode, String errorMessage, FileOutErr outErr)
+ throws DynamicInterruptedException {
+ if (exitCode == 0 || ignoreFailureCheck == null) {
+ return;
+ }
+ synchronized (spawn) {
+ if (ignoreFailureCheck.canIgnoreFailure(spawn, exitCode, errorMessage, outErr, true)) {
+ throw new DynamicInterruptedException(
+ String.format(
+ "Local branch of %s cancelling self in favor of remote.",
+ spawn.getResourceOwner().prettyPrint()));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/RemoteBranch.java b/src/main/java/com/google/devtools/build/lib/dynamic/RemoteBranch.java
new file mode 100644
index 0000000..0e9b40c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/RemoteBranch.java
@@ -0,0 +1,196 @@
+// Copyright 2021 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.dynamic;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.GoogleLogger;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry;
+import com.google.devtools.build.lib.actions.DynamicStrategyRegistry.DynamicMode;
+import com.google.devtools.build.lib.actions.ExecException;
+import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
+import com.google.devtools.build.lib.actions.Spawn;
+import com.google.devtools.build.lib.actions.SpawnResult;
+import com.google.devtools.build.lib.actions.SpawnStrategy;
+import com.google.devtools.build.lib.dynamic.DynamicExecutionModule.IgnoreFailureCheck;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
+
+/**
+ * The remove version of Branch. On top of the usual stop handles setting {@link
+ * #delayLocalExecution} when getting a cache hit.
+ */
+@VisibleForTesting
+class RemoteBranch extends Branch {
+ private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
+
+ private LocalBranch localBranch;
+ private final IgnoreFailureCheck ignoreFailureCheck;
+ private final AtomicBoolean delayLocalExecution;
+
+ public RemoteBranch(
+ ActionExecutionContext actionExecutionContext,
+ Spawn spawn,
+ AtomicReference<DynamicMode> strategyThatCancelled,
+ DynamicExecutionOptions options,
+ IgnoreFailureCheck ignoreFailureCheck,
+ AtomicBoolean delayLocalExecution) {
+ super(DynamicMode.REMOTE, actionExecutionContext, spawn, strategyThatCancelled, options);
+ this.ignoreFailureCheck = ignoreFailureCheck;
+ this.delayLocalExecution = delayLocalExecution;
+ }
+
+ /**
+ * Try to run the given spawn remotely.
+ *
+ * <p>Precondition: At least one {@code dynamic_remote_strategy} returns {@code true} from its
+ * {@link SpawnStrategy#canExec canExec} method for the given {@code spawn}.
+ */
+ static ImmutableList<SpawnResult> runRemotely(
+ Spawn spawn,
+ ActionExecutionContext actionExecutionContext,
+ @Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
+ throws ExecException, InterruptedException {
+ DynamicStrategyRegistry dynamicStrategyRegistry =
+ actionExecutionContext.getContext(DynamicStrategyRegistry.class);
+
+ for (SandboxedSpawnStrategy strategy :
+ dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, DynamicMode.REMOTE)) {
+ if (strategy.canExec(spawn, actionExecutionContext)) {
+ ImmutableList<SpawnResult> results =
+ strategy.exec(spawn, actionExecutionContext, stopConcurrentSpawns);
+ if (results == null) {
+ actionExecutionContext
+ .getEventHandler()
+ .handle(
+ Event.warn(
+ String.format(
+ "Remote strategy %s for %s target %s returned null, which it shouldn't"
+ + " do.",
+ strategy, spawn.getMnemonic(), spawn.getResourceOwner().prettyPrint())));
+ }
+ return results;
+ }
+ }
+ throw new AssertionError("canExec passed but no usable remote strategy for action " + spawn);
+ }
+
+ /** Sets up the future for this branch, once the other branch is available. */
+ public SettableFuture<ImmutableList<SpawnResult>> prepareFuture(LocalBranch localBranch) {
+ this.localBranch = localBranch;
+ future.addListener(
+ () -> {
+ if (starting.compareAndSet(true, false)) {
+ // If the remote branch got cancelled before even starting, we release its semaphore
+ // for it.
+ done.release();
+ }
+ if (!future.isCancelled()) {
+ localBranch.cancel();
+ }
+ },
+ MoreExecutors.directExecutor());
+ return future;
+ }
+
+ @Override
+ public ImmutableList<SpawnResult> callImpl(ActionExecutionContext context)
+ throws InterruptedException, ExecException {
+ if (localBranch == null) {
+ throw new IllegalStateException("Initialize not called");
+ }
+ try {
+ if (!starting.compareAndSet(true, false)) {
+ // If we ever get here, it's because we were cancelled early and the listener
+ // ran first. Just make sure that's the case.
+ checkState(Thread.interrupted());
+ throw new InterruptedException();
+ }
+ ImmutableList<SpawnResult> spawnResults =
+ runRemotely(
+ spawn,
+ context,
+ (exitCode, errorMessage, outErr) -> {
+ maybeIgnoreFailure(exitCode, errorMessage, outErr);
+ DynamicSpawnStrategy.stopBranch(
+ localBranch,
+ this,
+ DynamicMode.REMOTE,
+ strategyThatCancelled,
+ options,
+ this.context);
+ });
+ for (SpawnResult r : spawnResults) {
+ if (r.isCacheHit()) {
+ delayLocalExecution.set(true);
+ break;
+ }
+ }
+ return spawnResults;
+ } catch (DynamicInterruptedException e) {
+ if (options.debugSpawnScheduler) {
+ logger.atInfo().log(
+ "Remote branch of %s self-cancelling with %s: '%s'",
+ spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
+ }
+ // This exception can be thrown due to races in stopBranch(), in which case
+ // the branch that lost the race may not have been cancelled yet. Cancel it here
+ // to prevent the listener from cross-cancelling.
+ future.cancel(true);
+ throw e;
+ } catch (
+ @SuppressWarnings("InterruptedExceptionSwallowed")
+ Throwable e) {
+ if (options.debugSpawnScheduler) {
+ logger.atInfo().log(
+ "Remote branch of %s failed with %s: '%s'",
+ spawn.getResourceOwner().prettyPrint(), e.getClass().getSimpleName(), e.getMessage());
+ }
+ throw e;
+ } finally {
+ done.release();
+ }
+ }
+
+ /**
+ * Called when execution failed, to check if we should allow the other branch to continue instead
+ * of failing.
+ *
+ * @throws DynamicInterruptedException if this failure can be ignored in favor of the result of
+ * the other branch.
+ */
+ protected synchronized void maybeIgnoreFailure(
+ int exitCode, String errorMessage, FileOutErr outErr) throws DynamicInterruptedException {
+ if (exitCode == 0 || ignoreFailureCheck == null) {
+ return;
+ }
+ synchronized (spawn) {
+ if (ignoreFailureCheck.canIgnoreFailure(spawn, exitCode, errorMessage, outErr, false)) {
+ throw new DynamicInterruptedException(
+ String.format(
+ "Remote branch of %s cancelling self in favor of local.",
+ spawn.getResourceOwner().prettyPrint()));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyUnitTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyUnitTest.java
index 9fd4315..d6c4a36 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyUnitTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyUnitTest.java
@@ -42,8 +42,6 @@
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.UserExecException;
-import com.google.devtools.build.lib.dynamic.DynamicSpawnStrategy.LocalBranch;
-import com.google.devtools.build.lib.dynamic.DynamicSpawnStrategy.RemoteBranch;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.exec.ExecutionPolicy;