// 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.Function; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.devtools.build.lib.Constants; import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.ActionCacheChecker.Token; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionExecutionException; import com.google.devtools.build.lib.actions.ActionInputHelper; import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.ArtifactFile; import com.google.devtools.build.lib.actions.MissingInputFileException; import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit; import com.google.devtools.build.lib.actions.PackageRootResolutionException; import com.google.devtools.build.lib.actions.PackageRootResolver; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.packages.NoSuchPackageException; import com.google.devtools.build.lib.util.LoggingUtil; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunctionException; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import com.google.devtools.build.skyframe.ValueOrException2; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import javax.annotation.Nullable; /** * A {@link SkyFunction} that creates {@link ActionExecutionValue}s. There are four points where * this function can abort due to missing values in the graph: *
    *
  1. For actions that discover inputs, if missing metadata needed to resolve an artifact from a * string input in the action cache.
  2. *
  3. If missing metadata for artifacts in inputs (including the artifacts above).
  4. *
  5. For actions that discover inputs, if missing metadata for inputs discovered prior to * execution.
  6. *
  7. For actions that discover inputs, but do so during execution, if missing metadata for * inputs discovered during execution.
  8. *
*/ public class ActionExecutionFunction implements SkyFunction, CompletionReceiver { private final SkyframeActionExecutor skyframeActionExecutor; private final TimestampGranularityMonitor tsgm; private ConcurrentMap stateMap; public ActionExecutionFunction(SkyframeActionExecutor skyframeActionExecutor, TimestampGranularityMonitor tsgm) { this.skyframeActionExecutor = skyframeActionExecutor; this.tsgm = tsgm; stateMap = Maps.newConcurrentMap(); } @Override public SkyValue compute(SkyKey skyKey, Environment env) throws ActionExecutionFunctionException, InterruptedException { Action action = (Action) skyKey.argument(); // TODO(bazel-team): Non-volatile NotifyOnActionCacheHit actions perform worse in Skyframe than // legacy when they are not at the top of the action graph. In legacy, they are stored // separately, so notifying non-dirty actions is cheap. In Skyframe, they depend on the // BUILD_ID, forcing invalidation of upward transitive closure on each build. if ((action.isVolatile() && !(action instanceof SkyframeAwareAction)) || action instanceof NotifyOnActionCacheHit) { // Volatile build actions may need to execute even if none of their known inputs have changed. // Depending on the buildID ensure that these actions have a chance to execute. PrecomputedValue.BUILD_ID.get(env); } // For restarts of this ActionExecutionFunction we use a ContinuationState variable, below, to // avoid redoing work. However, if two actions are shared and the first one executes, when the // second one goes to execute, we should detect that and short-circuit, even without taking // ContinuationState into account. boolean sharedActionAlreadyRan = skyframeActionExecutor.probeActionExecution(action); ContinuationState state; if (action.discoversInputs()) { state = getState(action); } else { // Because this is a new state, all conditionals below about whether state has already done // something will return false, and so we will execute all necessary steps. state = new ContinuationState(); } if (!state.hasCollectedInputs()) { state.allInputs = collectInputs(action, env); if (state.allInputs == null) { // Missing deps. return null; } } else if (state.allInputs.keysRequested != null) { // Preserve the invariant that we ask for the same deps each build. env.getValues(state.allInputs.keysRequested); Preconditions.checkState(!env.valuesMissing(), "%s %s", action, state); } Pair, Map>> checkedInputs = null; try { // Declare deps on known inputs to action. We do this unconditionally to maintain our // invariant of asking for the same deps each build. Map> inputDeps = env.getValuesOrThrow(toKeys(state.allInputs.getAllInputs(), action.discoversInputs() ? action.getMandatoryInputs() : null), MissingInputFileException.class, ActionExecutionException.class); if (!sharedActionAlreadyRan && !state.hasArtifactData()) { // Do we actually need to find our metadata? checkedInputs = checkInputs(env, action, inputDeps); } } catch (ActionExecutionException e) { // Remove action from state map in case it's there (won't be unless it discovers inputs). stateMap.remove(action); throw new ActionExecutionFunctionException(e); } if (env.valuesMissing()) { // There was missing artifact metadata in the graph. Wait for it to be present. // We must check this and return here before attempting to establish any Skyframe dependencies // of the action; see establishSkyframeDependencies why. return null; } try { establishSkyframeDependencies(env, action); } catch (ActionExecutionException e) { throw new ActionExecutionFunctionException(e); } if (env.valuesMissing()) { return null; } if (checkedInputs != null) { Preconditions.checkState(!state.hasArtifactData(), "%s %s", state, action); state.inputArtifactData = checkedInputs.first; state.expandedArtifacts = checkedInputs.second; } ActionExecutionValue result; try { result = checkCacheAndExecuteIfNeeded(action, state, env); } catch (ActionExecutionException e) { // Remove action from state map in case it's there (won't be unless it discovers inputs). stateMap.remove(action); // In this case we do not report the error to the action reporter because we have already // done it in SkyframeExecutor.reportErrorIfNotAbortingMode() method. That method // prints the error in the top-level reporter and also dumps the recorded StdErr for the // action. Label can be null in the case of, e.g., the SystemActionOwner (for build-info.txt). throw new ActionExecutionFunctionException(new AlreadyReportedActionExecutionException(e)); } if (env.valuesMissing()) { Preconditions.checkState(stateMap.containsKey(action), action); return null; } // Remove action from state map in case it's there (won't be unless it discovers inputs). stateMap.remove(action); return result; } /** * An action's inputs needed for execution. May not just be the result of Action#getInputs(). If * the action cache's view of this action contains additional inputs, it will request metadata for * them, so we consider those inputs as dependencies of this action as well. Returns null if some * dependencies were missing and this ActionExecutionFunction needs to restart. * @throws ActionExecutionFunctionException */ @Nullable private AllInputs collectInputs(Action action, Environment env) throws ActionExecutionFunctionException { Iterable allKnownInputs = Iterables.concat( action.getInputs(), action.getRunfilesSupplier().getArtifacts()); if (action.inputsKnown()) { return new AllInputs(allKnownInputs); } Preconditions.checkState(action.discoversInputs(), action); PackageRootResolverWithEnvironment resolver = new PackageRootResolverWithEnvironment(env); Iterable actionCacheInputs; try { actionCacheInputs = skyframeActionExecutor.getActionCachedInputs(action, resolver); } catch (PackageRootResolutionException rre) { throw new ActionExecutionFunctionException( new ActionExecutionException("Failed to get cached inputs", rre, action, true)); } if (actionCacheInputs == null) { Preconditions.checkState(env.valuesMissing(), action); return null; } return new AllInputs(allKnownInputs, actionCacheInputs, resolver.keysRequested); } private static class AllInputs { final Iterable defaultInputs; @Nullable final Iterable actionCacheInputs; @Nullable final List keysRequested; AllInputs(Iterable defaultInputs) { this.defaultInputs = Preconditions.checkNotNull(defaultInputs); this.actionCacheInputs = null; this.keysRequested = null; } AllInputs(Iterable defaultInputs, Iterable actionCacheInputs, List keysRequested) { this.defaultInputs = Preconditions.checkNotNull(defaultInputs); this.actionCacheInputs = Preconditions.checkNotNull(actionCacheInputs); this.keysRequested = keysRequested; } Iterable getAllInputs() { return actionCacheInputs == null ? defaultInputs : Iterables.concat(defaultInputs, actionCacheInputs); } } /** * Skyframe implementation of {@link PackageRootResolver}. Should be used only from SkyFunctions, * because it uses SkyFunction.Environment for evaluation of ContainingPackageLookupValue. */ private static class PackageRootResolverWithEnvironment implements PackageRootResolver { final List keysRequested = new ArrayList<>(); private final Environment env; public PackageRootResolverWithEnvironment(Environment env) { this.env = env; } @Override public Map findPackageRootsForFiles(Iterable execPaths) throws PackageRootResolutionException { Preconditions.checkState(keysRequested.isEmpty(), "resolver should only be called once: %s %s", keysRequested, execPaths); // Create SkyKeys list based on execPaths. Map depKeys = new HashMap<>(); for (PathFragment path : execPaths) { PathFragment parent = Preconditions.checkNotNull( path.getParentDirectory(), "Must pass in files, not root directory"); Preconditions.checkArgument(!parent.isAbsolute(), path); SkyKey depKey = ContainingPackageLookupValue.key(PackageIdentifier.createInMainRepo(parent)); depKeys.put(path, depKey); keysRequested.add(depKey); } Map> values = env.getValuesOrThrow(depKeys.values(), NoSuchPackageException.class, InconsistentFilesystemException.class); // Check values even if some are missing so that we can throw an appropriate exception if // needed. Map result = new HashMap<>(); for (PathFragment path : execPaths) { ContainingPackageLookupValue value; try { value = (ContainingPackageLookupValue) values.get(depKeys.get(path)).get(); } catch (NoSuchPackageException | InconsistentFilesystemException e) { throw new PackageRootResolutionException("Could not determine containing package for " + path, e); } if (value == null) { Preconditions.checkState(env.valuesMissing(), path); continue; } if (value.hasContainingPackage()) { // We have found corresponding root for current execPath. result.put(path, Root.asSourceRoot(value.getContainingPackageRoot())); } else { // We haven't found corresponding root for current execPath. result.put(path, null); } } // If some values are missing, return null. return env.valuesMissing() ? null : result; } @Override @Nullable public Map findPackageRoots(Iterable execPaths) throws PackageRootResolutionException { // call sites for this implementation of PackageRootResolver shouldn't be passing in // directories. return findPackageRootsForFiles(execPaths); } } private ActionExecutionValue checkCacheAndExecuteIfNeeded( Action action, ContinuationState state, Environment env) throws ActionExecutionException, InterruptedException { // If this is a shared action and the other action is the one that executed, we must use that // other action's value, provided here, since it is populated with metadata for the outputs. if (!state.hasArtifactData()) { return skyframeActionExecutor.executeAction(action, null, -1, null); } // This may be recreated if we discover inputs. ActionMetadataHandler metadataHandler = new ActionMetadataHandler(state.inputArtifactData, action.getOutputs(), tsgm); long actionStartTime = System.nanoTime(); // We only need to check the action cache if we haven't done it on a previous run. if (!state.hasCheckedActionCache()) { state.token = skyframeActionExecutor.checkActionCache(action, metadataHandler, actionStartTime, state.allInputs.actionCacheInputs); } if (state.token == null) { // We got a hit from the action cache -- no need to execute. return new ActionExecutionValue( metadataHandler.getOutputArtifactFileData(), metadataHandler.getOutputTreeArtifactData(), metadataHandler.getAdditionalOutputData()); } // This may be recreated if we discover inputs. PerActionFileCache perActionFileCache = new PerActionFileCache(state.inputArtifactData); ActionExecutionContext actionExecutionContext = null; try { if (action.discoversInputs()) { if (!state.hasDiscoveredInputs()) { try { state.discoveredInputs = skyframeActionExecutor.discoverInputs(action, perActionFileCache, metadataHandler, env); } catch (MissingDepException e) { Preconditions.checkState(env.valuesMissing(), action); return null; } } // state.discoveredInputs can be null even after include scanning if action discovers them // during execution. if (state.discoveredInputs != null && !state.inputArtifactData.keySet().containsAll(state.discoveredInputs)) { Map inputArtifactData = addDiscoveredInputs(state.inputArtifactData, state.discoveredInputs, env); if (env.valuesMissing()) { return null; } state.inputArtifactData = inputArtifactData; perActionFileCache = new PerActionFileCache(state.inputArtifactData); metadataHandler = new ActionMetadataHandler(state.inputArtifactData, action.getOutputs(), tsgm); } } actionExecutionContext = skyframeActionExecutor.getContext(perActionFileCache, metadataHandler, state.expandedArtifacts); if (!state.hasExecutedAction()) { state.value = skyframeActionExecutor.executeAction(action, metadataHandler, actionStartTime, actionExecutionContext); } } finally { if (actionExecutionContext != null) { try { actionExecutionContext.getFileOutErr().close(); } catch (IOException e) { // Nothing we can do here. } } } if (action.discoversInputs()) { Map metadataFoundDuringActionExecution = declareAdditionalDependencies(env, action, state.inputArtifactData.keySet()); if (state.discoveredInputs == null) { // Include scanning didn't find anything beforehand -- these are the definitive discovered // inputs. state.discoveredInputs = metadataFoundDuringActionExecution.keySet(); if (env.valuesMissing()) { return null; } if (!metadataFoundDuringActionExecution.isEmpty()) { // We are in the interesting case of an action that discovered its inputs during // execution, and found some new ones, but the new ones were already present in the graph. // We must therefore cache the metadata for those new ones. Map inputArtifactData = new HashMap<>(); inputArtifactData.putAll(state.inputArtifactData); inputArtifactData.putAll(metadataFoundDuringActionExecution); state.inputArtifactData = inputArtifactData; metadataHandler = new ActionMetadataHandler(state.inputArtifactData, action.getOutputs(), tsgm); } } else if (!metadataFoundDuringActionExecution.isEmpty()) { // The action has run and discovered more inputs. This is a bug, probably the result of // the action dynamically executing locally instead of remotely, and a discrepancy between // our include scanning and the action's compiler. Fail the build so that the user notices, // and also report the issue. String errorMessage = action.prettyPrint() + " discovered unexpected inputs. This indicates a mismatch between " + Constants.PRODUCT_NAME + " and the action's compiler. Please report this issue. The "; if (metadataFoundDuringActionExecution.size() > 10) { errorMessage += "first ten "; } errorMessage += "additional inputs found were: "; int artifactPrinted = 0; for (Artifact extraArtifact : metadataFoundDuringActionExecution.keySet()) { if (artifactPrinted >= 10) { break; } if (artifactPrinted > 0) { errorMessage += ", "; } artifactPrinted++; errorMessage += extraArtifact.prettyPrint(); } ActionExecutionException exception = new ActionExecutionException(errorMessage, action, /*catastrophe=*/ false); LoggingUtil.logToRemote(Level.SEVERE, errorMessage, exception); throw skyframeActionExecutor.processAndThrow( exception, action, actionExecutionContext.getFileOutErr()); } } Preconditions.checkState(!env.valuesMissing(), action); skyframeActionExecutor.afterExecution(action, metadataHandler, state.token); return state.value; } private static Map addDiscoveredInputs( Map originalInputData, Collection discoveredInputs, Environment env) { // We assume nobody would want to discover a TreeArtifact, since TreeArtifacts are precisely // for undiscoverable contents. Map result = new HashMap<>(originalInputData); Set keys = new HashSet<>(); for (Artifact artifact : discoveredInputs) { if (!result.containsKey(artifact)) { // Note that if the artifact is derived, the mandatory flag is ignored. keys.add(ArtifactValue.key(artifact, /*mandatory=*/false)); } } // We do not do a getValuesOrThrow() call for the following reasons: // 1. No exceptions can be thrown for non-mandatory inputs; // 2. Any derived inputs must be in the transitive closure of this action's inputs. Therefore, // if there was an error building one of them, then that exception would have percolated up to // this action already, through one of its declared inputs, and we would not have reached input // discovery. // Therefore there is no need to catch and rethrow exceptions as there is with #checkInputs. Map data = env.getValues(keys); if (env.valuesMissing()) { return null; } result.putAll(transformArtifactMetadata(data)); return result; } private void establishSkyframeDependencies(Environment env, Action action) throws ActionExecutionException { // Before we may safely establish Skyframe dependencies, we must build all action inputs by // requesting their ArtifactValues. // This is very important to do, because the establishSkyframeDependencies method may request // FileValues for input files of this action (directly requesting them, or requesting some other // SkyValue whose builder requests FileValues), which may not yet exist if their generating // actions have not yet run. // See SkyframeAwareActionTest.testRaceConditionBetweenInputAcquisitionAndSkyframeDeps Preconditions.checkState(!env.valuesMissing(), action); if (action instanceof SkyframeAwareAction) { // Skyframe-aware actions should be executed unconditionally, i.e. bypass action cache // checking. See documentation of SkyframeAwareAction. Preconditions.checkState(action.executeUnconditionally(), action); try { ((SkyframeAwareAction) action).establishSkyframeDependencies(env); } catch (SkyframeAwareAction.ExceptionBase e) { throw new ActionExecutionException(e, action, false); } } } private static Iterable toKeys(Iterable inputs, Iterable mandatoryInputs) { if (mandatoryInputs == null) { // This is a non inputs-discovering action, so no need to distinguish mandatory from regular // inputs. return Iterables.transform(inputs, new Function() { @Override public SkyKey apply(Artifact artifact) { return ArtifactValue.key(artifact, true); } }); } else { Collection discoveredArtifacts = new HashSet<>(); Set mandatory = Sets.newHashSet(mandatoryInputs); for (Artifact artifact : inputs) { discoveredArtifacts.add(ArtifactValue.key(artifact, mandatory.contains(artifact))); } return discoveredArtifacts; } } /** * Declare dependency on all known inputs of action. Throws exception if any are known to be * missing. Some inputs may not yet be in the graph, in which case the builder should abort. */ private Pair, Map>> checkInputs(Environment env, Action action, Map> inputDeps) throws ActionExecutionException { int missingCount = 0; int actionFailures = 0; boolean catastrophe = false; // Only populate input data if we have the input values, otherwise they'll just go unused. // We still want to loop through the inputs to collect missing deps errors. During the // evaluator "error bubbling", we may get one last chance at reporting errors even though // some deps are still missing. boolean populateInputData = !env.valuesMissing(); NestedSetBuilder