aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java')
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java1786
1 files changed, 1786 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
new file mode 100644
index 0000000000..39f11d79ab
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
@@ -0,0 +1,1786 @@
+// 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.skyframe;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+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.NestedSetVisitor;
+import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
+import com.google.devtools.build.skyframe.BuildingState.DirtyState;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
+import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
+import com.google.devtools.build.skyframe.Scheduler.SchedulerException;
+import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException;
+import com.google.devtools.build.skyframe.ValueOrExceptionUtils.BottomException;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+/**
+ * Evaluates a set of given functions ({@code SkyFunction}s) with arguments ({@code SkyKey}s).
+ * Cycles are not allowed and are detected during the traversal.
+ *
+ * <p>This class implements multi-threaded evaluation. This is a fairly complex process that has
+ * strong consistency requirements between the {@link ProcessableGraph}, the nodes in the graph of
+ * type {@link NodeEntry}, the work queue, and the set of in-flight nodes.
+ *
+ * <p>The basic invariants are:
+ *
+ * <p>A node can be in one of three states: ready, waiting, and done. A node is ready if and only
+ * if all of its dependencies have been signaled. A node is done if it has a value. It is waiting
+ * if not all of its dependencies have been signaled.
+ *
+ * <p>A node must be in the work queue if and only if it is ready. It is an error for a node to be
+ * in the work queue twice at the same time.
+ *
+ * <p>A node is considered in-flight if it has been created, and is not done yet. In case of an
+ * interrupt, the work queue is discarded, and the in-flight set is used to remove partially
+ * computed values.
+ *
+ * <p>Each evaluation of the graph takes place at a "version," which is currently given by a
+ * non-negative {@code long}. The version can also be thought of as an "mtime." Each node in the
+ * graph has a version, which is the last version at which its value changed. This version data is
+ * used to avoid unnecessary re-evaluation of values. If a node is re-evaluated and found to have
+ * the same data as before, its version (mtime) remains the same. If all of a node's children's
+ * have the same version as before, its re-evaluation can be skipped.
+ *
+ * <p>This class is not intended for direct use, and is only exposed as public for use in
+ * evaluation implementations outside of this package.
+ */
+public final class ParallelEvaluator implements Evaluator {
+ private final ProcessableGraph graph;
+ private final Version graphVersion;
+
+ private final Predicate<SkyKey> nodeEntryIsDone = new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey skyKey) {
+ return isDoneForBuild(graph.get(skyKey));
+ }
+ };
+
+ private final ImmutableMap<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions;
+
+ private final EventHandler reporter;
+ private final NestedSetVisitor<TaggedEvents> replayingNestedSetEventVisitor;
+ private final boolean keepGoing;
+ private final int threadCount;
+ @Nullable private final EvaluationProgressReceiver progressReceiver;
+ private final DirtyKeyTracker dirtyKeyTracker;
+ private final AtomicBoolean errorEncountered = new AtomicBoolean(false);
+
+ private static final Interner<SkyKey> KEY_CANONICALIZER = Interners.newWeakInterner();
+
+ public ParallelEvaluator(ProcessableGraph graph, Version graphVersion,
+ ImmutableMap<? extends SkyFunctionName, ? extends SkyFunction> skyFunctions,
+ final EventHandler reporter,
+ MemoizingEvaluator.EmittedEventState emittedEventState,
+ boolean keepGoing, int threadCount,
+ @Nullable EvaluationProgressReceiver progressReceiver,
+ DirtyKeyTracker dirtyKeyTracker) {
+ this.graph = graph;
+ this.skyFunctions = skyFunctions;
+ this.graphVersion = graphVersion;
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.keepGoing = keepGoing;
+ this.threadCount = threadCount;
+ this.progressReceiver = progressReceiver;
+ this.dirtyKeyTracker = Preconditions.checkNotNull(dirtyKeyTracker);
+ this.replayingNestedSetEventVisitor =
+ new NestedSetVisitor<>(new NestedSetEventReceiver(reporter), emittedEventState);
+ }
+
+ /**
+ * Receives the events from the NestedSet and delegates to the reporter.
+ */
+ private static class NestedSetEventReceiver implements NestedSetVisitor.Receiver<TaggedEvents> {
+
+ private final EventHandler reporter;
+
+ public NestedSetEventReceiver(EventHandler reporter) {
+ this.reporter = reporter;
+ }
+ @Override
+ public void accept(TaggedEvents events) {
+ String tag = events.getTag();
+ for (Event e : events.getEvents()) {
+ reporter.handle(e.withTag(tag));
+ }
+ }
+ }
+
+ /**
+ * A suitable {@link SkyFunction.Environment} implementation.
+ */
+ class SkyFunctionEnvironment implements SkyFunction.Environment {
+ private boolean building = true;
+ private boolean valuesMissing = false;
+ private SkyKey depErrorKey = null;
+ private final SkyKey skyKey;
+ private SkyValue value = null;
+ private ErrorInfo errorInfo = null;
+ private final Map<SkyKey, ValueWithMetadata> bubbleErrorInfo;
+ /** The set of values previously declared as dependencies. */
+ private final Set<SkyKey> 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<SkyKey> newlyRequestedDeps = new GroupedListHelper<>();
+
+ /**
+ * The value visitor managing the thread pool. Used to enqueue parents when this value is
+ * finished, and, during testing, to block until an exception is thrown if a value builder
+ * requests that.
+ */
+ private final ValueVisitor visitor;
+
+ /** The set of errors encountered while fetching children. */
+ private final Collection<ErrorInfo> childErrorInfos = new LinkedHashSet<>();
+ private final StoredEventHandler eventHandler = new StoredEventHandler() {
+ @Override
+ public void handle(Event e) {
+ checkActive();
+ switch (e.getKind()) {
+ case INFO:
+ throw new UnsupportedOperationException("Values should not display INFO messages: " +
+ skyKey + " printed " + e.getLocation() + ": " + e.getMessage());
+ case PROGRESS:
+ reporter.handle(e);
+ break;
+ default:
+ super.handle(e);
+ }
+ }
+ };
+
+ private SkyFunctionEnvironment(SkyKey skyKey, Set<SkyKey> directDeps, ValueVisitor visitor) {
+ this(skyKey, directDeps, null, visitor);
+ }
+
+ private SkyFunctionEnvironment(SkyKey skyKey, Set<SkyKey> directDeps,
+ @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, ValueVisitor visitor) {
+ this.skyKey = skyKey;
+ this.directDeps = Collections.unmodifiableSet(directDeps);
+ this.bubbleErrorInfo = bubbleErrorInfo;
+ this.childErrorInfos.addAll(childErrorInfos);
+ this.visitor = visitor;
+ }
+
+ private void checkActive() {
+ Preconditions.checkState(building, skyKey);
+ }
+
+ private NestedSet<TaggedEvents> buildEvents(boolean missingChildren) {
+ // Aggregate the nested set of events from the direct deps, also adding the events from
+ // building this value.
+ NestedSetBuilder<TaggedEvents> eventBuilder = NestedSetBuilder.stableOrder();
+ ImmutableList<Event> events = eventHandler.getEvents();
+ if (!events.isEmpty()) {
+ eventBuilder.add(new TaggedEvents(getTagFromKey(), events));
+ }
+ for (SkyKey dep : graph.get(skyKey).getTemporaryDirectDeps()) {
+ ValueWithMetadata value = getValueMaybeFromError(dep, bubbleErrorInfo);
+ if (value != null) {
+ eventBuilder.addTransitive(value.getTransitiveEvents());
+ } else {
+ Preconditions.checkState(missingChildren, "", dep, skyKey);
+ }
+ }
+ return eventBuilder.build();
+ }
+
+ /**
+ * If this node has an error, that is, if errorInfo is non-null, do nothing. Otherwise, set
+ * errorInfo to the union of the child errors that were recorded earlier by getValueOrException,
+ * if there are any.
+ */
+ private void finalizeErrorInfo() {
+ if (errorInfo == null && !childErrorInfos.isEmpty()) {
+ errorInfo = new ErrorInfo(skyKey, childErrorInfos);
+ }
+ }
+
+ private 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 <i>must</i> already have been registered, since this method may
+ * register a dependence on the error transience node, which should always be the last dep.
+ */
+ private void setError(ErrorInfo errorInfo) {
+ Preconditions.checkState(value == null, "%s %s %s", skyKey, value, errorInfo);
+ Preconditions.checkState(this.errorInfo == null,
+ "%s %s %s", skyKey, this.errorInfo, errorInfo);
+
+ if (errorInfo.isTransient()) {
+ DependencyState triState =
+ graph.get(ErrorTransienceValue.key()).addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(triState == DependencyState.DONE,
+ "%s %s %s", skyKey, triState, errorInfo);
+
+ final NodeEntry state = graph.get(skyKey);
+ state.addTemporaryDirectDeps(
+ GroupedListHelper.create(ImmutableList.of(ErrorTransienceValue.key())));
+ state.signalDep();
+ }
+
+ this.errorInfo = Preconditions.checkNotNull(errorInfo, skyKey);
+ }
+
+ /** Get a child of the value being evaluated, for use by the value builder. */
+ private ValueOrUntypedException getValueOrUntypedException(SkyKey depKey) {
+ checkActive();
+ depKey = KEY_CANONICALIZER.intern(depKey); // Canonicalize SkyKeys to save memory.
+ ValueWithMetadata value = getValueMaybeFromError(depKey, bubbleErrorInfo);
+ if (value == null) {
+ // If this entry is not yet done then (optionally) record the missing dependency and return
+ // null.
+ valuesMissing = true;
+ if (bubbleErrorInfo != null) {
+ // Values being built just for their errors don't get to request new children.
+ return ValueOrExceptionUtils.ofNull();
+ }
+ Preconditions.checkState(!directDeps.contains(depKey), "%s %s %s", skyKey, depKey, value);
+ addDep(depKey);
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ if (!directDeps.contains(depKey)) {
+ // If this child is done, we will return it, but also record that it was newly requested so
+ // that the dependency can be properly registered in the graph.
+ addDep(depKey);
+ }
+
+ replayingNestedSetEventVisitor.visit(value.getTransitiveEvents());
+ ErrorInfo errorInfo = value.getErrorInfo();
+
+ if (errorInfo != null) {
+ childErrorInfos.add(errorInfo);
+ }
+
+ if (value.getValue() != null && (keepGoing || errorInfo == null)) {
+ // The caller is given the value of the value if there was no error computing the value, or
+ // if this is a keepGoing build (in which case each value should get child values even if
+ // there are also errors).
+ return ValueOrExceptionUtils.ofValueUntyped(value.getValue());
+ }
+
+ // 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 %s", skyKey, depKey, value);
+
+ if (!keepGoing && errorInfo.getException() != null && bubbleErrorInfo == null) {
+ // Child errors should not be propagated in noKeepGoing mode (except during error bubbling).
+ // Instead we should fail fast.
+
+ // We arbitrarily record the first child error.
+ if (depErrorKey == null) {
+ depErrorKey = depKey;
+ }
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ if (bubbleErrorInfo != null) {
+ // Set interrupted status, so that builder doesn't try anything fancy after this.
+ Thread.currentThread().interrupt();
+ }
+ if (errorInfo.getException() != null) {
+ // Give builder a chance to handle this exception.
+ Exception e = errorInfo.getException();
+ return ValueOrExceptionUtils.ofExn(e);
+ }
+ // In a cycle.
+ Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()), "%s %s %s %s", skyKey,
+ depKey, errorInfo, value);
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNull();
+ }
+
+ private <E extends Exception> ValueOrException<E> getValueOrException(SkyKey depKey,
+ Class<E> exceptionClass) {
+ return ValueOrExceptionUtils.downcovert(getValueOrException(depKey, exceptionClass,
+ BottomException.class), exceptionClass);
+ }
+
+ private <E1 extends Exception, E2 extends Exception> ValueOrException2<E1, E2>
+ getValueOrException(SkyKey depKey, Class<E1> exceptionClass1, Class<E2> exceptionClass2) {
+ return ValueOrExceptionUtils.downconvert(getValueOrException(depKey, exceptionClass1,
+ exceptionClass2, BottomException.class), exceptionClass1, exceptionClass2);
+ }
+
+ private <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ ValueOrException3<E1, E2, E3> getValueOrException(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3) {
+ return ValueOrExceptionUtils.downconvert(getValueOrException(depKey, exceptionClass1,
+ exceptionClass2, exceptionClass3, BottomException.class), exceptionClass1,
+ exceptionClass2, exceptionClass3);
+ }
+
+ private <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> ValueOrException4<E1, E2, E3, E4> getValueOrException(SkyKey depKey,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3,
+ Class<E4> exceptionClass4) {
+ SkyFunctionException.validateExceptionType(exceptionClass1);
+ SkyFunctionException.validateExceptionType(exceptionClass2);
+ SkyFunctionException.validateExceptionType(exceptionClass3);
+ SkyFunctionException.validateExceptionType(exceptionClass4);
+ ValueOrUntypedException voe = getValueOrUntypedException(depKey);
+ SkyValue value = voe.getValue();
+ if (value != null) {
+ return ValueOrExceptionUtils.ofValue(value);
+ }
+ Exception e = voe.getException();
+ if (e != null) {
+ if (exceptionClass1.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn1(exceptionClass1.cast(e));
+ }
+ if (exceptionClass2.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn2(exceptionClass2.cast(e));
+ }
+ if (exceptionClass3.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn3(exceptionClass3.cast(e));
+ }
+ if (exceptionClass4.isInstance(e)) {
+ return ValueOrExceptionUtils.ofExn4(exceptionClass4.cast(e));
+ }
+ }
+ valuesMissing = true;
+ return ValueOrExceptionUtils.ofNullValue();
+ }
+
+ @Override
+ @Nullable
+ public SkyValue getValue(SkyKey depKey) {
+ try {
+ return getValueOrThrow(depKey, BottomException.class);
+ } catch (BottomException e) {
+ throw new IllegalStateException("shouldn't reach here");
+ }
+ }
+
+ @Override
+ @Nullable
+ public <E extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E> exceptionClass)
+ throws E {
+ return getValueOrException(depKey, exceptionClass).get();
+ }
+
+ @Override
+ @Nullable
+ public <E1 extends Exception, E2 extends Exception> SkyValue getValueOrThrow(SkyKey depKey,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2) throws E1, E2 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2).get();
+ }
+
+ @Override
+ @Nullable
+ public <E1 extends Exception, E2 extends Exception,
+ E3 extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3) throws E1, E2, E3 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2, exceptionClass3).get();
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> SkyValue getValueOrThrow(SkyKey depKey, Class<E1> exceptionClass1,
+ Class<E2> exceptionClass2, Class<E3> exceptionClass3, Class<E4> exceptionClass4) throws E1,
+ E2, E3, E4 {
+ return getValueOrException(depKey, exceptionClass1, exceptionClass2, exceptionClass3,
+ exceptionClass4).get();
+ }
+
+ @Override
+ public Map<SkyKey, SkyValue> getValues(Iterable<SkyKey> depKeys) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, BottomException.class),
+ GET_VALUE_FROM_VOE);
+ }
+
+ @Override
+ public <E extends Exception> Map<SkyKey, ValueOrException<E>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E> exceptionClass) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass, BottomException.class),
+ makeSafeDowncastToVOEFunction(exceptionClass));
+ }
+
+ @Override
+ public <E1 extends Exception,
+ E2 extends Exception> Map<SkyKey, ValueOrException2<E1, E2>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E1> exceptionClass1, Class<E2> exceptionClass2) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass1, exceptionClass2,
+ BottomException.class), makeSafeDowncastToVOE2Function(exceptionClass1,
+ exceptionClass2));
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception> Map<SkyKey,
+ ValueOrException3<E1, E2, E3>> getValuesOrThrow(Iterable<SkyKey> depKeys,
+ Class<E1> exceptionClass1, Class<E2> exceptionClass2, Class<E3> exceptionClass3) {
+ return Maps.transformValues(getValuesOrThrow(depKeys, exceptionClass1, exceptionClass2,
+ exceptionClass3, BottomException.class), makeSafeDowncastToVOE3Function(exceptionClass1,
+ exceptionClass2, exceptionClass3));
+ }
+
+ @Override
+ public <E1 extends Exception, E2 extends Exception, E3 extends Exception,
+ E4 extends Exception> Map<SkyKey, ValueOrException4<E1, E2, E3, E4>> getValuesOrThrow(
+ Iterable<SkyKey> depKeys, Class<E1> exceptionClass1, Class<E2> exceptionClass2,
+ Class<E3> exceptionClass3, Class<E4> exceptionClass4) {
+ Map<SkyKey, ValueOrException4<E1, E2, E3, E4>> result = new HashMap<>();
+ newlyRequestedDeps.startGroup();
+ for (SkyKey depKey : depKeys) {
+ if (result.containsKey(depKey)) {
+ continue;
+ }
+ result.put(depKey, getValueOrException(depKey, exceptionClass1, exceptionClass2,
+ exceptionClass3, exceptionClass4));
+ }
+ newlyRequestedDeps.endGroup();
+ return Collections.unmodifiableMap(result);
+ }
+
+ private void addDep(SkyKey key) {
+ if (!newlyRequestedDeps.contains(key)) {
+ // dep may have been requested already this evaluation. If not, add it.
+ newlyRequestedDeps.add(key);
+ }
+ }
+
+ @Override
+ public boolean valuesMissing() {
+ return valuesMissing;
+ }
+
+ /**
+ * If {@code !keepGoing} and there is at least one dep in error, returns a dep in error.
+ * Otherwise returns {@code null}.
+ */
+ @Nullable
+ private SkyKey getDepErrorKey() {
+ return depErrorKey;
+ }
+
+ @Override
+ public EventHandler getListener() {
+ checkActive();
+ return eventHandler;
+ }
+
+ private void doneBuilding() {
+ building = false;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>The node entry is informed if the node's value and error are definitive via the flag
+ * {@code completeValue}.
+ */
+ void commit(boolean enqueueParents) {
+ NodeEntry primaryEntry = Preconditions.checkNotNull(graph.get(skyKey), skyKey);
+ // Construct the definitive error info, if there is one.
+ finalizeErrorInfo();
+
+ // 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<TaggedEvents> events = buildEvents(/*missingChildren=*/false);
+ if (value == null) {
+ Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry);
+ // 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<SkyKey> reverseDeps = primaryEntry.setValue(
+ ValueWithMetadata.error(errorInfo, events), graphVersion);
+ signalValuesAndEnqueueIfReady(enqueueParents ? visitor : null, reverseDeps, graphVersion);
+ } else {
+ // We must be enqueueing parents if we have a value.
+ Preconditions.checkState(enqueueParents, "%s %s", skyKey, primaryEntry);
+ Set<SkyKey> reverseDeps;
+ Version valueVersion;
+ // 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.
+ reverseDeps = primaryEntry.setValue(
+ ValueWithMetadata.normal(value, errorInfo, events), graphVersion);
+ // 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(graphVersion),
+ "%s should be at most %s in the version partial ordering",
+ valueVersion, graphVersion);
+ if (progressReceiver != null) {
+ // 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 as before.
+ progressReceiver.evaluated(skyKey, value,
+ valueVersion.equals(graphVersion) ? EvaluationState.BUILT : EvaluationState.CLEAN);
+ }
+ signalValuesAndEnqueueIfReady(visitor, reverseDeps, valueVersion);
+ }
+
+ visitor.notifyDone(skyKey);
+ replayingNestedSetEventVisitor.visit(events);
+ }
+
+ @Nullable
+ private String getTagFromKey() {
+ return skyFunctions.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 visitor.getExceptionLatchForTestingOnly();
+ }
+
+ @Override
+ public boolean inErrorBubblingForTesting() {
+ return bubbleErrorInfo != null;
+ }
+ }
+
+ private static final Function<ValueOrException<BottomException>, SkyValue> GET_VALUE_FROM_VOE =
+ new Function<ValueOrException<BottomException>, SkyValue>() {
+ @Override
+ public SkyValue apply(ValueOrException<BottomException> voe) {
+ return ValueOrExceptionUtils.downcovert(voe);
+ }
+ };
+
+ private static <E extends Exception>
+ Function<ValueOrException2<E, BottomException>, ValueOrException<E>>
+ makeSafeDowncastToVOEFunction(final Class<E> exceptionClass) {
+ return new Function<ValueOrException2<E, BottomException>, ValueOrException<E>>() {
+ @Override
+ public ValueOrException<E> apply(ValueOrException2<E, BottomException> voe) {
+ return ValueOrExceptionUtils.downcovert(voe, exceptionClass);
+ }
+ };
+ }
+
+ private static <E1 extends Exception, E2 extends Exception>
+ Function<ValueOrException3<E1, E2, BottomException>, ValueOrException2<E1, E2>>
+ makeSafeDowncastToVOE2Function(final Class<E1> exceptionClass1,
+ final Class<E2> exceptionClass2) {
+ return new Function<ValueOrException3<E1, E2, BottomException>,
+ ValueOrException2<E1, E2>>() {
+ @Override
+ public ValueOrException2<E1, E2> apply(ValueOrException3<E1, E2, BottomException> voe) {
+ return ValueOrExceptionUtils.downconvert(voe, exceptionClass1, exceptionClass2);
+ }
+ };
+ }
+
+ private static <E1 extends Exception, E2 extends Exception, E3 extends Exception>
+ Function<ValueOrException4<E1, E2, E3, BottomException>, ValueOrException3<E1, E2, E3>>
+ makeSafeDowncastToVOE3Function(final Class<E1> exceptionClass1,
+ final Class<E2> exceptionClass2, final Class<E3> exceptionClass3) {
+ return new Function<ValueOrException4<E1, E2, E3, BottomException>,
+ ValueOrException3<E1, E2, E3>>() {
+ @Override
+ public ValueOrException3<E1, E2, E3> apply(ValueOrException4<E1, E2, E3,
+ BottomException> voe) {
+ return ValueOrExceptionUtils.downconvert(voe, exceptionClass1, exceptionClass2,
+ exceptionClass3);
+ }
+ };
+ }
+
+ private class ValueVisitor extends AbstractQueueVisitor {
+ private AtomicBoolean preventNewEvaluations = new AtomicBoolean(false);
+ private final Set<SkyKey> inflightNodes = Sets.newConcurrentHashSet();
+
+ private ValueVisitor(int threadCount) {
+ super(/*concurrent*/true,
+ threadCount,
+ threadCount,
+ 1, TimeUnit.SECONDS,
+ /*failFastOnException*/true,
+ /*failFastOnInterrupt*/true,
+ "skyframe-evaluator");
+ }
+
+ @Override
+ protected boolean isCriticalError(Throwable e) {
+ return e instanceof RuntimeException;
+ }
+
+ protected void waitForCompletion() throws InterruptedException {
+ work(/*failFastOnInterrupt=*/true);
+ }
+
+ public void enqueueEvaluation(final SkyKey key) {
+ // We unconditionally add the key to the set of in-flight nodes because even if evaluation is
+ // never scheduled we still want to remove the previously created NodeEntry from the graph.
+ // Otherwise we would leave the graph in a weird state (wasteful garbage in the best case and
+ // inconsistent in the worst case).
+ boolean newlyEnqueued = inflightNodes.add(key);
+ // All nodes enqueued for evaluation will be either verified clean, re-evaluated, or cleaned
+ // up after being in-flight when an error happens in nokeep_going mode or in the event of an
+ // interrupt. In any of these cases, they won't be dirty anymore.
+ if (newlyEnqueued) {
+ dirtyKeyTracker.notDirty(key);
+ }
+ if (preventNewEvaluations.get()) {
+ return;
+ }
+ if (newlyEnqueued && progressReceiver != null) {
+ progressReceiver.enqueueing(key);
+ }
+ enqueue(new Evaluate(this, key));
+ }
+
+ public void preventNewEvaluations() {
+ preventNewEvaluations.set(true);
+ }
+
+ public void notifyDone(SkyKey key) {
+ inflightNodes.remove(key);
+ }
+
+ private boolean isInflight(SkyKey key) {
+ return inflightNodes.contains(key);
+ }
+ }
+
+ /**
+ * An action that evaluates a value.
+ */
+ private class Evaluate implements Runnable {
+ private final ValueVisitor visitor;
+ /** The name of the value to be evaluated. */
+ private final SkyKey skyKey;
+
+ private Evaluate(ValueVisitor visitor, SkyKey skyKey) {
+ this.visitor = visitor;
+ this.skyKey = skyKey;
+ }
+
+ private void enqueueChild(SkyKey skyKey, NodeEntry entry, SkyKey child) {
+ Preconditions.checkState(!entry.isDone(), "%s %s", skyKey, entry);
+ Preconditions.checkState(!ErrorTransienceValue.key().equals(child),
+ "%s cannot request ErrorTransienceValue as a dep: %s", skyKey, entry);
+
+ NodeEntry depEntry = graph.createIfAbsent(child);
+ switch (depEntry.addReverseDepAndCheckIfDone(skyKey)) {
+ case DONE :
+ if (entry.signalDep(depEntry.getVersion())) {
+ // This can only happen if there are no more children to be added.
+ visitor.enqueueEvaluation(skyKey);
+ }
+ break;
+ case ADDED_DEP :
+ break;
+ case NEEDS_SCHEDULING :
+ visitor.enqueueEvaluation(child);
+ break;
+ }
+ }
+
+ /**
+ * Returns true if this depGroup consists of the error transience value and the error transience
+ * value is newer than the entry, meaning that the entry must be re-evaluated.
+ */
+ private boolean invalidatedByErrorTransience(Collection<SkyKey> depGroup, NodeEntry entry) {
+ return depGroup.size() == 1
+ && depGroup.contains(ErrorTransienceValue.key())
+ && !graph.get(ErrorTransienceValue.key()).getVersion().atMost(entry.getVersion());
+ }
+
+ @Override
+ public void run() {
+ NodeEntry state = graph.get(skyKey);
+ Preconditions.checkNotNull(state, "%s %s", skyKey, state);
+ Preconditions.checkState(state.isReady(), "%s %s", skyKey, state);
+
+ if (state.isDirty()) {
+ switch (state.getDirtyState()) {
+ case CHECK_DEPENDENCIES:
+ // Evaluating a dirty node for the first time, and checking its children to see if any
+ // of them have changed. Note that there must be dirty children for this to happen.
+
+ // Check the children group by group -- we don't want to evaluate a value that is no
+ // longer needed because an earlier dependency changed. For example, //foo:foo depends
+ // on target //bar:bar and is built. Then foo/BUILD is modified to remove the dependence
+ // on bar, and bar/BUILD is deleted. Reloading //bar:bar would incorrectly throw an
+ // exception. To avoid this, we must reload foo/BUILD first, at which point we will
+ // discover that it has changed, and re-evaluate target //foo:foo from scratch.
+ // On the other hand, when an action requests all of its inputs, we can safely check all
+ // of them in parallel on a subsequent build. So we allow checking an entire group in
+ // parallel here, if the node builder requested a group last build.
+ Collection<SkyKey> directDepsToCheck = state.getNextDirtyDirectDeps();
+
+ if (invalidatedByErrorTransience(directDepsToCheck, state)) {
+ // If this dep is the ErrorTransienceValue and the ErrorTransienceValue has been
+ // updated then we need to force a rebuild. We would like to just signal the entry as
+ // usual, but we can't, because then the ErrorTransienceValue would remain as a dep,
+ // which would be incorrect if, for instance, the value re-evaluated to a non-error.
+ state.forceRebuild();
+ break; // Fall through to re-evaluation.
+ } else {
+ // If this isn't the error transience value, it is safe to add these deps back to the
+ // node -- even if one of them has changed, the contract of pruning is that the node
+ // will request these deps again when it rebuilds. We must add these deps before
+ // enqueuing them, so that the node knows that it depends on them.
+ state.addTemporaryDirectDeps(GroupedListHelper.create(directDepsToCheck));
+ }
+
+ for (SkyKey directDep : directDepsToCheck) {
+ enqueueChild(skyKey, state, directDep);
+ }
+ return;
+ case VERIFIED_CLEAN:
+ // No child has a changed value. This node can be marked done and its parents signaled
+ // without any re-evaluation.
+ visitor.notifyDone(skyKey);
+ Set<SkyKey> reverseDeps = state.markClean();
+ SkyValue value = state.getValue();
+ if (progressReceiver != null && value != null) {
+ // Tell the receiver that the value was not actually changed this run.
+ progressReceiver.evaluated(skyKey, value, EvaluationState.CLEAN);
+ }
+ signalValuesAndEnqueueIfReady(visitor, reverseDeps, state.getVersion());
+ return;
+ case REBUILDING:
+ // Nothing to be done if we are already rebuilding.
+ }
+ }
+
+ // TODO(bazel-team): Once deps are requested in a deterministic order within a group, or the
+ // framework is resilient to rearranging group order, change this so that
+ // SkyFunctionEnvironment "follows along" as the node builder runs, iterating through the
+ // direct deps that were requested on a previous run. This would allow us to avoid the
+ // conversion of the direct deps into a set.
+ Set<SkyKey> directDeps = state.getTemporaryDirectDeps();
+ Preconditions.checkState(!directDeps.contains(ErrorTransienceValue.key()),
+ "%s cannot have a dep on ErrorTransienceValue during building: %s", skyKey, state);
+ // Get the corresponding SkyFunction and call it on this value.
+ SkyFunctionEnvironment env = new SkyFunctionEnvironment(skyKey, directDeps, visitor);
+ SkyFunctionName functionName = skyKey.functionName();
+ SkyFunction factory = skyFunctions.get(functionName);
+ Preconditions.checkState(factory != null, "%s %s", functionName, state);
+
+ SkyValue value = null;
+ Profiler.instance().startTask(ProfilerTask.SKYFUNCTION, skyKey);
+ try {
+ // TODO(bazel-team): count how many of these calls returns null vs. non-null
+ value = factory.compute(skyKey, env);
+ } catch (final SkyFunctionException builderException) {
+ ReifiedSkyFunctionException reifiedBuilderException =
+ new ReifiedSkyFunctionException(builderException, skyKey);
+ // Propagated transitive errors are treated the same as missing deps.
+ if (reifiedBuilderException.getRootCauseSkyKey().equals(skyKey)) {
+ boolean shouldFailFast = !keepGoing || builderException.isCatastrophic();
+ if (shouldFailFast) {
+ // After we commit this error to the graph but before the eval call completes with the
+ // error there is a race-like opportunity for the error to be used, either by an
+ // in-flight computation or by a future computation.
+ if (errorEncountered.compareAndSet(false, true)) {
+ // This is the first error encountered.
+ visitor.preventNewEvaluations();
+ } else {
+ // This is not the first error encountered, so we ignore it so that we can terminate
+ // with the first error.
+ return;
+ }
+ }
+
+ registerNewlyDiscoveredDepsForDoneEntry(skyKey, state, env);
+ ErrorInfo errorInfo = new ErrorInfo(reifiedBuilderException);
+ env.setError(errorInfo);
+ env.commit(/*enqueueParents=*/keepGoing);
+ if (!shouldFailFast) {
+ return;
+ }
+ throw SchedulerException.ofError(errorInfo, skyKey);
+ }
+ } catch (InterruptedException ie) {
+ // InterruptedException cannot be thrown by Runnable.run, so we must wrap it.
+ // Interrupts can be caught by both the Evaluator and the AbstractQueueVisitor.
+ // The former will unwrap the IE and propagate it as is; the latter will throw a new IE.
+ throw SchedulerException.ofInterruption(ie, skyKey);
+ } catch (RuntimeException re) {
+ // Programmer error (most likely NPE or a failed precondition in a SkyFunction). Output
+ // some context together with the exception.
+ String msg = prepareCrashMessage(skyKey, state.getInProgressReverseDeps());
+ throw new RuntimeException(msg, re);
+ } finally {
+ env.doneBuilding();
+ Profiler.instance().completeTask(ProfilerTask.SKYFUNCTION);
+ }
+
+ GroupedListHelper<SkyKey> newDirectDeps = env.newlyRequestedDeps;
+
+ if (value != null) {
+ Preconditions.checkState(!env.valuesMissing,
+ "%s -> %s, ValueEntry: %s", skyKey, newDirectDeps, state);
+ env.setValue(value);
+ registerNewlyDiscoveredDepsForDoneEntry(skyKey, state, env);
+ env.commit(/*enqueueParents=*/true);
+ return;
+ }
+
+ if (!newDirectDeps.isEmpty() && env.getDepErrorKey() != null) {
+ Preconditions.checkState(!keepGoing);
+ // We encountered a child error in noKeepGoing mode, so we want to fail fast. But we first
+ // need to add the edge between the current node and the child error it requested so that
+ // error bubbling can occur. Note that this edge will subsequently be removed during graph
+ // cleaning (since the current node will never be committed to the graph).
+ SkyKey childErrorKey = env.getDepErrorKey();
+ NodeEntry childErrorEntry = Preconditions.checkNotNull(graph.get(childErrorKey),
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey);
+ if (!state.getTemporaryDirectDeps().contains(childErrorKey)) {
+ // This means the cached error was freshly requested (e.g. the parent has never been
+ // built before).
+ Preconditions.checkState(newDirectDeps.contains(childErrorKey), "%s %s %s", state,
+ childErrorKey, newDirectDeps);
+ state.addTemporaryDirectDeps(GroupedListHelper.create(ImmutableList.of(childErrorKey)));
+ DependencyState childErrorState = childErrorEntry.addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(childErrorState == DependencyState.DONE,
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey,
+ childErrorEntry);
+ } else {
+ // This means the cached error was previously requested, and was then subsequently (after
+ // a restart) requested along with another sibling dep. This can happen on an incremental
+ // eval call when the parent is dirty and the child error is in a separate dependency
+ // group from the sibling dep.
+ Preconditions.checkState(!newDirectDeps.contains(childErrorKey), "%s %s %s", state,
+ childErrorKey, newDirectDeps);
+ Preconditions.checkState(childErrorEntry.isDone(),
+ "skyKey: %s, state: %s childErrorKey: %s", skyKey, state, childErrorKey,
+ childErrorEntry);
+ }
+ ErrorInfo childErrorInfo = Preconditions.checkNotNull(childErrorEntry.getErrorInfo());
+ throw SchedulerException.ofError(childErrorInfo, childErrorKey);
+ }
+
+ // TODO(bazel-team): This code is not safe to interrupt, because we would lose the state in
+ // newDirectDeps.
+
+ // TODO(bazel-team): An ill-behaved SkyFunction can throw us into an infinite loop where we
+ // add more dependencies on every run. [skyframe-core]
+
+ // Add all new keys to the set of known deps.
+ state.addTemporaryDirectDeps(newDirectDeps);
+
+ // If there were no newly requested dependencies, at least one of them was in error or there
+ // is a bug in the SkyFunction implementation. The environment has collected its errors, so we
+ // just order it to be built.
+ if (newDirectDeps.isEmpty()) {
+ // TODO(bazel-team): This means a bug in the SkyFunction. What to do?
+ Preconditions.checkState(!env.childErrorInfos.isEmpty(), "%s %s", skyKey, state);
+ env.commit(/*enqueueParents=*/keepGoing);
+ if (!keepGoing) {
+ throw SchedulerException.ofError(state.getErrorInfo(), skyKey);
+ }
+ return;
+ }
+
+ for (SkyKey newDirectDep : newDirectDeps) {
+ enqueueChild(skyKey, state, newDirectDep);
+ }
+ // It is critical that there is no code below this point.
+ }
+
+ private String prepareCrashMessage(SkyKey skyKey, Iterable<SkyKey> reverseDeps) {
+ StringBuilder reverseDepDump = new StringBuilder();
+ for (SkyKey key : reverseDeps) {
+ if (reverseDepDump.length() > MAX_REVERSEDEP_DUMP_LENGTH) {
+ reverseDepDump.append(", ...");
+ break;
+ }
+ if (reverseDepDump.length() > 0) {
+ reverseDepDump.append(", ");
+ }
+ reverseDepDump.append("'");
+ reverseDepDump.append(key.toString());
+ reverseDepDump.append("'");
+ }
+
+ return String.format(
+ "Unrecoverable error while evaluating node '%s' (requested by nodes %s)",
+ skyKey, reverseDepDump);
+ }
+
+ private static final int MAX_REVERSEDEP_DUMP_LENGTH = 1000;
+ }
+
+ /**
+ * Signals all parents that this node is finished. If visitor is not null, also enqueues any
+ * parents that are ready. If visitor is null, indicating that we are building this node after
+ * the main build aborted, then skip any parents that are already done (that can happen with
+ * cycles).
+ */
+ private void signalValuesAndEnqueueIfReady(@Nullable ValueVisitor visitor, Iterable<SkyKey> keys,
+ Version version) {
+ if (visitor != null) {
+ for (SkyKey key : keys) {
+ if (graph.get(key).signalDep(version)) {
+ visitor.enqueueEvaluation(key);
+ }
+ }
+ } else {
+ for (SkyKey key : keys) {
+ NodeEntry entry = Preconditions.checkNotNull(graph.get(key), key);
+ if (!entry.isDone()) {
+ // In cycles, we can have parents that are already done.
+ entry.signalDep(version);
+ }
+ }
+ }
+ }
+
+ /**
+ * If child is not done, removes key from child's reverse deps. Returns whether child should be
+ * removed from key's entry's direct deps.
+ */
+ private boolean removeIncompleteChild(SkyKey key, SkyKey child) {
+ NodeEntry childEntry = graph.get(child);
+ if (!isDoneForBuild(childEntry)) {
+ childEntry.removeReverseDep(key);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Add any additional deps that were registered during the run of a builder that finished by
+ * creating a node or throwing an error. Builders may throw errors even if all their deps were
+ * not provided -- we trust that a SkyFunction may be know it should throw an error even if not
+ * all of its requested deps are done. However, that means we're assuming the SkyFunction would
+ * throw that same error if all of its requested deps were done. Unfortunately, there is no way to
+ * enforce that condition.
+ */
+ private void registerNewlyDiscoveredDepsForDoneEntry(SkyKey skyKey, NodeEntry entry,
+ SkyFunctionEnvironment env) {
+ Set<SkyKey> unfinishedDeps = new HashSet<>();
+ Iterables.addAll(unfinishedDeps,
+ Iterables.filter(env.newlyRequestedDeps, Predicates.not(nodeEntryIsDone)));
+ env.newlyRequestedDeps.remove(unfinishedDeps);
+ entry.addTemporaryDirectDeps(env.newlyRequestedDeps);
+ for (SkyKey newDep : env.newlyRequestedDeps) {
+ NodeEntry depEntry = graph.get(newDep);
+ DependencyState triState = depEntry.addReverseDepAndCheckIfDone(skyKey);
+ Preconditions.checkState(DependencyState.DONE == triState,
+ "new dep %s was not already done for %s. ValueEntry: %s. DepValueEntry: %s",
+ newDep, skyKey, entry, depEntry);
+ entry.signalDep();
+ }
+ Preconditions.checkState(entry.isReady(), "%s %s %s", skyKey, entry, env.newlyRequestedDeps);
+ }
+
+ private void informProgressReceiverThatValueIsDone(SkyKey key) {
+ if (progressReceiver != null) {
+ NodeEntry entry = graph.get(key);
+ Preconditions.checkState(entry.isDone(), entry);
+ SkyValue value = entry.getValue();
+ if (value != null) {
+ Version valueVersion = entry.getVersion();
+ Preconditions.checkState(valueVersion.atMost(graphVersion),
+ "%s should be at most %s in the version partial ordering", valueVersion, graphVersion);
+ // Nodes with errors will have no value. Don't inform the receiver in that case.
+ // For most nodes we do not inform the progress receiver if they were already done
+ // when we retrieve them, but top-level nodes are presumably of more interest.
+ // If valueVersion is not equal to graphVersion, it must be less than it (by the
+ // Preconditions check above), and so the node is clean.
+ progressReceiver.evaluated(key, value, valueVersion.equals(graphVersion)
+ ? EvaluationState.BUILT
+ : EvaluationState.CLEAN);
+ }
+ }
+ }
+
+ @Override
+ @ThreadCompatible
+ public <T extends SkyValue> EvaluationResult<T> eval(Iterable<SkyKey> skyKeys)
+ throws InterruptedException {
+ ImmutableSet<SkyKey> skyKeySet = ImmutableSet.copyOf(skyKeys);
+
+ // Optimization: if all required node values are already present in the cache, return them
+ // directly without launching the heavy machinery, spawning threads, etc.
+ // Inform progressReceiver that these nodes are done to be consistent with the main code path.
+ if (Iterables.all(skyKeySet, nodeEntryIsDone)) {
+ for (SkyKey skyKey : skyKeySet) {
+ informProgressReceiverThatValueIsDone(skyKey);
+ }
+ // Note that the 'catastrophe' parameter doesn't really matter here (it's only used for
+ // sanity checking).
+ return constructResult(null, skyKeySet, null, /*catastrophe=*/false);
+ }
+
+ if (!keepGoing) {
+ Set<SkyKey> cachedErrorKeys = new HashSet<>();
+ for (SkyKey skyKey : skyKeySet) {
+ NodeEntry entry = graph.get(skyKey);
+ if (entry == null) {
+ continue;
+ }
+ if (entry.isDone() && entry.getErrorInfo() != null) {
+ informProgressReceiverThatValueIsDone(skyKey);
+ cachedErrorKeys.add(skyKey);
+ }
+ }
+
+ // Errors, even cached ones, should halt evaluations not in keepGoing mode.
+ if (!cachedErrorKeys.isEmpty()) {
+ // Note that the 'catastrophe' parameter doesn't really matter here (it's only used for
+ // sanity checking).
+ return constructResult(null, cachedErrorKeys, null, /*catastrophe=*/false);
+ }
+ }
+
+ // We delay this check until we know that some kind of evaluation is necessary, since !keepGoing
+ // and !keepsEdges are incompatible only in the case of a failed evaluation -- there is no
+ // need to be overly harsh to callers who are just trying to retrieve a cached result.
+ Preconditions.checkState(keepGoing || !(graph instanceof InMemoryGraph)
+ || ((InMemoryGraph) graph).keepsEdges(),
+ "nokeep_going evaluations are not allowed if graph edges are not kept: %s", skyKeys);
+
+ Profiler.instance().startTask(ProfilerTask.SKYFRAME_EVAL, skyKeySet);
+ try {
+ return eval(skyKeySet, new ValueVisitor(threadCount));
+ } finally {
+ Profiler.instance().completeTask(ProfilerTask.SKYFRAME_EVAL);
+ }
+ }
+
+ @ThreadCompatible
+ private <T extends SkyValue> EvaluationResult<T> eval(ImmutableSet<SkyKey> skyKeys,
+ ValueVisitor visitor) throws InterruptedException {
+ // We unconditionally add the ErrorTransienceValue here, to ensure that it will be created, and
+ // in the graph, by the time that it is needed. Creating it on demand in a parallel context sets
+ // up a race condition, because there is no way to atomically create a node and set its value.
+ NodeEntry errorTransienceEntry = graph.createIfAbsent(ErrorTransienceValue.key());
+ DependencyState triState = errorTransienceEntry.addReverseDepAndCheckIfDone(null);
+ Preconditions.checkState(triState != DependencyState.ADDED_DEP,
+ "%s %s", errorTransienceEntry, triState);
+ if (triState != DependencyState.DONE) {
+ errorTransienceEntry.setValue(new ErrorTransienceValue(), graphVersion);
+ // The error transience entry is always invalidated by the RecordingDifferencer.
+ // Now that the entry's value is set, it is no longer dirty.
+ dirtyKeyTracker.notDirty(ErrorTransienceValue.key());
+
+ Preconditions.checkState(
+ errorTransienceEntry.addReverseDepAndCheckIfDone(null) != DependencyState.ADDED_DEP,
+ errorTransienceEntry);
+ }
+ for (SkyKey skyKey : skyKeys) {
+ NodeEntry entry = graph.createIfAbsent(skyKey);
+ // This must be equivalent to the code in enqueueChild above, in order to be thread-safe.
+ switch (entry.addReverseDepAndCheckIfDone(null)) {
+ case NEEDS_SCHEDULING:
+ visitor.enqueueEvaluation(skyKey);
+ break;
+ case DONE:
+ informProgressReceiverThatValueIsDone(skyKey);
+ break;
+ case ADDED_DEP:
+ break;
+ default:
+ throw new IllegalStateException(entry + " for " + skyKey + " in unknown state");
+ }
+ }
+ try {
+ return waitForCompletionAndConstructResult(visitor, skyKeys);
+ } finally {
+ // TODO(bazel-team): In nokeep_going mode or in case of an interrupt, we need to remove
+ // partial values from the graph. Find a better way to handle those cases.
+ clean(visitor.inflightNodes);
+ }
+ }
+
+ private void clean(Set<SkyKey> inflightNodes) throws InterruptedException {
+ boolean alreadyInterrupted = Thread.interrupted();
+ // This parallel computation is fully cpu-bound, so we use a thread for each processor.
+ ExecutorService executor = Executors.newFixedThreadPool(
+ Runtime.getRuntime().availableProcessors(),
+ new ThreadFactoryBuilder().setNameFormat("ParallelEvaluator#clean %d").build());
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("ParallelEvaluator#clean");
+ for (final SkyKey key : inflightNodes) {
+ final NodeEntry entry = graph.get(key);
+ if (entry.isDone()) {
+ // Entry may be done in case of a RuntimeException or other programming bug. Do nothing,
+ // since (a) we're about to crash anyway, and (b) getTemporaryDirectDeps cannot be called
+ // on a done node, so the call below would crash, which would mask the actual exception
+ // that caused this state.
+ continue;
+ }
+ executor.execute(wrapper.wrap(new Runnable() {
+ @Override
+ public void run() {
+ cleanInflightNode(key, entry);
+ }
+ }));
+ }
+ // We uninterruptibly wait for all nodes to be cleaned because we want to make sure the graph
+ // is left in a good state.
+ //
+ // TODO(bazel-team): Come up with a better design for graph cleaning such that we can respond
+ // to interrupts in constant time.
+ boolean newlyInterrupted = ExecutorShutdownUtil.uninterruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (newlyInterrupted || alreadyInterrupted) {
+ throw new InterruptedException();
+ }
+ }
+
+ private void cleanInflightNode(SkyKey key, NodeEntry entry) {
+ Set<SkyKey> temporaryDeps = entry.getTemporaryDirectDeps();
+ graph.remove(key);
+ for (SkyKey dep : temporaryDeps) {
+ NodeEntry nodeEntry = graph.get(dep);
+ // The direct dep might have already been cleaned from the graph.
+ if (nodeEntry != null) {
+ // Only bother removing the reverse dep on done nodes since other in-flight nodes will be
+ // cleaned too.
+ if (nodeEntry.isDone()) {
+ nodeEntry.removeReverseDep(key);
+ }
+ }
+ }
+ }
+
+ private <T extends SkyValue> EvaluationResult<T> waitForCompletionAndConstructResult(
+ ValueVisitor visitor, Iterable<SkyKey> skyKeys) throws InterruptedException {
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo = null;
+ boolean catastrophe = false;
+ try {
+ visitor.waitForCompletion();
+ } catch (final SchedulerException e) {
+ Throwables.propagateIfPossible(e.getCause(), InterruptedException.class);
+ if (Thread.interrupted()) {
+ // As per the contract of AbstractQueueVisitor#work, if an unchecked exception is thrown and
+ // the build is interrupted, the thrown exception is what will be rethrown. Since the user
+ // presumably wanted to interrupt the build, we ignore the thrown SchedulerException (which
+ // doesn't indicate a programming bug) and throw an InterruptedException.
+ throw new InterruptedException();
+ }
+
+ SkyKey errorKey = Preconditions.checkNotNull(e.getFailedValue(), e);
+ // ErrorInfo could only be null if SchedulerException wrapped an InterruptedException, but
+ // that should have been propagated.
+ ErrorInfo errorInfo = Preconditions.checkNotNull(e.getErrorInfo(), errorKey);
+ catastrophe = errorInfo.isCatastrophic();
+ if (!catastrophe || !keepGoing) {
+ bubbleErrorInfo = bubbleErrorUp(errorInfo, errorKey, skyKeys, visitor);
+ } else {
+ // Bubbling the error up requires that graph edges are present for done nodes. This is not
+ // always the case in a keepGoing evaluation, since it is assumed that done nodes do not
+ // need to be traversed. In this case, we hope the caller is tolerant of a possibly empty
+ // result, and return prematurely.
+ bubbleErrorInfo = ImmutableMap.of(errorKey, graph.get(errorKey).getValueWithMetadata());
+ }
+ }
+
+ // Successful evaluation, either because keepGoing or because we actually did succeed.
+ // TODO(bazel-team): Maybe report root causes during the build for lower latency.
+ return constructResult(visitor, skyKeys, bubbleErrorInfo, catastrophe);
+ }
+
+ /**
+ * Walk up graph to find a top-level node (without parents) that wanted this failure. Store
+ * the failed nodes along the way in a map, with ErrorInfos that are appropriate for that layer.
+ * Example:
+ * foo bar
+ * \ /
+ * unrequested baz
+ * \ |
+ * failed-node
+ * User requests foo, bar. When failed-node fails, we look at its parents. unrequested is not
+ * in-flight, so we replace failed-node by baz and repeat. We look at baz's parents. foo is
+ * in-flight, so we replace baz by foo. Since foo is a top-level node and doesn't have parents,
+ * we then break, since we know a top-level node, foo, that depended on the failed node.
+ *
+ * There's the potential for a weird "track jump" here in the case:
+ * foo
+ * / \
+ * fail1 fail2
+ * If fail1 and fail2 fail simultaneously, fail2 may start propagating up in the loop below.
+ * However, foo requests fail1 first, and then throws an exception based on that. This is not
+ * incorrect, but may be unexpected.
+ *
+ * <p>Returns a map of errors that have been constructed during the bubbling up, so that the
+ * appropriate error can be returned to the caller, even though that error was not written to the
+ * graph. If a cycle is detected during the bubbling, this method aborts and returns null so that
+ * the normal cycle detection can handle the cycle.
+ *
+ * <p>Note that we are not propagating error to the first top-level node but to the highest one,
+ * because during this process we can add useful information about error from other nodes.
+ */
+ private Map<SkyKey, ValueWithMetadata> bubbleErrorUp(final ErrorInfo leafFailure,
+ SkyKey errorKey, Iterable<SkyKey> skyKeys, ValueVisitor visitor) {
+ Set<SkyKey> rootValues = ImmutableSet.copyOf(skyKeys);
+ ErrorInfo error = leafFailure;
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo = new HashMap<>();
+ boolean externalInterrupt = false;
+ while (true) {
+ NodeEntry errorEntry = graph.get(errorKey);
+ Iterable<SkyKey> reverseDeps = errorEntry.isDone()
+ ? errorEntry.getReverseDeps()
+ : errorEntry.getInProgressReverseDeps();
+ // We should break from loop only when node doesn't have any parents.
+ if (Iterables.isEmpty(reverseDeps)) {
+ Preconditions.checkState(rootValues.contains(errorKey),
+ "Current key %s has to be a top-level key: %s", errorKey, rootValues);
+ break;
+ }
+ SkyKey parent = null;
+ NodeEntry parentEntry = null;
+ for (SkyKey bubbleParent : reverseDeps) {
+ if (bubbleErrorInfo.containsKey(bubbleParent)) {
+ // We are in a cycle. Don't try to bubble anything up -- cycle detection will kick in.
+ return null;
+ }
+ NodeEntry bubbleParentEntry = Preconditions.checkNotNull(graph.get(bubbleParent),
+ "parent %s of %s not in graph", bubbleParent, errorKey);
+ // Might be the parent that requested the error.
+ if (bubbleParentEntry.isDone()) {
+ // This parent is cached from a previous evaluate call. We shouldn't bubble up to it
+ // since any error message produced won't be meaningful to this evaluate call.
+ // The child error must also be cached from a previous build.
+ Preconditions.checkState(errorEntry.isDone(), "%s %s", errorEntry, bubbleParentEntry);
+ Version parentVersion = bubbleParentEntry.getVersion();
+ Version childVersion = errorEntry.getVersion();
+ Preconditions.checkState(childVersion.atMost(graphVersion)
+ && !childVersion.equals(graphVersion),
+ "child entry is not older than the current graph version, but had a done parent. "
+ + "child: %s childEntry: %s, childVersion: %s"
+ + "bubbleParent: %s bubbleParentEntry: %s, parentVersion: %s, graphVersion: %s",
+ errorKey, errorEntry, childVersion,
+ bubbleParent, bubbleParentEntry, parentVersion, graphVersion);
+ Preconditions.checkState(parentVersion.atMost(graphVersion)
+ && !parentVersion.equals(graphVersion),
+ "parent entry is not older than the current graph version. "
+ + "child: %s childEntry: %s, childVersion: %s"
+ + "bubbleParent: %s bubbleParentEntry: %s, parentVersion: %s, graphVersion: %s",
+ errorKey, errorEntry, childVersion,
+ bubbleParent, bubbleParentEntry, parentVersion, graphVersion);
+ continue;
+ }
+ // Arbitrarily pick the first in-flight parent.
+ Preconditions.checkState(visitor.isInflight(bubbleParent),
+ "errorKey: %s, errorEntry: %s, bubbleParent: %s, bubbleParentEntry: %s", errorKey,
+ errorEntry, bubbleParent, bubbleParentEntry);
+ parent = bubbleParent;
+ parentEntry = bubbleParentEntry;
+ break;
+ }
+ Preconditions.checkNotNull(parent, "", errorKey, bubbleErrorInfo);
+ errorKey = parent;
+ SkyFunction factory = skyFunctions.get(parent.functionName());
+ if (parentEntry.isDirty()) {
+ switch (parentEntry.getDirtyState()) {
+ case CHECK_DEPENDENCIES:
+ // If this value's child was bubbled up to, it did not signal this value, and so we must
+ // manually make it ready to build.
+ parentEntry.signalDep();
+ // Fall through to REBUILDING, since state is now REBUILDING.
+ case REBUILDING:
+ // Nothing to be done.
+ break;
+ default:
+ throw new AssertionError(parent + " not in valid dirty state: " + parentEntry);
+ }
+ }
+ SkyFunctionEnvironment env =
+ new SkyFunctionEnvironment(parent, parentEntry.getTemporaryDirectDeps(),
+ bubbleErrorInfo, visitor);
+ externalInterrupt = externalInterrupt || Thread.currentThread().isInterrupted();
+ try {
+ // This build is only to check if the parent node can give us a better error. We don't
+ // care about a return value.
+ factory.compute(parent, env);
+ } catch (SkyFunctionException builderException) {
+ ReifiedSkyFunctionException reifiedBuilderException =
+ new ReifiedSkyFunctionException(builderException, parent);
+ if (reifiedBuilderException.getRootCauseSkyKey().equals(parent)) {
+ error = new ErrorInfo(reifiedBuilderException);
+ bubbleErrorInfo.put(errorKey,
+ ValueWithMetadata.error(new ErrorInfo(errorKey, ImmutableSet.of(error)),
+ env.buildEvents(/*missingChildren=*/true)));
+ continue;
+ }
+ } catch (InterruptedException interruptedException) {
+ // Do nothing.
+ // This throw happens if the builder requested the failed node, and then checked the
+ // interrupted state later -- getValueOrThrow sets the interrupted bit after the failed
+ // value is requested, to prevent the builder from doing too much work.
+ } finally {
+ // Clear interrupted status. We're not listening to interrupts here.
+ Thread.interrupted();
+ }
+ // Builder didn't throw an exception, so just propagate this one up.
+ bubbleErrorInfo.put(errorKey,
+ ValueWithMetadata.error(new ErrorInfo(errorKey, ImmutableSet.of(error)),
+ env.buildEvents(/*missingChildren=*/true)));
+ }
+
+ // Reset the interrupt bit if there was an interrupt from outside this evaluator interrupt.
+ // Note that there are internal interrupts set in the node builder environment if an error
+ // bubbling node calls getValueOrThrow() on a node in error.
+ if (externalInterrupt) {
+ Thread.currentThread().interrupt();
+ }
+ return bubbleErrorInfo;
+ }
+
+ /**
+ * Constructs an {@link EvaluationResult} from the {@link #graph}. Looks for cycles if there
+ * are unfinished nodes but no error was already found through bubbling up
+ * (as indicated by {@code bubbleErrorInfo} being null).
+ *
+ * <p>{@code visitor} may be null, but only in the case where all graph entries corresponding to
+ * {@code skyKeys} are known to be in the DONE state ({@code entry.isDone()} returns true).
+ */
+ private <T extends SkyValue> EvaluationResult<T> constructResult(
+ @Nullable ValueVisitor visitor, Iterable<SkyKey> skyKeys,
+ Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, boolean catastrophe) {
+ Preconditions.checkState(!keepGoing || catastrophe || bubbleErrorInfo == null,
+ "", skyKeys, bubbleErrorInfo);
+ EvaluationResult.Builder<T> result = EvaluationResult.builder();
+ List<SkyKey> cycleRoots = new ArrayList<>();
+ boolean hasError = false;
+ for (SkyKey skyKey : skyKeys) {
+ ValueWithMetadata valueWithMetadata = getValueMaybeFromError(skyKey, bubbleErrorInfo);
+ // Cycle checking: if there is a cycle, evaluation cannot progress, therefore,
+ // the final values will not be in DONE state when the work runs out.
+ if (valueWithMetadata == null) {
+ // Don't look for cycles if the build failed for a known reason.
+ if (bubbleErrorInfo == null) {
+ cycleRoots.add(skyKey);
+ }
+ hasError = true;
+ continue;
+ }
+ SkyValue value = valueWithMetadata.getValue();
+ // TODO(bazel-team): Verify that message replay is fast and works in failure
+ // modes [skyframe-core]
+ // Note that replaying events here is only necessary on null builds, because otherwise we
+ // would have already printed the transitive messages after building these values.
+ replayingNestedSetEventVisitor.visit(valueWithMetadata.getTransitiveEvents());
+ ErrorInfo errorInfo = valueWithMetadata.getErrorInfo();
+ Preconditions.checkState(value != null || errorInfo != null, skyKey);
+ hasError = hasError || (errorInfo != null);
+ if (!keepGoing && errorInfo != null) {
+ // value will be null here unless the value was already built on a prior keepGoing build.
+ result.addError(skyKey, errorInfo);
+ continue;
+ }
+ if (value == null) {
+ // Note that we must be in the keepGoing case. Only make this value an error if it doesn't
+ // have a value. The error shouldn't matter to the caller since the value succeeded after a
+ // fashion.
+ result.addError(skyKey, errorInfo);
+ } else {
+ result.addResult(skyKey, value);
+ }
+ }
+ if (!cycleRoots.isEmpty()) {
+ Preconditions.checkState(visitor != null, skyKeys);
+ checkForCycles(cycleRoots, result, visitor, keepGoing);
+ }
+ Preconditions.checkState(bubbleErrorInfo == null || hasError,
+ "If an error bubbled up, some top-level node must be in error", bubbleErrorInfo, skyKeys);
+ result.setHasError(hasError);
+ return result.build();
+ }
+
+ private <T extends SkyValue> void checkForCycles(
+ Iterable<SkyKey> badRoots, EvaluationResult.Builder<T> result, final ValueVisitor visitor,
+ boolean keepGoing) {
+ for (SkyKey root : badRoots) {
+ ErrorInfo errorInfo = checkForCycles(root, visitor, keepGoing);
+ if (errorInfo == null) {
+ // This node just wasn't finished when evaluation aborted -- there were no cycles below it.
+ Preconditions.checkState(!keepGoing, "", root, badRoots);
+ continue;
+ }
+ Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()),
+ "%s was not evaluated, but was not part of a cycle", root);
+ result.addError(root, errorInfo);
+ if (!keepGoing) {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Marker value that we push onto a stack before we push a node's children on. When the marker
+ * value is popped, we know that all the children are finished. We would use null instead, but
+ * ArrayDeque does not permit null elements.
+ */
+ private static final SkyKey CHILDREN_FINISHED =
+ new SkyKey(new SkyFunctionName("MARKER", false), "MARKER");
+
+ /** The max number of cycles we will report to the user for a given root, to avoid OOMing. */
+ private static final int MAX_CYCLES = 20;
+
+ /**
+ * The algorithm for this cycle detector is as follows. We visit the graph depth-first, keeping
+ * track of the path we are currently on. We skip any DONE nodes (they are transitively
+ * error-free). If we come to a node already on the path, we immediately construct a cycle. If
+ * we are in the noKeepGoing case, we return ErrorInfo with that cycle to the caller. Otherwise,
+ * we continue. Once all of a node's children are done, we construct an error value for it, based
+ * on those children. Finally, when the original root's node is constructed, we return its
+ * ErrorInfo.
+ */
+ private ErrorInfo checkForCycles(SkyKey root, ValueVisitor visitor, boolean keepGoing) {
+ // The number of cycles found. Do not keep on searching for more cycles after this many were
+ // found.
+ int cyclesFound = 0;
+ // The path through the graph currently being visited.
+ List<SkyKey> graphPath = new ArrayList<>();
+ // Set of nodes on the path, to avoid expensive searches through the path for cycles.
+ Set<SkyKey> pathSet = new HashSet<>();
+
+ // Maintain a stack explicitly instead of recursion to avoid stack overflows
+ // on extreme graphs (with long dependency chains).
+ Deque<SkyKey> toVisit = new ArrayDeque<>();
+
+ toVisit.push(root);
+
+ // The procedure for this check is as follows: we visit a node, push it onto the graph stack,
+ // push a marker value onto the toVisit stack, and then push all of its children onto the
+ // toVisit stack. Thus, when the marker node comes to the top of the toVisit stack, we have
+ // visited the downward transitive closure of the value. At that point, all of its children must
+ // be finished, and so we can build the definitive error info for the node, popping it off the
+ // graph stack.
+ while (!toVisit.isEmpty()) {
+ SkyKey key = toVisit.pop();
+ NodeEntry entry = graph.get(key);
+
+ if (key == CHILDREN_FINISHED) {
+ // A marker node means we are done with all children of a node. Since all nodes have
+ // errors, we must have found errors in the children when that happens.
+ key = graphPath.remove(graphPath.size() - 1);
+ entry = graph.get(key);
+ pathSet.remove(key);
+ // Skip this node if it was first/last node of a cycle, and so has already been processed.
+ if (entry.isDone()) {
+ continue;
+ }
+ if (!keepGoing) {
+ // in the --nokeep_going mode, we would have already returned if we'd found a cycle below
+ // this node. The fact that we haven't means that there were no cycles below this node
+ // -- it just hadn't finished evaluating. So skip it.
+ continue;
+ }
+ if (cyclesFound < MAX_CYCLES) {
+ // Value must be ready, because all of its children have finished, so we can build its
+ // error.
+ Preconditions.checkState(entry.isReady(), "%s not ready. ValueEntry: %s", key, entry);
+ } else if (!entry.isReady()) {
+ removeIncompleteChildrenForCycle(key, entry, entry.getTemporaryDirectDeps());
+ }
+ Set<SkyKey> directDeps = entry.getTemporaryDirectDeps();
+ // Find out which children have errors. Similar logic to that in Evaluate#run().
+ List<ErrorInfo> errorDeps = getChildrenErrorsForCycle(directDeps);
+ Preconditions.checkState(!errorDeps.isEmpty(),
+ "Value %s was not successfully evaluated, but had no child errors. ValueEntry: %s", key,
+ entry);
+ SkyFunctionEnvironment env = new SkyFunctionEnvironment(key, directDeps, visitor);
+ env.setError(new ErrorInfo(key, errorDeps));
+ env.commit(/*enqueueParents=*/false);
+ }
+
+ // Nothing to be done for this node if it already has an entry.
+ if (entry.isDone()) {
+ continue;
+ }
+ if (cyclesFound == MAX_CYCLES) {
+ // Do not keep on searching for cycles indefinitely, to avoid excessive runtime/OOMs.
+ continue;
+ }
+
+ if (pathSet.contains(key)) {
+ int cycleStart = graphPath.indexOf(key);
+ // Found a cycle!
+ cyclesFound++;
+ Iterable<SkyKey> cycle = graphPath.subList(cycleStart, graphPath.size());
+ // Put this node into a consistent state for building if it is dirty.
+ if (entry.isDirty() && entry.getDirtyState() == DirtyState.CHECK_DEPENDENCIES) {
+ // In the check deps state, entry has exactly one child not done yet. Note that this node
+ // must be part of the path to the cycle we have found (since done nodes cannot be in
+ // cycles, and this is the only missing one). Thus, it will not be removed below in
+ // removeDescendantsOfCycleValue, so it is safe here to signal that it is done.
+ entry.signalDep();
+ }
+ if (keepGoing) {
+ // Any children of this node that we haven't already visited are not worth visiting,
+ // since this node is about to be done. Thus, the only child worth visiting is the one in
+ // this cycle, the cycleChild (which may == key if this cycle is a self-edge).
+ SkyKey cycleChild = selectCycleChild(key, graphPath, cycleStart);
+ removeDescendantsOfCycleValue(key, entry, cycleChild, toVisit,
+ graphPath.size() - cycleStart);
+ ValueWithMetadata dummyValue = ValueWithMetadata.wrapWithMetadata(new SkyValue() {});
+
+
+ SkyFunctionEnvironment env =
+ new SkyFunctionEnvironment(key, entry.getTemporaryDirectDeps(),
+ ImmutableMap.of(cycleChild, dummyValue), visitor);
+
+ // Construct error info for this node. Get errors from children, which are all done
+ // except possibly for the cycleChild.
+ List<ErrorInfo> allErrors =
+ getChildrenErrors(entry.getTemporaryDirectDeps(), /*unfinishedChild=*/cycleChild);
+ CycleInfo cycleInfo = new CycleInfo(cycle);
+ // Add in this cycle.
+ allErrors.add(new ErrorInfo(cycleInfo));
+ env.setError(new ErrorInfo(key, allErrors));
+ env.commit(/*enqueueParents=*/false);
+ continue;
+ } else {
+ // We need to return right away in the noKeepGoing case, so construct the cycle (with the
+ // path) and return.
+ Preconditions.checkState(graphPath.get(0).equals(root),
+ "%s not reached from %s. ValueEntry: %s", key, root, entry);
+ return new ErrorInfo(new CycleInfo(graphPath.subList(0, cycleStart), cycle));
+ }
+ }
+
+ // This node is not yet known to be in a cycle. So process its children.
+ Iterable<? extends SkyKey> children = graph.get(key).getTemporaryDirectDeps();
+ if (Iterables.isEmpty(children)) {
+ continue;
+ }
+
+ // This marker flag will tell us when all this node's children have been processed.
+ toVisit.push(CHILDREN_FINISHED);
+ // This node is now part of the path through the graph.
+ graphPath.add(key);
+ pathSet.add(key);
+ for (SkyKey nextValue : children) {
+ toVisit.push(nextValue);
+ }
+ }
+ return keepGoing ? getAndCheckDone(root).getErrorInfo() : null;
+ }
+
+ /**
+ * Returns the child of this node that is in the cycle that was just found. If the cycle is a
+ * self-edge, returns the node itself.
+ */
+ private static SkyKey selectCycleChild(SkyKey key, List<SkyKey> graphPath, int cycleStart) {
+ return cycleStart + 1 == graphPath.size() ? key : graphPath.get(cycleStart + 1);
+ }
+
+ /**
+ * Get all the errors of child nodes. There must be at least one cycle amongst them.
+ *
+ * @param children child nodes to query for errors.
+ * @return List of ErrorInfos from all children that had errors.
+ */
+ private List<ErrorInfo> getChildrenErrorsForCycle(Iterable<SkyKey> children) {
+ List<ErrorInfo> allErrors = new ArrayList<>();
+ boolean foundCycle = false;
+ for (SkyKey child : children) {
+ ErrorInfo errorInfo = getAndCheckDone(child).getErrorInfo();
+ if (errorInfo != null) {
+ foundCycle |= !Iterables.isEmpty(errorInfo.getCycleInfo());
+ allErrors.add(errorInfo);
+ }
+ }
+ Preconditions.checkState(foundCycle, "", children, allErrors);
+ return allErrors;
+ }
+
+ /**
+ * Get all the errors of child nodes.
+ *
+ * @param children child nodes to query for errors.
+ * @param unfinishedChild child which is allowed to not be done.
+ * @return List of ErrorInfos from all children that had errors.
+ */
+ private List<ErrorInfo> getChildrenErrors(Iterable<SkyKey> children, SkyKey unfinishedChild) {
+ List<ErrorInfo> allErrors = new ArrayList<>();
+ for (SkyKey child : children) {
+ ErrorInfo errorInfo = getErrorMaybe(child, /*allowUnfinished=*/child.equals(unfinishedChild));
+ if (errorInfo != null) {
+ allErrors.add(errorInfo);
+ }
+ }
+ return allErrors;
+ }
+
+ @Nullable
+ private ErrorInfo getErrorMaybe(SkyKey key, boolean allowUnfinished) {
+ if (!allowUnfinished) {
+ return getAndCheckDone(key).getErrorInfo();
+ }
+ NodeEntry entry = Preconditions.checkNotNull(graph.get(key), key);
+ return entry.isDone() ? entry.getErrorInfo() : null;
+ }
+
+ /**
+ * Removes direct children of key from toVisit and from the entry itself, and makes the entry
+ * ready if necessary. We must do this because it would not make sense to try to build the
+ * children after building the entry. It would violate the invariant that a parent can only be
+ * built after its children are built; See bug "Precondition error while evaluating a Skyframe
+ * graph with a cycle".
+ *
+ * @param key SkyKey of node in a cycle.
+ * @param entry NodeEntry of node in a cycle.
+ * @param cycleChild direct child of key in the cycle, or key itself if the cycle is a self-edge.
+ * @param toVisit list of remaining nodes to visit by the cycle-checker.
+ * @param cycleLength the length of the cycle found.
+ */
+ private void removeDescendantsOfCycleValue(SkyKey key, NodeEntry entry,
+ @Nullable SkyKey cycleChild, Iterable<SkyKey> toVisit, int cycleLength) {
+ Set<SkyKey> unvisitedDeps = new HashSet<>(entry.getTemporaryDirectDeps());
+ unvisitedDeps.remove(cycleChild);
+ // Remove any children from this node that are not part of the cycle we just found. They are
+ // irrelevant to the node as it stands, and if they are deleted from the graph because they are
+ // not built by the end of cycle-checking, we would have dangling references.
+ removeIncompleteChildrenForCycle(key, entry, unvisitedDeps);
+ if (!entry.isReady()) {
+ // The entry has at most one undone dep now, its cycleChild. Signal to make entry ready. Note
+ // that the entry can conceivably be ready if its cycleChild already found a different cycle
+ // and was built.
+ entry.signalDep();
+ }
+ Preconditions.checkState(entry.isReady(), "%s %s %s", key, cycleChild, entry);
+ Iterator<SkyKey> it = toVisit.iterator();
+ while (it.hasNext()) {
+ SkyKey descendant = it.next();
+ if (descendant == CHILDREN_FINISHED) {
+ // Marker value, delineating the end of a group of children that were enqueued.
+ cycleLength--;
+ if (cycleLength == 0) {
+ // We have seen #cycleLength-1 marker values, and have arrived at the one for this value,
+ // so we are done.
+ return;
+ }
+ continue; // Don't remove marker values.
+ }
+ if (cycleLength == 1) {
+ // Remove the direct children remaining to visit of the cycle node.
+ Preconditions.checkState(unvisitedDeps.contains(descendant),
+ "%s %s %s %s %s", key, descendant, cycleChild, unvisitedDeps, entry);
+ it.remove();
+ }
+ }
+ throw new IllegalStateException("There were not " + cycleLength + " groups of children in "
+ + toVisit + " when trying to remove children of " + key + " other than " + cycleChild);
+ }
+
+ private void removeIncompleteChildrenForCycle(SkyKey key, NodeEntry entry,
+ Iterable<SkyKey> children) {
+ Set<SkyKey> unfinishedDeps = new HashSet<>();
+ for (SkyKey child : children) {
+ if (removeIncompleteChild(key, child)) {
+ unfinishedDeps.add(child);
+ }
+ }
+ entry.removeUnfinishedDeps(unfinishedDeps);
+ }
+
+ private NodeEntry getAndCheckDone(SkyKey key) {
+ NodeEntry entry = graph.get(key);
+ Preconditions.checkNotNull(entry, key);
+ Preconditions.checkState(entry.isDone(), "%s %s", key, entry);
+ return entry;
+ }
+
+ private ValueWithMetadata getValueMaybeFromError(SkyKey key,
+ @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo) {
+ SkyValue value = bubbleErrorInfo == null ? null : bubbleErrorInfo.get(key);
+ NodeEntry entry = graph.get(key);
+ if (value != null) {
+ Preconditions.checkNotNull(entry,
+ "Value cannot have error before evaluation started", key, value);
+ return ValueWithMetadata.wrapWithMetadata(value);
+ }
+ return isDoneForBuild(entry) ? entry.getValueWithMetadata() : null;
+ }
+
+ /**
+ * Return true if the entry does not need to be re-evaluated this build. The entry will need to
+ * be re-evaluated if it is not done, but also if it was not completely evaluated last build and
+ * this build is keepGoing.
+ */
+ private boolean isDoneForBuild(@Nullable NodeEntry entry) {
+ return entry != null && entry.isDone();
+ }
+}