| // 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.util.concurrent.Futures.immediateVoidFuture; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| 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.Artifact.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.LostInputsActionExecutionException; |
| import com.google.devtools.build.lib.actions.LostInputsExecException; |
| import com.google.devtools.build.lib.actions.MetadataProvider; |
| 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.actions.cache.MetadataHandler; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler; |
| 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.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 boolean verboseFailures; |
| |
| protected AbstractSpawnStrategy(Path execRoot, SpawnRunner spawnRunner, boolean verboseFailures) { |
| this.spawnInputExpander = new SpawnInputExpander(execRoot, false); |
| this.spawnRunner = spawnRunner; |
| this.verboseFailures = verboseFailures; |
| } |
| |
| /** |
| * Get's 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, 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.getMetadataProvider(), |
| context.getInputMapping(PathFragment.EMPTY_FRAGMENT), |
| context.getTimeout(), |
| spawnResult); |
| } catch (IOException | ForbiddenActionInputException e) { |
| actionExecutionContext |
| .getEventHandler() |
| .handle( |
| Event.warn("Exception " + e + " while logging properties of " + spawn.toString())); |
| } |
| } |
| 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(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; |
| |
| 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 ListenableFuture<Void> prefetchInputs() |
| throws IOException, ForbiddenActionInputException { |
| if (Spawns.shouldPrefetchInputsForLocalExecution(spawn)) { |
| return actionExecutionContext |
| .getActionInputPrefetcher() |
| .prefetchFiles( |
| getInputMapping(PathFragment.EMPTY_FRAGMENT).values(), getMetadataProvider()); |
| } |
| |
| return immediateVoidFuture(); |
| } |
| |
| @Override |
| public MetadataProvider getMetadataProvider() { |
| return actionExecutionContext.getMetadataProvider(); |
| } |
| |
| @Override |
| public MetadataHandler getMetadataInjector() { |
| return actionExecutionContext.getMetadataHandler(); |
| } |
| |
| @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) |
| throws IOException, ForbiddenActionInputException { |
| if (lazyInputMapping == null || !inputMappingBaseDirectory.equals(baseDirectory)) { |
| try (SilentCloseable c = |
| Profiler.instance().profile("AbstractSpawnStrategy.getInputMapping")) { |
| inputMappingBaseDirectory = baseDirectory; |
| lazyInputMapping = |
| spawnInputExpander.getInputMapping( |
| spawn, |
| actionExecutionContext.getArtifactExpander(), |
| baseDirectory, |
| actionExecutionContext.getMetadataProvider()); |
| } |
| } |
| |
| return lazyInputMapping; |
| } |
| |
| @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(); |
| } |
| } |
| } |