// Copyright 2016 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.skyframe; import static com.google.devtools.build.skyframe.AbstractParallelEvaluator.isDoneForBuild; import static com.google.devtools.build.skyframe.ParallelEvaluator.maybeGetValueFromError; import com.google.common.base.Preconditions; 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.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.ExtendedEventHandler; import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; import com.google.devtools.build.lib.events.StoredEventHandler; import com.google.devtools.build.lib.util.GroupedList; import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper; import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState; import com.google.devtools.build.skyframe.NodeEntry.DependencyState; import com.google.devtools.build.skyframe.ParallelEvaluatorContext.EnqueueParentBehavior; import com.google.devtools.build.skyframe.QueryableGraph.Reason; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import javax.annotation.Nullable; /** A {@link SkyFunction.Environment} implementation for {@link ParallelEvaluator}. */ class SkyFunctionEnvironment extends AbstractSkyFunctionEnvironment { private static final SkyValue NULL_MARKER = new SkyValue() {}; private static final boolean PREFETCH_OLD_DEPS = Boolean.parseBoolean( System.getProperty("skyframe.ParallelEvaluator.PrefetchOldDeps", "true")); private boolean building = true; private SkyKey depErrorKey = null; private final SkyKey skyKey; /** * The deps requested during the previous build of this node. Used for two reasons: (1) They are * fetched eagerly before the node is built, to potentially prime the graph and speed up requests * for them during evaluation. (2) When the node finishes building, any deps from the previous * build that are not deps from this build must have this node removed from them as a reverse dep. * Thus, it is important that all nodes in this set have the property that they have this node as * a reverse dep from the last build, but that this node has not added them as a reverse dep on * this build. That set is normally {@link NodeEntry#getAllRemainingDirtyDirectDeps()}, but in * certain corner cases, like cycles, further filtering may be needed. */ private final Set oldDeps; private SkyValue value = null; private ErrorInfo errorInfo = null; private final Map bubbleErrorInfo; /** The values previously declared as dependencies. */ private final Map directDeps; /** * The grouped list of values requested during this build as dependencies. On a subsequent build, * if this value is dirty, all deps in the same dependency group can be checked in parallel for * changes. In other words, if dep1 and dep2 are in the same group, then dep1 will be checked in * parallel with dep2. See {@link #getValues} for more. */ private final GroupedListHelper newlyRequestedDeps = new GroupedListHelper<>(); /** The set of errors encountered while fetching children. */ private final Set childErrorInfos = new LinkedHashSet<>(); private final StoredEventHandler eventHandler = new StoredEventHandler() { @Override @SuppressWarnings("UnsynchronizedOverridesSynchronized") // only delegates to thread-safe. public void handle(Event e) { checkActive(); if (evaluatorContext.getStoredEventFilter().apply(e)) { super.handle(e); } else { evaluatorContext.getReporter().handle(e); } } @Override @SuppressWarnings("UnsynchronizedOverridesSynchronized") // only delegates to thread-safe. public void post(ExtendedEventHandler.Postable e) { checkActive(); if (e instanceof ExtendedEventHandler.ProgressLike) { evaluatorContext.getReporter().post(e); } else { super.post(e); } } }; private final ParallelEvaluatorContext evaluatorContext; SkyFunctionEnvironment( SkyKey skyKey, GroupedList directDeps, Set oldDeps, ParallelEvaluatorContext evaluatorContext) throws InterruptedException { this(skyKey, directDeps, null, oldDeps, evaluatorContext); } SkyFunctionEnvironment( SkyKey skyKey, GroupedList directDeps, @Nullable Map bubbleErrorInfo, Set oldDeps, ParallelEvaluatorContext evaluatorContext) throws InterruptedException { super(directDeps); this.skyKey = skyKey; this.oldDeps = oldDeps; this.evaluatorContext = evaluatorContext; this.directDeps = Collections.unmodifiableMap( batchPrefetch( skyKey, directDeps, oldDeps, /*assertDone=*/ bubbleErrorInfo == null, skyKey)); this.bubbleErrorInfo = bubbleErrorInfo; Preconditions.checkState( !this.directDeps.containsKey(ErrorTransienceValue.KEY), "%s cannot have a dep on ErrorTransienceValue during building", skyKey); } private Map batchPrefetch( SkyKey requestor, GroupedList depKeys, Set oldDeps, boolean assertDone, SkyKey keyForDebugging) throws InterruptedException { Set depKeysAsSet = null; if (PREFETCH_OLD_DEPS) { if (!oldDeps.isEmpty()) { // Create a set here so that filtering the old deps below is fast. Once we create this set, // we may as well use it for the call to evaluatorContext#getBatchValues since we've // precomputed the size. depKeysAsSet = depKeys.toSet(); evaluatorContext .getGraph() .getBatchAsync( requestor, Reason.PREFETCH, Iterables.filter(oldDeps, Predicates.not(Predicates.in(depKeysAsSet)))); } } Map batchMap = evaluatorContext.getBatchValues( requestor, Reason.PREFETCH, depKeysAsSet == null ? depKeys.getAllElementsAsIterable() : depKeysAsSet); if (batchMap.size() != depKeys.numElements()) { throw new IllegalStateException( "Missing keys for " + keyForDebugging + ": " + Sets.difference(depKeys.toSet(), batchMap.keySet())); } if (assertDone) { for (Map.Entry entry : batchMap.entrySet()) { Preconditions.checkState( entry.getValue().isDone(), "%s had not done %s", keyForDebugging, entry); } } return batchMap; } private void checkActive() { Preconditions.checkState(building, skyKey); } NestedSet buildEvents(NodeEntry entry, boolean missingChildren) throws InterruptedException { if (!evaluatorContext.getStoredEventFilter().storeEventsAndPosts()) { return NestedSetBuilder.emptySet(Order.STABLE_ORDER); } // Aggregate the nested set of events from the direct deps, also adding the events from // building this value. NestedSetBuilder eventBuilder = NestedSetBuilder.stableOrder(); ImmutableList events = eventHandler.getEvents(); if (!events.isEmpty()) { eventBuilder.add(new TaggedEvents(getTagFromKey(), events)); } GroupedList depKeys = entry.getTemporaryDirectDeps(); Collection deps = getDepValuesForDoneNodeMaybeFromError(depKeys); if (!missingChildren && depKeys.numElements() != deps.size()) { throw new IllegalStateException( "Missing keys for " + skyKey + ". Present values: " + deps + " requested from: " + depKeys + ", " + entry); } for (SkyValue value : deps) { eventBuilder.addTransitive(ValueWithMetadata.getEvents(value)); } return eventBuilder.build(); } NestedSet buildPosts(NodeEntry entry) throws InterruptedException { if (!evaluatorContext.getStoredEventFilter().storeEventsAndPosts()) { return NestedSetBuilder.emptySet(Order.STABLE_ORDER); } NestedSetBuilder postBuilder = NestedSetBuilder.stableOrder(); postBuilder.addAll(eventHandler.getPosts()); GroupedList depKeys = entry.getTemporaryDirectDeps(); Collection deps = getDepValuesForDoneNodeMaybeFromError(depKeys); for (SkyValue value : deps) { postBuilder.addTransitive(ValueWithMetadata.getPosts(value)); } return postBuilder.build(); } void setValue(SkyValue newValue) { Preconditions.checkState( errorInfo == null && bubbleErrorInfo == null, "%s %s %s %s", skyKey, newValue, errorInfo, bubbleErrorInfo); Preconditions.checkState(value == null, "%s %s %s", skyKey, value, newValue); value = newValue; } /** * Set this node to be in error. The node's value must not have already been set. However, all * dependencies of this node must already have been registered, since this method may * register a dependence on the error transience node, which should always be the last dep. */ void setError(NodeEntry state, ErrorInfo errorInfo) throws InterruptedException { Preconditions.checkState(value == null, "%s %s %s", skyKey, value, errorInfo); Preconditions.checkState(this.errorInfo == null, "%s %s %s", skyKey, this.errorInfo, errorInfo); if (errorInfo.isDirectlyTransient()) { NodeEntry errorTransienceNode = Preconditions.checkNotNull( evaluatorContext .getGraph() .get(skyKey, Reason.RDEP_ADDITION, ErrorTransienceValue.KEY), "Null error value? %s", skyKey); DependencyState triState; if (oldDeps.contains(ErrorTransienceValue.KEY)) { triState = errorTransienceNode.checkIfDoneForDirtyReverseDep(skyKey); } else { triState = errorTransienceNode.addReverseDepAndCheckIfDone(skyKey); } Preconditions.checkState( triState == DependencyState.DONE, "%s %s %s", skyKey, triState, errorInfo); state.addTemporaryDirectDeps(GroupedListHelper.create(ErrorTransienceValue.KEY)); state.signalDep(); } this.errorInfo = Preconditions.checkNotNull(errorInfo, skyKey); } private Map getValuesMaybeFromError(Iterable keys) throws InterruptedException { // Use a HashMap, not an ImmutableMap.Builder, because we have not yet deduplicated these keys // and ImmutableMap.Builder does not tolerate duplicates. The map will be thrown away // shortly in any case. Map result = new HashMap<>(); ArrayList missingKeys = new ArrayList<>(); for (SkyKey key : keys) { Preconditions.checkState( !key.equals(ErrorTransienceValue.KEY), "Error transience key cannot be in requested deps of %s", skyKey); SkyValue value = maybeGetValueFromErrorOrDeps(key); if (value == null) { missingKeys.add(key); } else { result.put(key, value); } } Map missingEntries = evaluatorContext.getBatchValues(skyKey, Reason.DEP_REQUESTED, missingKeys); for (SkyKey key : missingKeys) { result.put(key, getValueOrNullMarker(missingEntries.get(key))); } return result; } /** * Returns just the values of the deps in {@code depKeys}, looking at {@code bubbleErrorInfo}, * {@link #directDeps}, and the backing {@link #evaluatorContext#graph} in that order. Any deps * that are not yet done will not have their values present in the returned collection. */ private Collection getDepValuesForDoneNodeMaybeFromError(GroupedList depKeys) throws InterruptedException { int keySize = depKeys.numElements(); List result = new ArrayList<>(keySize); // depKeys consists of all known deps of this entry. That should include all the keys in // directDeps, and any keys in bubbleErrorInfo. We expect to have to retrieve the keys that // are not in either one. int expectedMissingKeySize = Math.max( keySize - directDeps.size() - (bubbleErrorInfo == null ? 0 : bubbleErrorInfo.size()), 0); ArrayList missingKeys = new ArrayList<>(expectedMissingKeySize); for (SkyKey key : depKeys.getAllElementsAsIterable()) { SkyValue value = maybeGetValueFromErrorOrDeps(key); if (value == null) { missingKeys.add(key); } else { result.add(value); } } for (NodeEntry entry : evaluatorContext.getBatchValues(skyKey, Reason.DEP_REQUESTED, missingKeys).values()) { result.add(getValueOrNullMarker(entry)); } return result; } @Nullable private SkyValue maybeGetValueFromErrorOrDeps(SkyKey key) throws InterruptedException { return maybeGetValueFromError(key, directDeps.get(key), bubbleErrorInfo); } private static SkyValue getValueOrNullMarker(@Nullable NodeEntry nodeEntry) throws InterruptedException { return isDoneForBuild(nodeEntry) ? nodeEntry.getValueMaybeWithMetadata() : NULL_MARKER; } @Override protected Map getValueOrUntypedExceptions( Iterable depKeys) throws InterruptedException { checkActive(); newlyRequestedDeps.startGroup(); Map values = getValuesMaybeFromError(depKeys); for (Map.Entry depEntry : values.entrySet()) { SkyKey depKey = depEntry.getKey(); SkyValue depValue = depEntry.getValue(); if (depValue == NULL_MARKER) { if (directDeps.containsKey(depKey)) { throw new IllegalStateException( "Undone key " + depKey + " was already in deps of " + skyKey + "( dep: " + evaluatorContext.getGraph().get(skyKey, Reason.OTHER, depKey) + ", parent: " + evaluatorContext.getGraph().get(null, Reason.OTHER, skyKey)); } valuesMissing = true; addDep(depKey); continue; } ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(depEntry.getValue()); if (errorInfo != null) { childErrorInfos.add(errorInfo); if (bubbleErrorInfo != null) { // Set interrupted status, to try to prevent the calling SkyFunction from doing anything // fancy after this. SkyFunctions executed during error bubbling are supposed to // (quickly) rethrow errors or return a value/null (but there's currently no way to // enforce this). Thread.currentThread().interrupt(); } if ((!evaluatorContext.keepGoing() && bubbleErrorInfo == null) || errorInfo.getException() == null) { valuesMissing = true; // We arbitrarily record the first child error if we are about to abort. if (!evaluatorContext.keepGoing() && depErrorKey == null) { depErrorKey = depKey; } } } if (!directDeps.containsKey(depKey)) { if (bubbleErrorInfo == null) { addDep(depKey); } evaluatorContext .getReplayingNestedSetPostableVisitor() .visit(ValueWithMetadata.getPosts(depValue)); evaluatorContext .getReplayingNestedSetEventVisitor() .visit(ValueWithMetadata.getEvents(depValue)); } } newlyRequestedDeps.endGroup(); return Maps.transformValues( values, maybeWrappedValue -> { if (maybeWrappedValue == NULL_MARKER) { return ValueOrUntypedException.ofNull(); } SkyValue justValue = ValueWithMetadata.justValue(maybeWrappedValue); ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(maybeWrappedValue); if (justValue != null && (evaluatorContext.keepGoing() || errorInfo == null)) { // If the dep did compute a value, it is given to the caller if we are in // keepGoing mode or if we are in noKeepGoingMode and there were no errors computing // it. return ValueOrUntypedException.ofValueUntyped(justValue); } // There was an error building the value, which we will either report by throwing an // exception or insulate the caller from by returning null. Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, maybeWrappedValue); Exception exception = errorInfo.getException(); if (!evaluatorContext.keepGoing() && exception != null && bubbleErrorInfo == null) { // Child errors should not be propagated in noKeepGoing mode (except during error // bubbling). Instead we should fail fast. return ValueOrUntypedException.ofNull(); } if (exception != null) { // Give builder a chance to handle this exception. return ValueOrUntypedException.ofExn(exception); } // In a cycle. Preconditions.checkState( !Iterables.isEmpty(errorInfo.getCycleInfo()), "%s %s %s", skyKey, errorInfo, maybeWrappedValue); return ValueOrUntypedException.ofNull(); }); } private void addDep(SkyKey key) { newlyRequestedDeps.add(key); } /** * If {@code !keepGoing} and there is at least one dep in error, returns a dep in error. Otherwise * returns {@code null}. */ @Nullable SkyKey getDepErrorKey() { return depErrorKey; } @Override public ExtendedEventHandler getListener() { checkActive(); return eventHandler; } void doneBuilding() { building = false; } GroupedListHelper getNewlyRequestedDeps() { return newlyRequestedDeps; } Collection getDirectDepsValues() { return directDeps.values(); } Collection getChildErrorInfos() { return childErrorInfos; } /** * Apply the change to the graph (mostly) atomically and signal all nodes that are waiting for * this node to complete. Adding nodes and signaling is not atomic, but may need to be changed for * interruptibility. * *

Parents are only enqueued if {@code enqueueParents} holds. Parents should be enqueued unless * (1) this node is being built after the main evaluation has aborted, or (2) this node is being * built with --nokeep_going, and so we are about to shut down the main evaluation anyway. * *

The reverse deps that would have been enqueued are returned if {@code enqueueParents} is * {@link EnqueueParentBehavior#SIGNAL} or {@link EnqueueParentBehavior#NO_ACTION}, so that the * caller may simulate actions on the parents if desired. Otherwise this method returns null. */ Set commit(NodeEntry primaryEntry, EnqueueParentBehavior enqueueParents) throws InterruptedException { // Construct the definitive error info, if there is one. if (errorInfo == null) { errorInfo = evaluatorContext.getErrorInfoManager().getErrorInfoToUse( skyKey, value != null, childErrorInfos); } // We have the following implications: // errorInfo == null => value != null => enqueueParents. // All these implications are strict: // (1) errorInfo != null && value != null happens for values with recoverable errors. // (2) value == null && enqueueParents happens for values that are found to have errors // during a --keep_going build. NestedSet posts = buildPosts(primaryEntry); NestedSet events = buildEvents(primaryEntry, /*missingChildren=*/ false); Version valueVersion; SkyValue valueWithMetadata; if (value == null) { Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry); valueWithMetadata = ValueWithMetadata.error(errorInfo, events, posts); } else { // We must be enqueueing parents if we have a value. Preconditions.checkState( enqueueParents == EnqueueParentBehavior.ENQUEUE, "%s %s", skyKey, primaryEntry); valueWithMetadata = ValueWithMetadata.normal(value, errorInfo, events, posts); } if (!oldDeps.isEmpty()) { // Remove the rdep on this entry for each of its old deps that is no longer a direct dep. Set depsToRemove = Sets.difference(oldDeps, primaryEntry.getTemporaryDirectDeps().toSet()); Collection oldDepEntries = evaluatorContext.getGraph().getBatch(skyKey, Reason.RDEP_REMOVAL, depsToRemove).values(); for (NodeEntry oldDepEntry : oldDepEntries) { oldDepEntry.removeReverseDep(skyKey); } } // If this entry is dirty, setValue may not actually change it, if it determines that // the data being written now is the same as the data already present in the entry. // We could consider using max(childVersions) here instead of graphVersion. When full // versioning is implemented, this would allow evaluation at a version between // max(childVersions) and graphVersion to re-use this result. Set reverseDeps = primaryEntry.setValue(valueWithMetadata, evaluatorContext.getGraphVersion()); // Note that if this update didn't actually change the value entry, this version may not // be the graph version. valueVersion = primaryEntry.getVersion(); Preconditions.checkState( valueVersion.atMost(evaluatorContext.getGraphVersion()), "%s should be at most %s in the version partial ordering", valueVersion, evaluatorContext.getGraphVersion()); // Tell the receiver that this value was built. If valueVersion.equals(graphVersion), it was // evaluated this run, and so was changed. Otherwise, it is less than graphVersion, by the // Preconditions check above, and was not actually changed this run -- when it was written // above, its version stayed below this update's version, so its value remains the same. // We use a SkyValueSupplier here because it keeps a reference to the entry, allowing for // the receiver to be confident that the entry is readily accessible in memory. EvaluationState evaluationState = valueVersion.equals(evaluatorContext.getGraphVersion()) ? EvaluationState.BUILT : EvaluationState.CLEAN; evaluatorContext .getProgressReceiver() .evaluated( skyKey, evaluationState == EvaluationState.BUILT ? value : null, new EvaluationSuccessStateSupplier(primaryEntry), evaluationState); evaluatorContext.signalValuesAndEnqueueIfReady( skyKey, reverseDeps, valueVersion, enqueueParents); evaluatorContext.getReplayingNestedSetPostableVisitor().visit(posts); evaluatorContext.getReplayingNestedSetEventVisitor().visit(events); return enqueueParents == EnqueueParentBehavior.ENQUEUE ? null : reverseDeps; } @Nullable private String getTagFromKey() { return evaluatorContext.getSkyFunctions().get(skyKey.functionName()).extractTag(skyKey); } /** * Gets the latch that is counted down when an exception is thrown in {@code * AbstractQueueVisitor}. For use in tests to check if an exception actually was thrown. Calling * {@code AbstractQueueVisitor#awaitExceptionForTestingOnly} can throw a spurious {@link * InterruptedException} because {@link CountDownLatch#await} checks the interrupted bit before * returning, even if the latch is already at 0. See bug "testTwoErrors is flaky". */ CountDownLatch getExceptionLatchForTesting() { return evaluatorContext.getVisitor().getExceptionLatchForTestingOnly(); } @Override public boolean inErrorBubblingForTesting() { return bubbleErrorInfo != null; } @Override public void registerDependencies(Iterable keys) { newlyRequestedDeps.startGroup(); for (SkyKey key : keys) { if (!directDeps.containsKey(key)) { addDep(key); } } newlyRequestedDeps.endGroup(); } }