blob: 342d97715e8944ea13a66d22515c4401dfbf71c9 [file] [log] [blame]
// Copyright 2014 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.skyframe;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.GoogleLogger;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionCacheChecker;
import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
import com.google.devtools.build.lib.actions.ActionCompletionEvent;
import com.google.devtools.build.lib.actions.ActionContext;
import com.google.devtools.build.lib.actions.ActionContext.ActionContextRegistry;
import com.google.devtools.build.lib.actions.ActionContinuationOrResult;
import com.google.devtools.build.lib.actions.ActionExecutedEvent;
import com.google.devtools.build.lib.actions.ActionExecutedEvent.ErrorTiming;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionContext.LostInputsCheck;
import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.ActionInputMap;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.ActionMiddlemanEvent;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.ActionResult;
import com.google.devtools.build.lib.actions.ActionResultReceivedEvent;
import com.google.devtools.build.lib.actions.ActionScanningCompletedEvent;
import com.google.devtools.build.lib.actions.ActionStartedEvent;
import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.ArtifactExpander;
import com.google.devtools.build.lib.actions.Artifact.OwnerlessArtifactWrapper;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.CachedActionEvent;
import com.google.devtools.build.lib.actions.DiscoveredModulesPruner;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.Executor;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.actions.LostInputsActionExecutionException;
import com.google.devtools.build.lib.actions.MetadataProvider;
import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit.ActionCachedContext;
import com.google.devtools.build.lib.actions.PackageRootResolver;
import com.google.devtools.build.lib.actions.ScanningActionEvent;
import com.google.devtools.build.lib.actions.SpawnResult.MetadataLog;
import com.google.devtools.build.lib.actions.StoppedScanningActionEvent;
import com.google.devtools.build.lib.actions.ThreadStateReceiver;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.actions.cache.MetadataHandler;
import com.google.devtools.build.lib.actions.cache.MetadataInjector;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.profiler.SilentCloseable;
import com.google.devtools.build.lib.remote.options.RemoteOptions;
import com.google.devtools.build.lib.runtime.KeepGoingOption;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Execution;
import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.skyframe.ActionExecutionState.ActionStep;
import com.google.devtools.build.lib.skyframe.ActionExecutionState.ActionStepOrResult;
import com.google.devtools.build.lib.skyframe.ActionExecutionState.SharedActionCallback;
import com.google.devtools.build.lib.skyframe.ActionOutputDirectoryHelper.CreateOutputDirectoryException;
import com.google.devtools.build.lib.util.CrashFailureDetails;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException;
import com.google.devtools.build.lib.vfs.OutputService;
import com.google.devtools.build.lib.vfs.OutputService.ActionFileSystemType;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.lib.vfs.XattrProvider;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.common.options.OptionsProvider;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;
/**
* Action executor: takes care of preparing an action for execution, executing it, validating that
* all output artifacts were created, error reporting, etc.
*/
public final class SkyframeActionExecutor {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final MetadataInjector THROWING_METADATA_INJECTOR_FOR_ACTIONFS =
new MetadataInjector() {
@Override
public void injectFile(Artifact output, FileArtifactValue metadata) {
throw new IllegalStateException(
"Unexpected output during input discovery: " + output + " (" + metadata + ")");
}
@Override
public void injectTree(SpecialArtifact output, TreeArtifactValue tree) {
// ActionFS injects only metadata for files.
throw new UnsupportedOperationException(
String.format(
"Unexpected injection of: %s for a tree artifact value: %s", output, tree));
}
};
private final ActionKeyContext actionKeyContext;
private final MetadataConsumerForMetrics outputArtifactsSeen;
private final MetadataConsumerForMetrics outputArtifactsFromActionCache;
private final SyscallCache syscallCache;
private final Function<SkyKey, ThreadStateReceiver> threadStateReceiverFactory;
private Reporter reporter;
private Map<String, String> clientEnv = ImmutableMap.of();
private Executor executorEngine;
private ExtendedEventHandler progressSuppressingEventHandler;
private ActionLogBufferPathGenerator actionLogBufferPathGenerator;
private ActionCacheChecker actionCacheChecker;
private final Profiler profiler = Profiler.instance();
// We keep track of actions already executed this build in order to avoid executing a shared
// action twice. Note that we may still unnecessarily re-execute the action on a subsequent
// build: say actions A and B are shared. If A is requested on the first build and then B is
// requested on the second build, we will execute B even though its output files are up to date.
// However, we will not re-execute A on a subsequent build.
// We do not allow the shared action to re-execute in the same build, even after the first
// action has finished execution, because a downstream action might be reading the output file
// at the same time as the shared action was writing to it.
//
// This map is also used for Actions that try to execute twice because they have discovered
// headers -- the SkyFunction tries to declare a dep on the missing headers and has to restart.
// We don't want to execute the action again on the second entry to the SkyFunction.
// In both cases, we store the already-computed ActionExecutionValue to avoid having to compute it
// again.
private ConcurrentMap<OwnerlessArtifactWrapper, ActionExecutionState> buildActionMap;
// We also keep track of actions which were reset this build from a previously-completed state.
// When re-evaluated, these actions should not emit progress events, in order to not confuse the
// downstream consumers of action-related event streams, which may (reasonably) have expected an
// action to be executed at most once per build.
//
// Note: actions which fail due to lost inputs, and get rewound, will not have any events
// suppressed during their second evaluation. Consumers of events which get emitted before
// execution (e.g. ActionStartedEvent, SpawnExecutedEvent) must support receiving more than one of
// those events per action.
private Set<OwnerlessArtifactWrapper> completedAndResetActions;
// We also keep track of actions that failed due to lost discovered inputs. In some circumstances
// the input discovery process will use a discovered input before requesting it as a dep. If that
// input was generated but is lost, and action rewinding resets it and its generating action, then
// the lost input's generating action must be rerun before the failed action tries input discovery
// again. A previously failed action satisfies that requirement by requesting the deps in this map
// at the start of its next attempt,
private ConcurrentMap<OwnerlessArtifactWrapper, ImmutableList<SkyKey>> lostDiscoveredInputsMap;
private ActionOutputDirectoryHelper outputDirectoryHelper;
private OptionsProvider options;
private boolean useAsyncExecution;
private boolean hadExecutionError;
private boolean replayActionOutErr;
private boolean freeDiscoveredInputsAfterExecution;
private MetadataProvider perBuildFileCache;
private ActionInputPrefetcher actionInputPrefetcher;
/** These variables are nulled out between executions. */
@Nullable private ProgressSupplier progressSupplier;
@Nullable private ActionCompletedReceiver completionReceiver;
private final AtomicReference<ActionExecutionStatusReporter> statusReporterRef;
private OutputService outputService;
private boolean finalizeActions;
private final Supplier<ImmutableList<Root>> sourceRootSupplier;
private DiscoveredModulesPruner discoveredModulesPruner;
SkyframeActionExecutor(
ActionKeyContext actionKeyContext,
MetadataConsumerForMetrics outputArtifactsSeen,
MetadataConsumerForMetrics outputArtifactsFromActionCache,
AtomicReference<ActionExecutionStatusReporter> statusReporterRef,
Supplier<ImmutableList<Root>> sourceRootSupplier,
SyscallCache syscallCache,
Function<SkyKey, ThreadStateReceiver> threadStateReceiverFactory) {
this.actionKeyContext = actionKeyContext;
this.outputArtifactsSeen = outputArtifactsSeen;
this.outputArtifactsFromActionCache = outputArtifactsFromActionCache;
this.statusReporterRef = statusReporterRef;
this.sourceRootSupplier = sourceRootSupplier;
this.syscallCache = syscallCache;
this.threadStateReceiverFactory = threadStateReceiverFactory;
}
SharedActionCallback getSharedActionCallback(
ExtendedEventHandler eventHandler,
boolean hasDiscoveredInputs,
Action action,
ActionLookupData actionLookupData) {
return new SharedActionCallback() {
@Override
public void actionStarted() {
if (hasDiscoveredInputs) {
eventHandler.post(new ActionScanningCompletedEvent(action, actionLookupData));
}
}
@Override
public void actionCompleted() {
if (completionReceiver != null) {
completionReceiver.actionCompleted(actionLookupData);
}
}
};
}
void prepareForExecution(
Reporter reporter,
Executor executor,
OptionsProvider options,
ActionCacheChecker actionCacheChecker,
OutputService outputService,
boolean trackIncrementalState) {
this.reporter = Preconditions.checkNotNull(reporter);
this.executorEngine = Preconditions.checkNotNull(executor);
this.progressSuppressingEventHandler = new ProgressSuppressingEventHandler(reporter);
// Start with a new map each build so there's no issue with internal resizing.
this.buildActionMap = Maps.newConcurrentMap();
this.completedAndResetActions = Sets.newConcurrentHashSet();
this.lostDiscoveredInputsMap = Maps.newConcurrentMap();
this.hadExecutionError = false;
this.actionCacheChecker = Preconditions.checkNotNull(actionCacheChecker);
// Don't cache possibly stale data from the last build.
this.options = options;
// Cache some option values for performance, since we consult them on every action.
this.useAsyncExecution = options.getOptions(BuildRequestOptions.class).useAsyncExecution;
this.finalizeActions = options.getOptions(BuildRequestOptions.class).finalizeActions;
this.replayActionOutErr = options.getOptions(BuildRequestOptions.class).replayActionOutErr;
this.outputService = outputService;
this.outputDirectoryHelper =
new ActionOutputDirectoryHelper(
options.getOptions(BuildRequestOptions.class).directoryCreationCacheSpec);
// Retaining discovered inputs is only worthwhile for incremental builds or builds with extra
// actions, which consume their shadowed action's discovered inputs.
freeDiscoveredInputsAfterExecution =
!trackIncrementalState && options.getOptions(CoreOptions.class).actionListeners.isEmpty();
}
public void setActionLogBufferPathGenerator(
ActionLogBufferPathGenerator actionLogBufferPathGenerator) {
this.actionLogBufferPathGenerator = actionLogBufferPathGenerator;
}
public void setClientEnv(Map<String, String> clientEnv) {
// Copy once here, instead of on every construction of ActionExecutionContext.
this.clientEnv = ImmutableMap.copyOf(clientEnv);
}
ActionFileSystemType actionFileSystemType() {
return outputService != null
? outputService.actionFileSystemType()
: ActionFileSystemType.DISABLED;
}
Path getExecRoot() {
return executorEngine.getExecRoot();
}
ActionContextRegistry getActionContextRegistry() {
return executorEngine;
}
boolean useArchivedTreeArtifacts(ActionAnalysisMetadata action) {
return options
.getOptions(CoreOptions.class)
.archivedArtifactsMnemonicsFilter
.test(action.getMnemonic());
}
boolean supportsPartialTreeArtifactInputs() {
return actionInputPrefetcher.supportsPartialTreeArtifactInputs();
}
boolean publishTargetSummaries() {
return options.getOptions(BuildEventProtocolOptions.class).publishTargetSummary;
}
XattrProvider getXattrProvider() {
return syscallCache;
}
/** REQUIRES: {@link #actionFileSystemType()} to be not {@code DISABLED}. */
FileSystem createActionFileSystem(
String relativeOutputPath,
ActionInputMap inputArtifactData,
Iterable<Artifact> outputArtifacts,
boolean rewindingEnabled) {
return outputService.createActionFileSystem(
executorEngine.getFileSystem(),
executorEngine.getExecRoot().asFragment(),
relativeOutputPath,
sourceRootSupplier.get(),
inputArtifactData,
outputArtifacts,
rewindingEnabled);
}
private void updateActionFileSystemContext(
FileSystem actionFileSystem,
Environment env,
MetadataInjector metadataInjector,
ImmutableMap<Artifact, ImmutableList<FilesetOutputSymlink>> filesets) {
outputService.updateActionFileSystemContext(actionFileSystem, env, metadataInjector, filesets);
}
void executionOver() {
// These may transitively holds a bunch of heavy objects, so it's important to clear it at the
// end of a build.
this.reporter = null;
this.options = null;
this.executorEngine = null;
this.progressSuppressingEventHandler = null;
this.outputService = null;
this.buildActionMap = null;
this.completedAndResetActions = null;
this.lostDiscoveredInputsMap = null;
this.actionCacheChecker = null;
this.outputDirectoryHelper = null;
}
/**
* Due to multi-threading, a null return value from this method does not guarantee that there is
* no such action - a concurrent thread may already be executing the same (shared) action. Any
* such race is resolved in the subsequent call to {@link #executeAction}.
*/
@Nullable
ActionExecutionState probeActionExecution(Action action) {
return buildActionMap.get(new OwnerlessArtifactWrapper(action.getPrimaryOutput()));
}
/**
* Determines whether the action should have its progress events emitted.
*
* <p>Returns {@code false} for completed and reset actions, indicating that their progress events
* should be suppressed.
*/
boolean shouldEmitProgressEvents(Action action) {
return !completedAndResetActions.contains(
new OwnerlessArtifactWrapper(action.getPrimaryOutput()));
}
void resetPreviouslyCompletedAction(ActionLookupData actionLookupData, Action action) {
OwnerlessArtifactWrapper ownerlessArtifactWrapper =
new OwnerlessArtifactWrapper(action.getPrimaryOutput());
ActionExecutionState actionExecutionState = buildActionMap.get(ownerlessArtifactWrapper);
if (actionExecutionState != null) {
actionExecutionState.obsolete(actionLookupData, buildActionMap, ownerlessArtifactWrapper);
}
completedAndResetActions.add(ownerlessArtifactWrapper);
if (!actionFileSystemType().inMemoryFileSystem()) {
outputDirectoryHelper.invalidateTreeArtifactDirectoryCreation(action.getOutputs());
}
}
@Nullable
ImmutableList<SkyKey> getLostDiscoveredInputs(Action action) {
return lostDiscoveredInputsMap.get(new OwnerlessArtifactWrapper(action.getPrimaryOutput()));
}
void resetRewindingAction(
ActionLookupData actionLookupData,
Action action,
ImmutableList<SkyKey> lostDiscoveredInputs) {
OwnerlessArtifactWrapper ownerlessArtifactWrapper =
new OwnerlessArtifactWrapper(action.getPrimaryOutput());
ActionExecutionState state = buildActionMap.get(ownerlessArtifactWrapper);
if (state != null) {
// If an action failed from lost inputs during input discovery then it won't have a state to
// obsolete.
state.obsolete(actionLookupData, buildActionMap, ownerlessArtifactWrapper);
}
if (!lostDiscoveredInputs.isEmpty()) {
lostDiscoveredInputsMap.put(ownerlessArtifactWrapper, lostDiscoveredInputs);
}
if (!actionFileSystemType().inMemoryFileSystem()) {
outputDirectoryHelper.invalidateTreeArtifactDirectoryCreation(action.getOutputs());
}
}
void noteActionEvaluationStarted(ActionLookupData actionLookupData, Action action) {
if (completionReceiver != null) {
completionReceiver.noteActionEvaluationStarted(actionLookupData, action);
}
}
/**
* Executes the provided action on the current thread. Returns the ActionExecutionValue with the
* result, either computed here or already computed on another thread.
*/
@SuppressWarnings("SynchronizeOnNonFinalField")
ActionExecutionValue executeAction(
Environment env,
Action action,
ActionMetadataHandler metadataHandler,
long actionStartTime,
ActionLookupData actionLookupData,
ArtifactExpander artifactExpander,
ImmutableMap<Artifact, ImmutableList<FilesetOutputSymlink>> expandedFilesets,
ImmutableMap<Artifact, ImmutableList<FilesetOutputSymlink>> topLevelFilesets,
@Nullable FileSystem actionFileSystem,
@Nullable Object skyframeDepsResult,
ActionPostprocessing postprocessing,
boolean hasDiscoveredInputs)
throws ActionExecutionException, InterruptedException {
if (actionFileSystem != null) {
updateActionFileSystemContext(actionFileSystem, env, metadataHandler, expandedFilesets);
}
ActionExecutionContext actionExecutionContext =
getContext(
env,
action,
metadataHandler,
artifactExpander,
topLevelFilesets,
actionFileSystem,
skyframeDepsResult,
actionLookupData);
if (actionCacheChecker.isActionExecutionProhibited(action)) {
// We can't execute an action (e.g. because --check_???_up_to_date option was used). Fail the
// build instead.
String message = action.prettyPrint() + " is not up-to-date";
DetailedExitCode code = createDetailedExitCode(message, Code.ACTION_NOT_UP_TO_DATE);
ActionExecutionException e = new ActionExecutionException(message, action, false, code);
Event error = Event.error(e.getMessage());
synchronized (reporter) {
reporter.handle(error);
}
throw e;
}
// Use computeIfAbsent to handle concurrent attempts to execute the same shared action.
ActionExecutionState activeAction =
buildActionMap.computeIfAbsent(
new OwnerlessArtifactWrapper(action.getPrimaryOutput()),
(unusedKey) ->
new ActionExecutionState(
actionLookupData,
new ActionRunner(
action,
metadataHandler,
actionStartTime,
actionExecutionContext,
actionLookupData,
postprocessing)));
SharedActionCallback callback =
getSharedActionCallback(env.getListener(), hasDiscoveredInputs, action, actionLookupData);
ActionExecutionValue result = null;
ActionExecutionException finalException = null;
try {
result = activeAction.getResultOrDependOnFuture(env, actionLookupData, action, callback);
} catch (ActionExecutionException e) {
finalException = e;
}
if (result != null || finalException != null) {
closeContext(actionExecutionContext, action, finalException);
}
return result;
}
private ExtendedEventHandler selectEventHandler(Action action) {
return selectEventHandler(shouldEmitProgressEvents(action));
}
private ExtendedEventHandler selectEventHandler(boolean emitProgressEvents) {
return emitProgressEvents ? reporter : progressSuppressingEventHandler;
}
private ActionExecutionContext getContext(
Environment env,
Action action,
MetadataHandler metadataHandler,
ArtifactExpander artifactExpander,
ImmutableMap<Artifact, ImmutableList<FilesetOutputSymlink>> topLevelFilesets,
@Nullable FileSystem actionFileSystem,
@Nullable Object skyframeDepsResult,
ActionLookupData actionLookupData)
throws InterruptedException {
boolean emitProgressEvents = shouldEmitProgressEvents(action);
ArtifactPathResolver artifactPathResolver =
ArtifactPathResolver.createPathResolver(actionFileSystem, executorEngine.getExecRoot());
FileOutErr fileOutErr;
if (replayActionOutErr) {
String actionKey = action.getKey(actionKeyContext, artifactExpander);
fileOutErr = actionLogBufferPathGenerator.persistent(actionKey, artifactPathResolver);
try {
fileOutErr.getErrorPath().delete();
fileOutErr.getOutputPath().delete();
} catch (IOException e) {
throw new IllegalStateException(e);
}
} else {
fileOutErr = actionLogBufferPathGenerator.generate(artifactPathResolver);
}
return new ActionExecutionContext(
executorEngine,
createFileCache(metadataHandler, actionFileSystem),
actionInputPrefetcher,
actionKeyContext,
metadataHandler,
env.restartPermitted(),
lostInputsCheck(actionFileSystem, action, outputService),
fileOutErr,
replayActionOutErr && emitProgressEvents
? env.getListener()
: selectEventHandler(emitProgressEvents),
clientEnv,
topLevelFilesets,
artifactExpander,
actionFileSystem,
skyframeDepsResult,
discoveredModulesPruner,
syscallCache,
threadStateReceiverFactory.apply(actionLookupData));
}
private static void closeContext(
ActionExecutionContext context,
Action action,
@Nullable ActionExecutionException finalException)
throws ActionExecutionException {
try (Closeable c = context) {
if (finalException != null) {
throw finalException;
}
} catch (IOException e) {
String message = "Failed to close action output: " + e.getMessage();
DetailedExitCode code = createDetailedExitCode(message, Code.ACTION_OUTPUT_CLOSE_FAILURE);
throw new ActionExecutionException(message, e, action, /*catastrophe=*/ false, code);
}
}
/**
* Checks the action cache to see if {@code action} needs to be executed, or is up to date.
* Returns a token with the semantics of {@link ActionCacheChecker#getTokenIfNeedToExecute}: null
* if the action is up to date, and non-null if it needs to be executed, in which case that token
* should be provided to the ActionCacheChecker after execution.
*/
Token checkActionCache(
ExtendedEventHandler eventHandler,
Action action,
MetadataHandler metadataHandler,
ArtifactExpander artifactExpander,
long actionStartTime,
List<Artifact> resolvedCacheArtifacts,
Map<String, String> clientEnv,
ArtifactPathResolver pathResolver)
throws ActionExecutionException, InterruptedException {
Token token;
RemoteOptions remoteOptions;
SortedMap<String, String> remoteDefaultProperties;
EventHandler handler;
boolean loadCachedOutputMetadata;
try (SilentCloseable c = profiler.profile(ProfilerTask.ACTION_CHECK, action.describe())) {
remoteOptions = this.options.getOptions(RemoteOptions.class);
remoteDefaultProperties =
remoteOptions != null
? remoteOptions.getRemoteDefaultExecProperties()
: ImmutableSortedMap.of();
loadCachedOutputMetadata =
outputService
!= null; // Only load cached output metadata if remote output service is available
handler =
options.getOptions(BuildRequestOptions.class).explanationPath != null ? reporter : null;
token =
actionCacheChecker.getTokenIfNeedToExecute(
action,
resolvedCacheArtifacts,
clientEnv,
handler,
metadataHandler,
artifactExpander,
remoteDefaultProperties,
loadCachedOutputMetadata);
} catch (UserExecException e) {
throw ActionExecutionException.fromExecException(e, action);
}
if (token == null) {
boolean eventPosted = false;
// Notify BlazeRuntimeStatistics about the action middleman 'execution'.
if (action.getActionType().isMiddleman()) {
eventHandler.post(new ActionMiddlemanEvent(action, actionStartTime));
eventPosted = true;
}
if (replayActionOutErr) {
// TODO(ulfjack): This assumes that the stdout/stderr files are unmodified. It would be
// better to integrate them with the action cache and rerun the action when they change.
String actionKey = action.getKey(actionKeyContext, artifactExpander);
FileOutErr fileOutErr = actionLogBufferPathGenerator.persistent(actionKey, pathResolver);
// getOutputPath and getErrorPath cause the FileOutErr to be marked as "dirty" which
// invalidates any prior in-memory state it had. Need to do this so that hasRecordedOutput()
// checks for file existence again.
fileOutErr.getOutputPath();
fileOutErr.getErrorPath();
if (fileOutErr.hasRecordedOutput()) {
dumpRecordedOutErr(eventHandler, action, fileOutErr);
}
}
if (action instanceof NotifyOnActionCacheHit) {
NotifyOnActionCacheHit notify = (NotifyOnActionCacheHit) action;
ExtendedEventHandler contextEventHandler = selectEventHandler(action);
ActionCachedContext context =
new ActionCachedContext() {
@Override
public ExtendedEventHandler getEventHandler() {
return contextEventHandler;
}
@Override
public Path getExecRoot() {
return executorEngine.getExecRoot();
}
@Override
public <T extends ActionContext> T getContext(Class<? extends T> type) {
return executorEngine.getContext(type);
}
};
boolean recordActionCacheHit = notify.actionCacheHit(context);
if (!recordActionCacheHit) {
token =
actionCacheChecker.getTokenUnconditionallyAfterFailureToRecordActionCacheHit(
action,
resolvedCacheArtifacts,
clientEnv,
handler,
metadataHandler,
artifactExpander,
remoteDefaultProperties,
loadCachedOutputMetadata);
}
}
// We still need to check the outputs so that output file data is available to the value.
// Filesets cannot be cached in the action cache, so it is fine to pass null here.
checkOutputs(
action,
metadataHandler,
/*filesetOutputSymlinksForMetrics=*/ null,
/*isActionCacheHitForMetrics=*/ true);
if (!eventPosted) {
eventHandler.post(new CachedActionEvent(action, actionStartTime));
}
}
return token;
}
void updateActionCache(
Action action,
MetadataHandler metadataHandler,
ArtifactExpander artifactExpander,
Token token,
Map<String, String> clientEnv)
throws ActionExecutionException, InterruptedException {
if (!actionCacheChecker.enabled()) {
return;
}
final SortedMap<String, String> remoteDefaultProperties;
try {
RemoteOptions remoteOptions = this.options.getOptions(RemoteOptions.class);
remoteDefaultProperties =
remoteOptions != null
? remoteOptions.getRemoteDefaultExecProperties()
: ImmutableSortedMap.of();
} catch (UserExecException e) {
throw ActionExecutionException.fromExecException(e, action);
}
try {
actionCacheChecker.updateActionCache(
action, token, metadataHandler, artifactExpander, clientEnv, remoteDefaultProperties);
} catch (IOException e) {
// Skyframe has already done all the filesystem access needed for outputs and swallows
// IOExceptions for inputs. So an IOException is impossible here.
throw new IllegalStateException(
"failed to update action cache for "
+ action.prettyPrint()
+ ", but all outputs should already have been checked",
e);
}
}
@Nullable
List<Artifact> getActionCachedInputs(Action action, PackageRootResolver resolver)
throws AlreadyReportedActionExecutionException, InterruptedException {
try {
return actionCacheChecker.getCachedInputs(action, resolver);
} catch (PackageRootResolver.PackageRootException e) {
printError(e.getMessage(), action);
throw new AlreadyReportedActionExecutionException(
new ActionExecutionException(
e,
action,
/*catastrophe=*/ false,
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(e.getMessage())
.setIncludeScanning(e.getError())
.build())));
}
}
/**
* Perform dependency discovery for action, which must discover its inputs.
*
* <p>This method is just a wrapper around {@link Action#discoverInputs} that properly processes
* any {@link ActionExecutionException} thrown before rethrowing it to the caller.
*/
NestedSet<Artifact> discoverInputs(
Action action,
ActionLookupData actionLookupData,
MetadataHandler metadataHandler,
Environment env,
@Nullable FileSystem actionFileSystem)
throws ActionExecutionException, InterruptedException {
FileOutErr fileOutErr =
actionLogBufferPathGenerator.generate(
ArtifactPathResolver.createPathResolver(
actionFileSystem, executorEngine.getExecRoot()));
ExtendedEventHandler eventHandler = selectEventHandler(action);
ActionExecutionContext actionExecutionContext =
ActionExecutionContext.forInputDiscovery(
executorEngine,
createFileCache(metadataHandler, actionFileSystem),
actionInputPrefetcher,
actionKeyContext,
metadataHandler,
env.restartPermitted(),
lostInputsCheck(actionFileSystem, action, outputService),
fileOutErr,
eventHandler,
clientEnv,
env,
actionFileSystem,
discoveredModulesPruner,
syscallCache,
threadStateReceiverFactory.apply(actionLookupData));
if (actionFileSystem != null) {
updateActionFileSystemContext(
actionFileSystem,
env,
THROWING_METADATA_INJECTOR_FOR_ACTIONFS,
/*filesets=*/ ImmutableMap.of());
// Note that when not using ActionFS, a global setup of the parent directories of the OutErr
// streams is sufficient.
setupActionFsFileOutErr(fileOutErr, action);
}
eventHandler.post(new ScanningActionEvent(action));
ActionExecutionException finalException = null;
try {
NestedSet<Artifact> artifacts = action.discoverInputs(actionExecutionContext);
// Input discovery may have been affected by lost inputs. If an action filesystem is used, it
// may know whether inputs were lost. We should fail fast if any were; rewinding may be able
// to fix it.
checkActionFileSystemForLostInputs(actionFileSystem, action, outputService);
return artifacts;
} catch (ActionExecutionException e) {
// Input discovery failures may be caused by lost inputs. Lost input failures have higher
// priority because rewinding may be able to restore what was lost and allow the action to
// complete without error.
if (!(e instanceof LostInputsActionExecutionException)) {
try {
checkActionFileSystemForLostInputs(actionFileSystem, action, outputService);
} catch (LostInputsActionExecutionException lostInputsException) {
e = lostInputsException;
}
}
Path primaryOutputPath = actionExecutionContext.getInputPath(action.getPrimaryOutput());
if (e instanceof LostInputsActionExecutionException) {
// If inputs were lost during input discovery, then enrich the exception, informing action
// rewinding machinery that these lost inputs are now Skyframe deps of the action.
LostInputsActionExecutionException lostInputsException =
(LostInputsActionExecutionException) e;
lostInputsException.setFromInputDiscovery();
enrichLostInputsException(
primaryOutputPath, actionLookupData, fileOutErr, lostInputsException);
finalException = lostInputsException;
} else {
finalException =
processAndGetExceptionToThrow(
env.getListener(),
primaryOutputPath,
action,
e,
fileOutErr,
ErrorTiming.BEFORE_EXECUTION);
}
throw finalException;
} finally {
eventHandler.post(new StoppedScanningActionEvent(action));
closeContext(actionExecutionContext, action, finalException);
}
}
private MetadataProvider createFileCache(
MetadataProvider graphFileCache, @Nullable FileSystem actionFileSystem) {
if (actionFileSystem instanceof MetadataProvider) {
return (MetadataProvider) actionFileSystem;
}
return new DelegatingPairFileCache(graphFileCache, perBuildFileCache);
}
/**
* This method should be called if the builder encounters an error during execution. This allows
* the builder to record that it encountered at least one error, and may make it swallow its
* output to prevent spamming the user any further.
*/
void recordExecutionError() {
hadExecutionError = true;
}
/**
* Returns true if the Builder is winding down (i.e. cancelling outstanding actions and preparing
* to abort.) The builder is winding down iff:
*
* <ul>
* <li>we had an execution error
* <li>we are not running with --keep_going
* </ul>
*/
private boolean isBuilderAborting() {
return hadExecutionError && !options.getOptions(KeepGoingOption.class).keepGoing;
}
public void configure(
MetadataProvider fileCache,
ActionInputPrefetcher actionInputPrefetcher,
DiscoveredModulesPruner discoveredModulesPruner) {
this.perBuildFileCache = fileCache;
this.actionInputPrefetcher = actionInputPrefetcher;
this.discoveredModulesPruner = discoveredModulesPruner;
}
/**
* Temporary interface to allow delegation of action postprocessing to ActionExecutionFunction.
* The current implementation requires access to local fields in ActionExecutionFunction.
*/
interface ActionPostprocessing {
void run(
Environment env,
Action action,
ActionMetadataHandler metadataHandler,
Map<String, String> clientEnv)
throws InterruptedException, ActionExecutionException;
}
private static ActionContinuationOrResult begin(
Action action, ActionExecutionContext actionExecutionContext) {
return new ActionContinuationOrResult() {
@Nullable
@Override
public ListenableFuture<?> getFuture() {
return null;
}
@Override
public ActionContinuationOrResult execute()
throws ActionExecutionException, InterruptedException {
return action.beginExecution(actionExecutionContext);
}
};
}
/** Returns a continuation to run the specified action in a profiler task. */
private static ActionContinuationOrResult runFully(
Action action, ActionExecutionContext actionExecutionContext) {
return new ActionContinuationOrResult() {
@Nullable
@Override
public ListenableFuture<?> getFuture() {
return null;
}
@Override
public ActionContinuationOrResult execute()
throws ActionExecutionException, InterruptedException {
return ActionContinuationOrResult.of(action.execute(actionExecutionContext));
}
};
}
/** Represents an action that needs to be run. */
private final class ActionRunner extends ActionStep {
private final Action action;
private final ActionMetadataHandler metadataHandler;
private final long actionStartTime;
private final ActionExecutionContext actionExecutionContext;
private final ActionLookupData actionLookupData;
@Nullable private final ActionExecutionStatusReporter statusReporter;
private final ActionPostprocessing postprocessing;
ActionRunner(
Action action,
ActionMetadataHandler metadataHandler,
long actionStartTime,
ActionExecutionContext actionExecutionContext,
ActionLookupData actionLookupData,
ActionPostprocessing postprocessing) {
this.action = action;
this.metadataHandler = metadataHandler;
this.actionStartTime = actionStartTime;
this.actionExecutionContext = actionExecutionContext;
this.actionLookupData = actionLookupData;
this.statusReporter = statusReporterRef.get();
this.postprocessing = postprocessing;
}
@SuppressWarnings("LogAndThrow") // Thrown exception shown in user output, not info logs.
@Override
public ActionStepOrResult run(Environment env)
throws LostInputsActionExecutionException, InterruptedException {
// There are three ExtendedEventHandler instances available while this method is running.
// SkyframeActionExecutor.this.reporter
// actionExecutionContext.getEventHandler
// env.getListener
// Apparently, one isn't enough.
//
// Progress events that are generated in this class should be posted to env.getListener, while
// progress events that are generated in the Action implementation are posted to
// actionExecutionContext.getEventHandler. The reason for this is action rewinding, in which
// case env.getListener may be a ProgressSuppressingEventHandler. See shouldEmitProgressEvents
// and completedAndResetActions.
//
// It is also unclear why we are posting anything directly to reporter. That probably
// shouldn't happen.
try (SilentCloseable c =
profiler.profileAction(
ProfilerTask.ACTION,
action.getMnemonic(),
action.describe(),
action.getPrimaryOutput().getExecPathString(),
getOwnerLabelAsString(action))) {
String message = action.getProgressMessage();
if (message != null) {
reporter.startTask(null, prependExecPhaseStats(message));
}
try {
// It is vital that updateStatus and remove are called in pairs. Unfortunately, if async
// action execution is enabled, we cannot use a simple finally block, but have to manually
// ensure that any code path that finishes the state machine also removes the action from
// the status reporter.
// To complicate things, the ActionCompletionEvent must _not_ be posted when this action
// is rewound.
// TODO(ulfjack): Change the uses of ActionStartedEvent and ActionCompletionEvent such
// that they can be reposted when rewinding and simplify this code path. Maybe also keep
// track of the rewind attempt, so that listeners can use that to adjust their behavior.
ActionStartedEvent event = new ActionStartedEvent(action, actionStartTime);
if (statusReporter != null) {
statusReporter.updateStatus(event);
}
env.getListener().post(event);
if (actionFileSystemType().supportsLocalActions()) {
try (SilentCloseable d = profiler.profile(ProfilerTask.INFO, "action.prepare")) {
// This call generally deletes any files at locations that are declared outputs of the
// action, although some actions perform additional work, while others intentionally
// keep previous outputs in place.
action.prepare(
actionExecutionContext.getExecRoot(),
actionExecutionContext.getPathResolver(),
outputService != null ? outputService.bulkDeleter() : null,
useArchivedTreeArtifacts(action));
} catch (IOException e) {
logger.atWarning().withCause(e).log(
"failed to delete output files before executing action: '%s'", action);
throw toActionExecutionException(
"failed to delete output files before executing action",
e,
action,
null,
Code.ACTION_OUTPUTS_DELETION_FAILURE);
}
}
if (actionFileSystemType().inMemoryFileSystem()) {
// There's nothing to delete when the action file system is used, but we must ensure
// that the output directories for stdout and stderr exist.
setupActionFsFileOutErr(actionExecutionContext.getFileOutErr(), action);
createActionFsOutputDirectories(action, actionExecutionContext.getPathResolver());
} else {
createOutputDirectories(action);
}
} catch (ActionExecutionException e) {
// This try-catch block cannot trigger rewinding, so it is safe to notify the status
// reporter and also post the ActionCompletionEvent.
notifyActionCompletion(env.getListener(), /*postActionCompletionEvent=*/ true);
return ActionStepOrResult.of(e);
}
// This is the first iteration of the async action execution framework. It is currently only
// implemented for SpawnAction (and subclasses), and will need to be extended for all other
// action types.
if (useAsyncExecution) {
// TODO(ulfjack): This causes problems in that REMOTE_EXECUTION segments now heavily
// overlap in the Json profile, which the renderer isn't able to handle. We should move
// those to some sort of 'virtual thread' to visualize the work that's happening on other
// machines.
return continueAction(
actionExecutionContext.getEventHandler(), begin(action, actionExecutionContext));
}
return continueAction(env.getListener(), runFully(action, actionExecutionContext));
}
}
private String getOwnerLabelAsString(Action action) {
ActionOwner owner = action.getOwner();
if (owner == null) {
return "";
}
Label ownerLabel = owner.getLabel();
if (ownerLabel == null) {
return "";
}
return ownerLabel.getCanonicalForm();
}
private void notifyActionCompletion(
ExtendedEventHandler eventHandler, boolean postActionCompletionEvent) {
if (statusReporter != null) {
statusReporter.remove(action);
}
if (postActionCompletionEvent) {
eventHandler.post(new ActionCompletionEvent(actionStartTime, action, actionLookupData));
}
String message = action.getProgressMessage();
if (message != null) {
if (completionReceiver != null) {
completionReceiver.actionCompleted(actionLookupData);
}
reporter.finishTask(null, prependExecPhaseStats(message));
}
}
/** Executes the given continuation and returns a new one or a final result. */
private ActionStepOrResult continueAction(
ExtendedEventHandler eventHandler, ActionContinuationOrResult actionContinuation)
throws LostInputsActionExecutionException, InterruptedException {
// Every code path that exits this method must call notifyActionCompletion, except for the
// one that returns a new ActionContinuationStep. Unfortunately, that requires some code
// duplication.
ActionContinuationOrResult nextActionContinuationOrResult;
try (SilentCloseable c = profiler.profile(ProfilerTask.INFO, "ActionContinuation.execute")) {
nextActionContinuationOrResult = actionContinuation.execute();
// An action's result (or intermediate state) may have been affected by lost inputs. If an
// action filesystem is used, it may know whether inputs were lost. We should fail fast if
// any were; rewinding may be able to fix it.
checkActionFileSystemForLostInputs(
actionExecutionContext.getActionFileSystem(), action, outputService);
} catch (ActionExecutionException e) {
LostInputsActionExecutionException lostInputsException = null;
// Action failures may be caused by lost inputs. Lost input failures have higher priority
// because rewinding may be able to restore what was lost and allow the action to complete
// without error.
if (e instanceof LostInputsActionExecutionException) {
lostInputsException = (LostInputsActionExecutionException) e;
} else {
try {
checkActionFileSystemForLostInputs(
actionExecutionContext.getActionFileSystem(), action, outputService);
} catch (LostInputsActionExecutionException e2) {
lostInputsException = e2;
}
}
Path primaryOutputPath = actionExecutionContext.getInputPath(action.getPrimaryOutput());
notifyActionCompletion(
eventHandler, /*postActionCompletionEvent=*/ lostInputsException == null);
if (lostInputsException != null) {
// If inputs are lost, then avoid publishing ActionExecutedEvent or reporting the error.
// Action rewinding will rerun this failed action after trying to regenerate the lost
// inputs.
lostInputsException.setActionStartedEventAlreadyEmitted();
enrichLostInputsException(
primaryOutputPath,
actionLookupData,
actionExecutionContext.getFileOutErr(),
lostInputsException);
throw lostInputsException;
}
return ActionStepOrResult.of(
processAndGetExceptionToThrow(
eventHandler,
primaryOutputPath,
action,
e,
actionExecutionContext.getFileOutErr(),
ErrorTiming.AFTER_EXECUTION));
} catch (InterruptedException e) {
notifyActionCompletion(eventHandler, /*postActionCompletionEvent=*/ true);
return ActionStepOrResult.of(e);
}
if (!nextActionContinuationOrResult.isDone()) {
return new ActionContinuationStep(nextActionContinuationOrResult);
}
try {
ActionExecutionValue actionExecutionValue;
try (SilentCloseable c =
profiler.profile(ProfilerTask.ACTION_COMPLETE, "actuallyCompleteAction")) {
actionExecutionValue =
actuallyCompleteAction(eventHandler, nextActionContinuationOrResult.get());
}
return new ActionPostprocessingStep(actionExecutionValue);
} catch (ActionExecutionException e) {
return ActionStepOrResult.of(e);
} finally {
notifyActionCompletion(eventHandler, /*postActionCompletionEvent=*/ true);
}
}
@SuppressWarnings("LogAndThrow") // Thrown exception shown in user output, not info logs.
private ActionExecutionValue actuallyCompleteAction(
ExtendedEventHandler eventHandler, ActionResult actionResult)
throws ActionExecutionException, InterruptedException {
boolean outputAlreadyDumped = false;
if (actionResult != ActionResult.EMPTY) {
eventHandler.post(new ActionResultReceivedEvent(action, actionResult));
}
// Action terminated fine, now report the output.
// The .showOutput() method is not necessarily a quick check: in its
// current implementation it uses regular expression matching.
FileOutErr outErrBuffer = actionExecutionContext.getFileOutErr();
if (outErrBuffer.hasRecordedOutput()) {
if (replayActionOutErr) {
dumpRecordedOutErr(actionExecutionContext.getEventHandler(), action, outErrBuffer);
outputAlreadyDumped = true;
} else if (action.showsOutputUnconditionally()
|| reporter.showOutput(Label.print(action.getOwner().getLabel()))) {
dumpRecordedOutErr(reporter, action, outErrBuffer);
outputAlreadyDumped = true;
}
}
MetadataHandler metadataHandler = actionExecutionContext.getMetadataHandler();
FileOutErr fileOutErr = actionExecutionContext.getFileOutErr();
Artifact primaryOutput = action.getPrimaryOutput();
Path primaryOutputPath = actionExecutionContext.getInputPath(primaryOutput);
try {
Preconditions.checkState(
action.inputsDiscovered(),
"Action %s successfully executed, but inputs still not known",
action);
try {
flushActionFileSystem(actionExecutionContext.getActionFileSystem(), outputService);
} catch (IOException e) {
logger.atWarning().withCause(e).log("unable to flush action filesystem: '%s'", action);
throw toActionExecutionException(
"unable to flush action filesystem",
e,
action,
fileOutErr,
Code.ACTION_FINALIZATION_FAILURE);
}
if (!checkOutputs(
action,
metadataHandler,
actionExecutionContext.getOutputSymlinks(),
/*isActionCacheHitForMetrics=*/ false)) {
throw toActionExecutionException(
"not all outputs were created or valid",
null,
action,
outputAlreadyDumped ? null : fileOutErr,
Code.ACTION_OUTPUTS_NOT_CREATED);
}
if (outputService != null && finalizeActions) {
try (SilentCloseable c =
profiler.profile(ProfilerTask.INFO, "outputService.finalizeAction")) {
outputService.finalizeAction(action, metadataHandler);
} catch (EnvironmentalExecException | IOException e) {
logger.atWarning().withCause(e).log("unable to finalize action: '%s'", action);
throw toActionExecutionException(
"unable to finalize action",
e,
action,
fileOutErr,
Code.ACTION_FINALIZATION_FAILURE);
}
}
} catch (ActionExecutionException actionException) {
// Success in execution but failure in completion.
reportActionExecution(
eventHandler,
primaryOutputPath,
/*primaryOutputMetadata=*/ null,
action,
actionResult,
actionException,
fileOutErr,
ErrorTiming.AFTER_EXECUTION);
throw actionException;
} catch (IllegalStateException exception) {
// More serious internal error, but failure still reported.
reportActionExecution(
eventHandler,
primaryOutputPath,
/*primaryOutputMetadata=*/ null,
action,
actionResult,
new ActionExecutionException(
exception,
action,
true,
CrashFailureDetails.detailedExitCodeForThrowable(exception)),
fileOutErr,
ErrorTiming.AFTER_EXECUTION);
throw exception;
}
FileArtifactValue primaryOutputMetadata;
if (metadataHandler.artifactOmitted(primaryOutput)) {
primaryOutputMetadata = FileArtifactValue.OMITTED_FILE_MARKER;
} else {
try {
primaryOutputMetadata = metadataHandler.getMetadata(primaryOutput);
} catch (IOException e) {
throw new IllegalStateException("Metadata already obtained for " + primaryOutput, e);
}
}
reportActionExecution(
eventHandler,
primaryOutputPath,
primaryOutputMetadata,
action,
actionResult,
null,
fileOutErr,
ErrorTiming.NO_ERROR);
Preconditions.checkState(
actionExecutionContext.getOutputSymlinks() == null
|| action instanceof SkyframeAwareAction,
"Unexpected to find outputSymlinks set"
+ " in an action which is not a SkyframeAwareAction. Action: %s\n symlinks:%s",
action,
actionExecutionContext.getOutputSymlinks());
return ActionExecutionValue.createFromOutputStore(
this.metadataHandler.getOutputStore(),
actionExecutionContext.getOutputSymlinks(),
action);
}
/** A closure to continue an asynchronously running action. */
private class ActionContinuationStep extends ActionStep {
private final ActionContinuationOrResult actionContinuationOrResult;
ActionContinuationStep(ActionContinuationOrResult actionContinuationOrResult) {
Preconditions.checkArgument(!actionContinuationOrResult.isDone());
this.actionContinuationOrResult = actionContinuationOrResult;
}
@Override
public ActionStepOrResult run(Environment env)
throws LostInputsActionExecutionException, InterruptedException {
ListenableFuture<?> future = actionContinuationOrResult.getFuture();
if (future != null && !future.isDone()) {
env.dependOnFuture(future);
return this;
}
return continueAction(actionExecutionContext.getEventHandler(), actionContinuationOrResult);
}
}
/**
* A closure to post-process the executed action, doing work like updating cached state with any
* newly discovered inputs, and writing the result to the action cache.
*/
private class ActionPostprocessingStep extends ActionStep {
private final ActionExecutionValue value;
ActionPostprocessingStep(ActionExecutionValue value) {
this.value = value;
}
@Override
public ActionStepOrResult run(Environment env) {
try (SilentCloseable c = profiler.profile(ProfilerTask.INFO, "postprocessing.run")) {
postprocessing.run(env, action, metadataHandler, actionExecutionContext.getClientEnv());
if (env.valuesMissing()) {
return this;
}
} catch (InterruptedException e) {
return ActionStepOrResult.of(e);
} catch (ActionExecutionException e) {
return ActionStepOrResult.of(e);
}
// Once the action has been written to the action cache, we can free its discovered inputs.
if (freeDiscoveredInputsAfterExecution && action.discoversInputs()) {
action.resetDiscoveredInputs();
}
return ActionStepOrResult.of(value);
}
}
}
/**
* Create output directories for an ActionFS. The action-local filesystem starts empty, so we
* expect the output directory creation to always succeed. There can be no interference from state
* left behind by prior builds or other actions intra-build.
*/
private void createActionFsOutputDirectories(
Action action, ArtifactPathResolver artifactPathResolver) throws ActionExecutionException {
try {
outputDirectoryHelper.createActionFsOutputDirectories(
action.getOutputs(), artifactPathResolver);
} catch (CreateOutputDirectoryException e) {
throw toActionExecutionException(
String.format("failed to create output directory '%s'", e.directoryPath),
e,
action,
null,
Code.ACTION_FS_OUTPUT_DIRECTORY_CREATION_FAILURE);
}
}
private void createOutputDirectories(Action action) throws ActionExecutionException {
try {
outputDirectoryHelper.createOutputDirectories(action.getOutputs());
} catch (CreateOutputDirectoryException e) {
throw toActionExecutionException(
String.format(
"failed to create output directory '%s': %s", e.directoryPath, e.getMessage()),
e,
action,
/*actionOutput=*/ null,
Code.ACTION_OUTPUT_DIRECTORY_CREATION_FAILURE);
}
}
/**
* Returns a progress message like:
*
* <p>[2608/6445] Compiling foo/bar.cc [host]
*/
private String prependExecPhaseStats(String message) {
if (progressSupplier == null) {
return "";
}
return progressSupplier.getProgressString() + " " + message;
}
private static void setupActionFsFileOutErr(FileOutErr fileOutErr, Action action)
throws ActionExecutionException {
try {
fileOutErr.getOutputPath().getParentDirectory().createDirectoryAndParents();
fileOutErr.getErrorPath().getParentDirectory().createDirectoryAndParents();
} catch (IOException e) {
String message =
String.format(
"failed to create output directory for output streams'%s': %s",
fileOutErr.getErrorPath(), e.getMessage());
DetailedExitCode code =
createDetailedExitCode(message, Code.ACTION_FS_OUT_ERR_DIRECTORY_CREATION_FAILURE);
throw new ActionExecutionException(message, e, action, false, code);
}
}
/** Must not be called with a {@link LostInputsActionExecutionException}. */
ActionExecutionException processAndGetExceptionToThrow(
ExtendedEventHandler eventHandler,
Path primaryOutputPath,
Action action,
ActionExecutionException e,
FileOutErr outErrBuffer,
ErrorTiming errorTiming) {
Preconditions.checkArgument(
!(e instanceof LostInputsActionExecutionException),
"unexpected LostInputs exception: %s",
e);
reportActionExecution(
eventHandler,
primaryOutputPath,
/*primaryOutputMetadata=*/ null,
action,
null,
e,
outErrBuffer,
errorTiming);
// Return the exception to rethrow. This can have two effects:
// If we're still building, the exception will get retrieved by the completor and rethrown.
// If we're aborting, the exception will never be retrieved from the completor, since the
// completor is waiting for all outstanding jobs to finish. After they have finished, it will
// only rethrow the exception that initially caused it to abort and not check the exit status of
// any actions that had finished in the meantime.
// If we already printed the error for the exception we mark it as already reported
// so that we do not print it again in upper levels.
// Note that we need to report it here since we want immediate feedback of the errors
// and in some cases the upper-level printing mechanism only prints one of the errors.
return printError(e.getMessage(), e.getAction(), outErrBuffer)
? new AlreadyReportedActionExecutionException(e)
: e;
}
/**
* Enriches the exception so it can be confirmed as the primary action in a shared action set and
* so that, if rewinding fails, an ActionExecutedEvent can be published, and the error reported.
*/
private static void enrichLostInputsException(
Path primaryOutputPath,
ActionLookupData actionLookupData,
FileOutErr outErrBuffer,
LostInputsActionExecutionException lostInputsException) {
lostInputsException.setPrimaryAction(actionLookupData);
lostInputsException.setPrimaryOutputPath(primaryOutputPath);
lostInputsException.setFileOutErr(outErrBuffer);
}
private static void reportMissingOutputFile(
Action action, Artifact output, Reporter reporter, boolean isSymlink, IOException exception) {
boolean genrule = action.getMnemonic().equals("Genrule");
String prefix = (genrule ? "declared output '" : "output '") + output.prettyPrint() + "' ";
logger.atWarning().log(
"Error creating %s%s%s: %s",
isSymlink ? "symlink " : "", prefix, genrule ? " by genrule" : "", exception.getMessage());
if (isSymlink) {
String msg = prefix + "is a dangling symbolic link";
reporter.handle(Event.error(action.getOwner().getLocation(), msg));
} else {
String suffix =
genrule
? " by genrule. This is probably because the genrule actually didn't create this"
+ " output, or because the output was a directory and the genrule was run"
+ " remotely (note that only the contents of declared file outputs are copied"
+ " from genrules run remotely)"
: "";
reporter.handle(
Event.error(action.getOwner().getLocation(), prefix + "was not created" + suffix));
}
}
private static void reportOutputTreeArtifactErrors(
Action action, Artifact output, Reporter reporter, IOException e) {
String errorMessage;
if (e instanceof FileNotFoundException) {
errorMessage = String.format("TreeArtifact %s was not created", output.prettyPrint());
} else {
errorMessage =
String.format(
"Error while validating output TreeArtifact %s : %s", output, e.getMessage());
}
reporter.handle(Event.error(action.getOwner().getLocation(), errorMessage));
}
/**
* Validates that all action input contents were not lost if they were read, and if an action file
* system was used. Throws a {@link LostInputsActionExecutionException} describing the lost inputs
* if any were.
*/
private static void checkActionFileSystemForLostInputs(
@Nullable FileSystem actionFileSystem, Action action, OutputService outputService)
throws LostInputsActionExecutionException {
if (actionFileSystem != null) {
outputService.checkActionFileSystemForLostInputs(actionFileSystem, action);
}
}
private static LostInputsCheck lostInputsCheck(
@Nullable FileSystem actionFileSystem, Action action, OutputService outputService) {
return actionFileSystem == null
? LostInputsCheck.NONE
: () -> outputService.checkActionFileSystemForLostInputs(actionFileSystem, action);
}
private static void flushActionFileSystem(
@Nullable FileSystem actionFileSystem, @Nullable OutputService outputService)
throws IOException {
if (outputService != null && actionFileSystem != null) {
outputService.flushActionFileSystem(actionFileSystem);
}
}
/**
* Validates that all action outputs were created or intentionally omitted. This can result in
* chmod calls on the output files; see {@link ActionMetadataHandler}.
*
* @return false if some outputs are missing, true - otherwise.
*/
private boolean checkOutputs(
Action action,
MetadataHandler metadataHandler,
@Nullable ImmutableList<FilesetOutputSymlink> filesetOutputSymlinksForMetrics,
boolean isActionCacheHitForMetrics) {
boolean success = true;
for (Artifact output : action.getOutputs()) {
// getMetadata has the side effect of adding the artifact to the cache if it's not there
// already (e.g., due to a previous call to MetadataHandler.injectDigest), therefore we only
// call it if we know the artifact is not omitted.
if (!metadataHandler.artifactOmitted(output)) {
try {
FileArtifactValue metadata = metadataHandler.getMetadata(output);
addOutputToMetrics(
output,
metadata,
metadataHandler,
filesetOutputSymlinksForMetrics,
isActionCacheHitForMetrics,
action);
} catch (IOException e) {
success = false;
if (output.isTreeArtifact()) {
reportOutputTreeArtifactErrors(action, output, reporter, e);
} else if (output.isSymlink() && e instanceof NotASymlinkException) {
reporter.handle(
Event.error(
action.getOwner().getLocation(),
String.format("declared output '%s' is not a symlink", output.prettyPrint())));
} else {
// Are all other exceptions caught due to missing files?
reportMissingOutputFile(action, output, reporter, output.getPath().isSymbolicLink(), e);
}
}
}
}
return success;
}
private void addOutputToMetrics(
Artifact output,
FileArtifactValue metadata,
MetadataHandler metadataHandler,
@Nullable ImmutableList<FilesetOutputSymlink> filesetOutputSymlinks,
boolean isActionCacheHit,
Action actionForDebugging)
throws IOException {
if (metadata == null) {
BugReport.sendBugReport(
new IllegalStateException(
String.format(
"Metadata for %s not present in %s (for %s)",
output, metadataHandler, actionForDebugging)));
return;
}
if (output.isFileset() && filesetOutputSymlinks != null) {
outputArtifactsSeen.accumulate(filesetOutputSymlinks);
} else if (!output.isTreeArtifact()) {
outputArtifactsSeen.accumulate(metadata);
if (isActionCacheHit) {
outputArtifactsFromActionCache.accumulate(metadata);
}
} else {
TreeArtifactValue treeArtifactValue;
try {
treeArtifactValue = metadataHandler.getTreeArtifactValue((SpecialArtifact) output);
} catch (IOException e) {
BugReport.sendBugReport(
new IllegalStateException(
String.format(
"Unexpected IO exception after metadata %s was retrieved for %s (action %s)",
metadata, output, actionForDebugging)));
throw e;
}
outputArtifactsSeen.accumulate(treeArtifactValue);
if (isActionCacheHit) {
outputArtifactsFromActionCache.accumulate(treeArtifactValue);
}
}
}
/**
* Convenience function for creating an ActionExecutionException reporting that the action failed
* due to the exception cause, if there is an additional explanatory message that clarifies the
* message of the exception. Combines the user-provided message and the exception's message and
* reports the combination as error.
*
* @param message A small text that explains why the action failed
* @param cause The exception that caused the action to fail
* @param action The action that failed
* @param actionOutput The output of the failed Action. May be null, if there is no output to
* display
* @param detailedCode The fine-grained failure code describing the failure
*/
private ActionExecutionException toActionExecutionException(
String message,
Throwable cause,
Action action,
FileOutErr actionOutput,
FailureDetails.Execution.Code detailedCode) {
DetailedExitCode code = createDetailedExitCode(message, detailedCode);
ActionExecutionException ex;
if (cause == null) {
ex = new ActionExecutionException(message, action, false, code);
} else {
ex = new ActionExecutionException(message, cause, action, false, code);
}
printError(ex.getMessage(), action, actionOutput);
return ex;
}
private static DetailedExitCode createDetailedExitCode(String message, Code detailedCode) {
return DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setExecution(Execution.newBuilder().setCode(detailedCode))
.build());
}
/**
* Prints the given error {@code message} ascribed to {@code action}. May be called multiple times
* for the same action if there are multiple errors: will print all of them.
*/
void printError(String message, ActionAnalysisMetadata action) {
printError(message, action, null);
}
/**
* For the action 'action' that failed due to 'message' with the output 'actionOutput', notify the
* user about the error. To notify the user, the method displays the output of the action and
* reports an error via the reporter.
*
* @param message The reason why the action failed
* @param action The action that failed, must not be null.
* @param actionOutput The output of the failed Action. May be null, if there is no output to
* display
* @return whether error was printed
*/
private boolean printError(
String message, ActionAnalysisMetadata action, @Nullable FileOutErr actionOutput) {
message = action.describe() + " failed: " + message;
return dumpRecordedOutErr(
reporter, Event.error(action.getOwner().getLocation(), message), actionOutput);
}
/**
* Dumps the output from the action.
*
* @param action The action whose output is being dumped
* @param outErrBuffer The OutErr that recorded the actions output
*/
private void dumpRecordedOutErr(
EventHandler eventHandler, Action action, FileOutErr outErrBuffer) {
Event event =
replayActionOutErr
// Info events are not cached in Skyframe, so we make this a warning.
? Event.warn("From " + action.describe() + ":")
: Event.info("From " + action.describe() + ":");
dumpRecordedOutErr(eventHandler, event, outErrBuffer);
}
/**
* Dumps output from the action along with {@code prefixEvent} if the build is not aborting.
*
* @param prefixEvent An event to post before dumping the output
* @param outErrBuffer The OutErr that recorded the actions output
* @return whether output was displayed (false if aborting)
*/
private boolean dumpRecordedOutErr(
EventHandler eventHandler, Event prefixEvent, FileOutErr outErrBuffer) {
// For some actions (e.g., many local actions) the pollInterruptedStatus()
// won't notice that we had an interrupted job. It will continue.
// For that reason we must take care to NOT report errors if we're
// in the 'aborting' mode: Any cancelled action would show up here.
if (isBuilderAborting()) {
return false;
}
if (outErrBuffer != null && outErrBuffer.hasRecordedOutput()) {
// Bind the output to the prefix event.
eventHandler.handle(prefixEvent.withProcessOutput(new ActionOutputEventData(outErrBuffer)));
} else {
eventHandler.handle(prefixEvent);
}
return true;
}
private static void reportActionExecution(
ExtendedEventHandler eventHandler,
Path primaryOutputPath,
@Nullable FileArtifactValue primaryOutputMetadata,
Action action,
@Nullable ActionResult actionResult,
ActionExecutionException exception,
FileOutErr outErr,
ErrorTiming errorTiming) {
Path stdout = null;
Path stderr = null;
ImmutableList<MetadataLog> logs = ImmutableList.of();
if (outErr.hasRecordedStdout()) {
stdout = outErr.getOutputPath();
}
if (outErr.hasRecordedStderr()) {
stderr = outErr.getErrorPath();
}
if (actionResult != null) {
logs =
actionResult.spawnResults().stream()
.filter(spawnResult -> spawnResult.getActionMetadataLog().isPresent())
.map(spawnResult -> spawnResult.getActionMetadataLog().get())
.collect(ImmutableList.toImmutableList());
}
eventHandler.post(
new ActionExecutedEvent(
action.getPrimaryOutput().getExecPath(),
action,
exception,
primaryOutputPath,
action.getPrimaryOutput(),
primaryOutputMetadata,
stdout,
stderr,
logs,
errorTiming));
}
/** An object supplying data for action execution progress reporting. */
public interface ProgressSupplier {
/** Returns the progress string to prefix action execution messages with. */
String getProgressString();
}
/** An object that can be notified about action completion. */
public interface ActionCompletedReceiver {
/** Receives a completed action. */
void actionCompleted(ActionLookupData actionLookupData);
/** Notes that an action has started, giving the key. */
void noteActionEvaluationStarted(ActionLookupData actionLookupData, Action action);
}
public void setActionExecutionProgressReportingObjects(
@Nullable ProgressSupplier progressSupplier,
@Nullable ActionCompletedReceiver completionReceiver) {
this.progressSupplier = progressSupplier;
this.completionReceiver = completionReceiver;
}
private static final class DelegatingPairFileCache implements MetadataProvider {
private final MetadataProvider perActionCache;
private final MetadataProvider perBuildFileCache;
private DelegatingPairFileCache(
MetadataProvider mainCache, MetadataProvider perBuildFileCache) {
this.perActionCache = mainCache;
this.perBuildFileCache = perBuildFileCache;
}
@Override
public FileArtifactValue getMetadata(ActionInput input) throws IOException {
FileArtifactValue metadata = perActionCache.getMetadata(input);
return (metadata != null) && (metadata != FileArtifactValue.MISSING_FILE_MARKER)
? metadata
: perBuildFileCache.getMetadata(input);
}
@Override
public ActionInput getInput(String execPath) {
ActionInput input = perActionCache.getInput(execPath);
return input != null ? input : perBuildFileCache.getInput(execPath);
}
}
/** Adapts a {@link FileOutErr} to an {@link Event.ProcessOutput}. */
private static class ActionOutputEventData implements Event.ProcessOutput {
private final FileOutErr fileOutErr;
private ActionOutputEventData(FileOutErr fileOutErr) {
this.fileOutErr = fileOutErr;
}
@Override
public String getStdOutPath() {
return fileOutErr.getOutputPathFragment().getPathString();
}
@Override
public long getStdOutSize() throws IOException {
return fileOutErr.outSize();
}
@Override
public byte[] getStdOut() {
return fileOutErr.outAsBytes();
}
@Override
public String getStdErrPath() {
return fileOutErr.getErrorPathFragment().getPathString();
}
@Override
public long getStdErrSize() throws IOException {
return fileOutErr.errSize();
}
@Override
public byte[] getStdErr() {
return fileOutErr.errAsBytes();
}
}
}