blob: c689df66c18a213cb0d507b7f8f99284db1bf99e [file] [log] [blame]
// Copyright 2017 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.exec;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.devtools.build.lib.actions.ActionContext;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher.Priority;
import com.google.devtools.build.lib.actions.ArtifactExpander;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ForbiddenActionInputException;
import com.google.devtools.build.lib.actions.InputMetadataProvider;
import com.google.devtools.build.lib.actions.LostInputsActionExecutionException;
import com.google.devtools.build.lib.actions.LostInputsExecException;
import com.google.devtools.build.lib.actions.SandboxedSpawnStrategy;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.SpawnExecutedEvent;
import com.google.devtools.build.lib.actions.SpawnResult;
import com.google.devtools.build.lib.actions.SpawnResult.Status;
import com.google.devtools.build.lib.actions.Spawns;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.exec.Protos.Digest;
import com.google.devtools.build.lib.exec.SpawnCache.CacheHandle;
import com.google.devtools.build.lib.exec.SpawnRunner.ProgressStatus;
import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.remote.common.BulkTransferException;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
import com.google.devtools.build.lib.util.CommandFailureUtils;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.time.Duration;
import java.time.Instant;
import java.util.SortedMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
/** Abstract common ancestor for spawn strategies implementing the common parts. */
public abstract class AbstractSpawnStrategy implements SandboxedSpawnStrategy {
/**
* Last unique identifier assigned to a spawn by this strategy.
*
* <p>These identifiers must be unique per strategy within the context of a Bazel server instance
* to avoid cross-contamination across actions in case we perform asynchronous deletions.
*/
private static final AtomicInteger execCount = new AtomicInteger();
private final SpawnInputExpander spawnInputExpander;
private final SpawnRunner spawnRunner;
private final ExecutionOptions executionOptions;
protected AbstractSpawnStrategy(
Path execRoot, SpawnRunner spawnRunner, ExecutionOptions executionOptions) {
this.spawnInputExpander = new SpawnInputExpander(execRoot);
this.spawnRunner = spawnRunner;
this.executionOptions = executionOptions;
}
/**
* Gets the {@link SpawnRunner} that this {@link AbstractSpawnStrategy} uses to actually run
* spawns.
*
* <p>This is considered a stop-gap until we refactor the entire SpawnStrategy / SpawnRunner
* mechanism to no longer need Spawn strategies.
*/
public SpawnRunner getSpawnRunner() {
return spawnRunner;
}
@Override
public boolean canExec(Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
return spawnRunner.canExec(spawn);
}
@Override
public boolean canExecWithLegacyFallback(
Spawn spawn, ActionContext.ActionContextRegistry actionContextRegistry) {
return spawnRunner.canExecWithLegacyFallback(spawn);
}
@Override
public ImmutableList<SpawnResult> exec(Spawn spawn, ActionExecutionContext actionExecutionContext)
throws ExecException, InterruptedException {
return exec(spawn, actionExecutionContext, null);
}
@Override
public ImmutableList<SpawnResult> exec(
Spawn spawn,
ActionExecutionContext actionExecutionContext,
@Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns)
throws ExecException, InterruptedException {
actionExecutionContext.maybeReportSubcommand(spawn);
final Duration timeout = Spawns.getTimeout(spawn);
SpawnExecutionContext context =
new SpawnExecutionContextImpl(spawn, actionExecutionContext, stopConcurrentSpawns, timeout);
// Avoid caching for runners which handle caching internally e.g. RemoteSpawnRunner.
SpawnCache cache =
spawnRunner.handlesCaching()
? SpawnCache.NO_CACHE
: actionExecutionContext.getContext(SpawnCache.class);
// In production, the getContext method guarantees that we never get null back. However, our
// integration tests don't set it up correctly, so cache may be null in testing.
if (cache == null) {
cache = SpawnCache.NO_CACHE;
}
// Avoid using the remote cache of a dynamic execution setup for the local runner.
if (context.speculating() && !cache.usefulInDynamicExecution()) {
cache = SpawnCache.NO_CACHE;
}
SpawnResult spawnResult;
ExecException ex = null;
try (CacheHandle cacheHandle = cache.lookup(spawn, context)) {
if (cacheHandle.hasResult()) {
spawnResult = Preconditions.checkNotNull(cacheHandle.getResult());
} else {
Instant startTime =
Instant.ofEpochMilli(actionExecutionContext.getClock().currentTimeMillis());
// Actual execution.
spawnResult = spawnRunner.exec(spawn, context);
actionExecutionContext
.getEventHandler()
.post(
new SpawnExecutedEvent(
spawn,
actionExecutionContext.getInputMetadataProvider(),
spawnResult,
startTime));
if (cacheHandle.willStore()) {
cacheHandle.store(spawnResult);
}
}
} catch (InterruptedIOException e) {
throw new InterruptedException(e.getMessage());
} catch (IOException e) {
throw new EnvironmentalExecException(
e,
FailureDetail.newBuilder()
.setMessage("Exec failed due to IOException")
.setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.EXEC_IO_EXCEPTION))
.build());
} catch (SpawnExecException e) {
ex = e;
spawnResult = e.getSpawnResult();
// Log the Spawn and re-throw.
} catch (ForbiddenActionInputException e) {
throw new UserExecException(
e,
FailureDetail.newBuilder()
.setMessage("Exec failed due to forbidden input")
.setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.FORBIDDEN_INPUT))
.build());
}
SpawnLogContext spawnLogContext = actionExecutionContext.getContext(SpawnLogContext.class);
if (spawnLogContext != null) {
try {
spawnLogContext.logSpawn(
spawn,
actionExecutionContext.getInputMetadataProvider(),
context.getInputMapping(PathFragment.EMPTY_FRAGMENT, /* willAccessRepeatedly= */ false),
actionExecutionContext.getActionFileSystem() != null
? actionExecutionContext.getActionFileSystem()
: actionExecutionContext.getExecRoot().getFileSystem(),
context.getTimeout(),
spawnResult);
} catch (IOException e) {
throw new EnvironmentalExecException(
e,
FailureDetail.newBuilder()
.setMessage("IOException while logging spawn")
.setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.SPAWN_LOG_IO_EXCEPTION))
.build());
} catch (ForbiddenActionInputException e) {
// Should have already been thrown during execution.
throw new IllegalStateException("ForbiddenActionInputException while logging spawn", e);
}
}
if (ex != null) {
throw ex;
}
if (spawnResult.status() != Status.SUCCESS) {
String cwd = actionExecutionContext.getExecRoot().getPathString();
String resultMessage = spawnResult.getFailureMessage();
String message =
!Strings.isNullOrEmpty(resultMessage)
? resultMessage
: CommandFailureUtils.describeCommandFailure(
executionOptions.verboseFailures, cwd, spawn);
throw new SpawnExecException(message, spawnResult, /* forciblyRunRemotely= */ false);
}
return ImmutableList.of(spawnResult);
}
private final class SpawnExecutionContextImpl implements SpawnExecutionContext {
private final Spawn spawn;
private final ActionExecutionContext actionExecutionContext;
@Nullable private final SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns;
private final Duration timeout;
private final int id = execCount.incrementAndGet();
// Memoize the input mapping so that prefetchInputs can reuse it instead of recomputing it.
// TODO(ulfjack): Guard against client modification of this map.
private SortedMap<PathFragment, ActionInput> lazyInputMapping;
private PathFragment inputMappingBaseDirectory;
@Nullable private Digest digest;
SpawnExecutionContextImpl(
Spawn spawn,
ActionExecutionContext actionExecutionContext,
@Nullable SandboxedSpawnStrategy.StopConcurrentSpawns stopConcurrentSpawns,
Duration timeout) {
this.spawn = spawn;
this.actionExecutionContext = actionExecutionContext;
this.stopConcurrentSpawns = stopConcurrentSpawns;
this.timeout = timeout;
}
@Override
public int getId() {
return id;
}
@Override
public void setDigest(Digest digest) {
if (this.digest != null) {
checkArgument(
this.digest.equals(digest),
"setDigest was called more than once with different digests: %s vs %s",
this.digest,
digest);
}
this.digest = checkNotNull(digest);
}
@Override
@Nullable
public Digest getDigest() {
return digest;
}
@Override
public ListenableFuture<Void> prefetchInputs() throws ForbiddenActionInputException {
if (Spawns.shouldPrefetchInputsForLocalExecution(spawn)) {
return Futures.catchingAsync(
actionExecutionContext
.getActionInputPrefetcher()
.prefetchFiles(
spawn.getResourceOwner(),
getInputMapping(PathFragment.EMPTY_FRAGMENT, /* willAccessRepeatedly= */ true)
.values(),
getInputMetadataProvider()::getInputMetadata,
Priority.MEDIUM),
BulkTransferException.class,
(BulkTransferException e) -> {
if (executionOptions.useNewExitCodeForLostInputs
|| executionOptions.remoteRetryOnTransientCacheError > 0) {
var message =
BulkTransferException.allCausedByCacheNotFoundException(e)
? "Failed to fetch blobs because they do not exist remotely."
: "Failed to fetch blobs because of a remote cache error.";
throw new EnvironmentalExecException(
e,
FailureDetail.newBuilder()
.setMessage(message)
.setSpawn(
FailureDetails.Spawn.newBuilder().setCode(Code.REMOTE_CACHE_EVICTED))
.build());
} else if (BulkTransferException.allCausedByCacheNotFoundException(e)) {
throw new EnvironmentalExecException(
e,
FailureDetail.newBuilder()
.setMessage("Failed to fetch blobs because they do not exist remotely.")
.setSpawn(
FailureDetails.Spawn.newBuilder().setCode(Code.REMOTE_CACHE_FAILED))
.build());
} else {
throw e;
}
},
directExecutor());
}
return immediateVoidFuture();
}
@Override
public InputMetadataProvider getInputMetadataProvider() {
return actionExecutionContext.getInputMetadataProvider();
}
@Override
public <T extends ActionContext> T getContext(Class<T> identifyingType) {
return actionExecutionContext.getContext(identifyingType);
}
@Override
public ArtifactExpander getArtifactExpander() {
return actionExecutionContext.getArtifactExpander();
}
@Override
public ArtifactPathResolver getPathResolver() {
return actionExecutionContext.getPathResolver();
}
@Override
public SpawnInputExpander getSpawnInputExpander() {
return spawnInputExpander;
}
@Override
public void lockOutputFiles(int exitCode, String errorMessage, FileOutErr outErr)
throws InterruptedException {
if (stopConcurrentSpawns != null) {
stopConcurrentSpawns.stop(exitCode, errorMessage, outErr);
}
}
@Override
public boolean speculating() {
return stopConcurrentSpawns != null;
}
@Override
public Duration getTimeout() {
return timeout;
}
@Override
public FileOutErr getFileOutErr() {
return actionExecutionContext.getFileOutErr();
}
@Override
public SortedMap<PathFragment, ActionInput> getInputMapping(
PathFragment baseDirectory, boolean willAccessRepeatedly)
throws ForbiddenActionInputException {
// Return previously computed copy if present.
if (lazyInputMapping != null && inputMappingBaseDirectory.equals(baseDirectory)) {
return lazyInputMapping;
}
SortedMap<PathFragment, ActionInput> inputMapping;
try (SilentCloseable c =
Profiler.instance().profile("AbstractSpawnStrategy.getInputMapping")) {
inputMapping =
spawnInputExpander.getInputMapping(
spawn,
actionExecutionContext.getArtifactExpander(),
actionExecutionContext.getInputMetadataProvider(),
baseDirectory);
}
// Don't cache the input mapping if it is unlikely that it is used again.
// This reduces memory usage in the case where remote caching/execution is
// used, and the expected cache hit rate is high.
if (willAccessRepeatedly) {
inputMappingBaseDirectory = baseDirectory;
lazyInputMapping = inputMapping;
}
return inputMapping;
}
@Override
public void report(ProgressStatus progress) {
ActionExecutionMetadata action = spawn.getResourceOwner();
if (action.getOwner() == null) {
return;
}
// TODO(djasper): This should not happen as per the contract of ActionExecutionMetadata, but
// there are implementations that violate the contract. Remove when those are gone.
if (action.getPrimaryOutput() == null) {
return;
}
ExtendedEventHandler eventHandler = actionExecutionContext.getEventHandler();
progress.postTo(eventHandler, action);
}
@Override
public boolean isRewindingEnabled() {
return actionExecutionContext.isRewindingEnabled();
}
@Override
public void checkForLostInputs() throws LostInputsExecException {
try {
actionExecutionContext.checkForLostInputs();
} catch (LostInputsActionExecutionException e) {
throw e.toExecException();
}
}
@Nullable
@Override
public FileSystem getActionFileSystem() {
return actionExecutionContext.getActionFileSystem();
}
}
}