| // 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(); |
| } |
| } |
| } |