// Copyright 2014 Google Inc. 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.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; 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.AlreadyReportedActionExecutionException; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.MissingInputFileException; import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit; import com.google.devtools.build.lib.actions.PackageRootResolver; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.packages.PackageIdentifier; import com.google.devtools.build.lib.syntax.Label; import com.google.devtools.build.lib.util.Pair; 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 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 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. return null; } if (checkedInputs != null) { Preconditions.checkState(!state.hasArtifactData(), "%s %s", state, action); state.inputArtifactData = checkedInputs.first; state.expandedMiddlemen = 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. */ @Nullable private AllInputs collectInputs(Action action, Environment env) { if (action.inputsKnown()) { return new AllInputs(action.getInputs()); } Preconditions.checkState(action.discoversInputs(), action); PackageRootResolverWithEnvironment resolver = new PackageRootResolverWithEnvironment(env); Iterable actionCacheInputs = skyframeActionExecutor.getActionCachedInputs(action, resolver); if (actionCacheInputs == null) { Preconditions.checkState(env.valuesMissing(), action); return null; } return new AllInputs(action.getInputs(), 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 findPackageRoots(Iterable execPaths) { Preconditions.checkState(keysRequested.isEmpty(), "resolver should only be called once: %s %s", keysRequested, execPaths); Map depKeys = new HashMap<>(); // Create SkyKeys list based on execPaths. for (PathFragment path : execPaths) { SkyKey depKey = ContainingPackageLookupValue.key(PackageIdentifier.createInDefaultRepo(path)); depKeys.put(path, depKey); keysRequested.add(depKey); } Map values = env.getValues(depKeys.values()); if (env.valuesMissing()) { // Some values are not computed yet. return null; } Map result = new HashMap<>(); for (PathFragment path : execPaths) { // TODO(bazel-team): Add check for errors here, when loading phase will be removed. // For now all possible errors that ContainingPackageLookupFunction can generate // are caught in previous phases. ContainingPackageLookupValue value = (ContainingPackageLookupValue) values.get(depKeys.get(path)); 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); } } return result; } } 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. FileAndMetadataCache fileAndMetadataCache = new FileAndMetadataCache( state.inputArtifactData, state.expandedMiddlemen, skyframeActionExecutor.getExecRoot(), 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, fileAndMetadataCache, actionStartTime, state.allInputs.actionCacheInputs); } if (state.token == null) { // We got a hit from the action cache -- no need to execute. return new ActionExecutionValue( fileAndMetadataCache.getOutputData(), fileAndMetadataCache.getAdditionalOutputData()); } // This may be recreated if we discover inputs. ActionExecutionContext actionExecutionContext = skyframeActionExecutor.constructActionExecutionContext(fileAndMetadataCache); boolean inputsDiscoveredDuringActionExecution = false; Map metadataFoundDuringActionExecution = null; try { if (action.discoversInputs()) { if (!state.hasDiscoveredInputs()) { state.discoveredInputs = skyframeActionExecutor.discoverInputs(action, actionExecutionContext); if (state.discoveredInputs == null) { // Action had nothing to tell us about discovered inputs before execution. We'll have to // add them afterwards. inputsDiscoveredDuringActionExecution = true; } } // 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; fileAndMetadataCache = new FileAndMetadataCache( state.inputArtifactData, state.expandedMiddlemen, skyframeActionExecutor.getExecRoot(), action.getOutputs(), tsgm ); actionExecutionContext = skyframeActionExecutor.constructActionExecutionContext(fileAndMetadataCache); } } if (!state.hasExecutedAction()) { state.value = skyframeActionExecutor.executeAction(action, fileAndMetadataCache, actionStartTime, actionExecutionContext); } } finally { try { actionExecutionContext.getFileOutErr().close(); } catch (IOException e) { // Nothing we can do here. } if (inputsDiscoveredDuringActionExecution) { metadataFoundDuringActionExecution = declareAdditionalDependencies(env, action, state.inputArtifactData.keySet()); state.discoveredInputs = metadataFoundDuringActionExecution.keySet(); } } if (env.valuesMissing()) { return null; } if (inputsDiscoveredDuringActionExecution && !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; fileAndMetadataCache = new FileAndMetadataCache( state.inputArtifactData, state.expandedMiddlemen, skyframeActionExecutor.getExecRoot(), action.getOutputs(), tsgm ); } skyframeActionExecutor.afterExecution(action, fileAndMetadataCache, state.token); return state.value; } private static Map addDiscoveredInputs( Map originalInputData, Collection discoveredInputs, Environment env) { 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 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