| // 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.REMOTE; | |
| 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.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.SandboxedSpawnStrategy.StopConcurrentSpawns; | |
| 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(actionExecutionContext, spawn, strategyThatCancelled, options); | |
| this.ignoreFailureCheck = ignoreFailureCheck; | |
| this.delayLocalExecution = delayLocalExecution; | |
| } | |
| @Override | |
| public DynamicMode getMode() { | |
| return REMOTE; | |
| } | |
| /** | |
| * Try to run the given spawn remotely. If successful, updates {@link #delayLocalExecution} if | |
| * there was a cache hit among the results. | |
| * | |
| * <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 StopConcurrentSpawns stopConcurrentSpawns, | |
| AtomicBoolean delayLocalExecution) | |
| throws ExecException, InterruptedException { | |
| DynamicStrategyRegistry dynamicStrategyRegistry = | |
| actionExecutionContext.getContext(DynamicStrategyRegistry.class); | |
| for (SandboxedSpawnStrategy strategy : | |
| dynamicStrategyRegistry.getDynamicSpawnActionContexts(spawn, 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()))); | |
| } | |
| for (SpawnResult r : results) { | |
| if (r.isCacheHit()) { | |
| delayLocalExecution.set(true); | |
| break; | |
| } | |
| } | |
| 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 void 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()); | |
| } | |
| @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(); | |
| } | |
| return runRemotely( | |
| spawn, | |
| context, | |
| (exitCode, errorMessage, outErr) -> { | |
| maybeIgnoreFailure(exitCode, errorMessage, outErr); | |
| DynamicSpawnStrategy.stopBranch( | |
| localBranch, this, strategyThatCancelled, options, this.context); | |
| }, | |
| delayLocalExecution); | |
| } 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, context, exitCode, errorMessage, outErr, false)) { | |
| throw new DynamicInterruptedException( | |
| String.format( | |
| "Remote branch of %s cancelling self in favor of local.", | |
| spawn.getResourceOwner().prettyPrint())); | |
| } | |
| } | |
| } | |
| } |