blob: dd2b86be9862170095c99fb2dc99cc95364f049e [file] [log] [blame]
// 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.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.bugreport.BugReport;
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.concurrent.QuiescingExecutor;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.Reportable;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
import com.google.devtools.build.skyframe.QueryableGraph.LookupHint;
import com.google.devtools.build.skyframe.QueryableGraph.Reason;
import com.google.devtools.build.skyframe.proto.GraphInconsistency.Inconsistency;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.function.Supplier;
import javax.annotation.Nullable;
/**
* A {@link SkyFunction.Environment} implementation for {@link ParallelEvaluator}.
*
* <p>The base {@link SkyFunctionEnvironment} class batch prefetches previously requested deps
* during environment creation.
*
* <p>The {@link SkipsBatchPrefetch} subclass skips batch prefetching, so that it is more efficient
* to create the environment when the number of previously requested deps is extremely large.
*/
// TODO: b/324948927 - Instead of having individual `SkyKey`s overriding the `skipsBatchPrefetch`
// method, some method similar to QueryableGraph#getLookupHint() when creating the environment to
// know whether batch prefetch should happen.
public class SkyFunctionEnvironment extends AbstractSkyFunctionEnvironment
implements SkyframeLookupResult, ExtendedEventHandler {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final SkyValue NULL_MARKER = new SkyValue() {};
private static final SkyValue PENDING_MARKER = new SkyValue() {};
private static final SkyValue MANUALLY_REGISTERED_MARKER = new SkyValue() {};
private boolean building = true;
private SkyKey depErrorKey = null;
private final SkyKey skyKey;
private final GroupedDeps previouslyRequestedDeps;
/**
* 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<SkyKey> oldDeps;
private SkyValue value = null;
private ErrorInfo errorInfo = null;
@Nullable private Version maxTransitiveSourceVersion;
/**
* This is not {@code null} only during cycle detection and error bubbling. The nullness of this
* field is used to detect whether evaluation is in one of those special states.
*
* <p>When this is not {@code null}, values in this map should be used (while getting
* dependencies' values, events, or posts) over values from the graph for keys present in this
* map.
*/
@Nullable private final Map<SkyKey, ValueWithMetadata> bubbleErrorInfo;
private boolean encounteredErrorDuringBubbling = false;
/**
* The values previously declared as dependencies during an earlier {@link SkyFunction#compute}
* call for {@link #skyKey}.
*
* <p>Values in this map were generally retrieved via {@link NodeEntry#getValueMaybeWithMetadata}
* from done nodes. In some cases, values may be {@link #NULL_MARKER} (see {@link #batchPrefetch}
* for more details).
*
* <p>In {@link SkipsBatchPrefetch}, this map is not exhaustive. It populates as the {@link
* SkyFunction} re-requests dep values, and will contain {@link #PENDING_MARKER}s when a key is
* about to be requested from the graph.
*/
private final Map<SkyKey, SkyValue> previouslyRequestedDepsValues;
/**
* The values newly requested from the graph during the {@link SkyFunction#compute} call for this
* environment.
*
* <p>Values in this map were either retrieved via {@link NodeEntry#getValueMaybeWithMetadata} or
* are one of the following special marker values:
*
* <ol>
* <li>{@link #NULL_MARKER}: The key was already requested from the graph but was either not
* present or not done.
* <li>{@link #PENDING_MARKER}: The key is about to be requested from the graph. This is a
* placeholder to detect duplicate keys in the same batch. It will be overwritten with
* either {@link #NULL_MARKER} or a value once it is requested.
* <li>{@link #MANUALLY_REGISTERED_MARKER}: The key was manually registered via {@link
* #registerDependencies} and has not been otherwise requested. Such keys are assumed to be
* done.
* </ol>
*
* <p>This map is ordered to preserve dep groups. The sizes of each group are stored in {@link
* #newlyRequestedDepGroupSizes}. On a subsequent build, if the value is dirty, all deps in the
* same 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
* SkyFunction.Environment#getValuesAndExceptions} for more.
*
* <p>Keys in this map are disjoint with {@link #previouslyRequestedDepsValues}. This map may
* contain entries from {@link #bubbleErrorInfo} if they were requested.
*/
private final Map<SkyKey, SkyValue> newlyRequestedDepsValues = new LinkedHashMap<>();
/** Size delimiters for dep groups in {@link #newlyRequestedDepsValues}. */
private final List<Integer> newlyRequestedDepGroupSizes = new ArrayList<>();
/** The set of errors encountered while fetching children. */
private final Set<ErrorInfo> childErrorInfos = new LinkedHashSet<>();
private final ParallelEvaluatorContext evaluatorContext;
private final List<Reportable> eventsToReport = new ArrayList<>();
static SkyFunctionEnvironment create(
SkyKey skyKey,
GroupedDeps previouslyRequestedDeps,
Set<SkyKey> oldDeps,
@Nullable Version maxTransitiveSourceVersionSoFar,
ParallelEvaluatorContext evaluatorContext)
throws InterruptedException, UndonePreviouslyRequestedDeps {
Version maxTransitiveSourceVersion =
skyKey.functionName().getHermeticity() != FunctionHermeticity.NONHERMETIC
? firstNonNull(maxTransitiveSourceVersionSoFar, evaluatorContext.getMinimalVersion())
: null;
return skyKey.skipsBatchPrefetch()
? new SkyFunctionEnvironment.SkipsBatchPrefetch(
skyKey, previouslyRequestedDeps, oldDeps, evaluatorContext, maxTransitiveSourceVersion)
: new SkyFunctionEnvironment(
skyKey,
previouslyRequestedDeps,
/* bubbleErrorInfo= */ null,
oldDeps,
evaluatorContext,
/* throwIfPreviouslyRequestedDepsUndone= */ true,
maxTransitiveSourceVersion);
}
static SkyFunctionEnvironment createForError(
SkyKey skyKey,
GroupedDeps previouslyRequestedDeps,
Map<SkyKey, ValueWithMetadata> bubbleErrorInfo,
Set<SkyKey> oldDeps,
ParallelEvaluatorContext evaluatorContext)
throws InterruptedException {
try {
return new SkyFunctionEnvironment(
skyKey,
previouslyRequestedDeps,
checkNotNull(bubbleErrorInfo),
oldDeps,
evaluatorContext,
/* throwIfPreviouslyRequestedDepsUndone= */ false,
// Cycles can lead to a state where the versions of done children don't accurately reflect
// the state that led to this node's value. Be conservative then.
/* maxTransitiveSourceVersion= */ null);
} catch (UndonePreviouslyRequestedDeps undonePreviouslyRequestedDeps) {
throw new IllegalStateException(undonePreviouslyRequestedDeps);
}
}
private SkyFunctionEnvironment(
SkyKey skyKey,
GroupedDeps previouslyRequestedDeps,
@Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo,
Set<SkyKey> oldDeps,
ParallelEvaluatorContext evaluatorContext,
boolean throwIfPreviouslyRequestedDepsUndone,
@Nullable Version maxTransitiveSourceVersion)
throws UndonePreviouslyRequestedDeps, InterruptedException {
this.skyKey = checkNotNull(skyKey);
this.previouslyRequestedDeps = checkNotNull(previouslyRequestedDeps);
this.bubbleErrorInfo = bubbleErrorInfo;
this.oldDeps = checkNotNull(oldDeps);
this.evaluatorContext = checkNotNull(evaluatorContext);
this.maxTransitiveSourceVersion = maxTransitiveSourceVersion;
this.previouslyRequestedDepsValues = batchPrefetch(throwIfPreviouslyRequestedDepsUndone);
}
@ForOverride
Map<SkyKey, SkyValue> batchPrefetch(boolean throwIfPreviouslyRequestedDepsUndone)
throws InterruptedException, UndonePreviouslyRequestedDeps {
ImmutableSet<SkyKey> excludedKeys =
evaluatorContext.getGraph().prefetchDeps(skyKey, oldDeps, previouslyRequestedDeps);
Collection<SkyKey> keysToPrefetch =
excludedKeys != null ? excludedKeys : previouslyRequestedDeps.getAllElementsAsIterable();
NodeBatch batch = evaluatorContext.getGraph().getBatch(skyKey, Reason.PREFETCH, keysToPrefetch);
ImmutableMap.Builder<SkyKey, SkyValue> depValuesBuilder =
ImmutableMap.builderWithExpectedSize(keysToPrefetch.size());
ImmutableList.Builder<SkyKey> missingRequestedDeps = null;
for (SkyKey depKey : keysToPrefetch) {
NodeEntry entry = batch.get(depKey);
if (entry == null) {
if (missingRequestedDeps == null) {
missingRequestedDeps = ImmutableList.builder();
}
missingRequestedDeps.add(depKey);
continue;
}
SkyValue valueMaybeWithMetadata = entry.getValueMaybeWithMetadata();
boolean depDone = valueMaybeWithMetadata != null;
if (throwIfPreviouslyRequestedDepsUndone && !depDone) {
// A previously requested dep may have transitioned from done to dirty between when the node
// was read during a previous attempt to build this node and now. Notify the graph
// inconsistency receiver so that we can crash if that's unexpected.
evaluatorContext
.getGraphInconsistencyReceiver()
.noteInconsistencyAndMaybeThrow(
skyKey, ImmutableList.of(depKey), Inconsistency.BUILDING_PARENT_FOUND_UNDONE_CHILD);
throw new UndonePreviouslyRequestedDeps(ImmutableList.of(depKey));
}
depValuesBuilder.put(depKey, !depDone ? NULL_MARKER : valueMaybeWithMetadata);
if (depDone) {
maybeUpdateMaxTransitiveSourceVersion(entry);
}
}
if (missingRequestedDeps != null) {
// Notify `GraphInconsistencyReceiver` when there are some dependencies missing from the graph
// to check whether this is expected.
ImmutableList<SkyKey> allMissingDeps = missingRequestedDeps.build();
evaluatorContext
.getGraphInconsistencyReceiver()
.noteInconsistencyAndMaybeThrow(
skyKey, allMissingDeps, Inconsistency.ALREADY_DECLARED_CHILD_MISSING);
throw new UndonePreviouslyRequestedDeps(allMissingDeps);
}
ImmutableMap<SkyKey, SkyValue> prefetched = depValuesBuilder.buildOrThrow();
checkState(
!prefetched.containsKey(ErrorTransienceValue.KEY),
"%s cannot have a dep on ErrorTransienceValue during building",
skyKey);
return prefetched;
}
private void checkActive() {
checkState(building, skyKey);
}
/**
* Reports events which were temporarily stored in this environment per the specification of
* {@link SkyFunction.Environment#getListener}. Returns events that should be stored for potential
* replay on a future evaluation.
*/
NestedSet<Reportable> reportEventsAndGetEventsToStore(NodeEntry entry, boolean expectDoneDeps)
throws InterruptedException {
EventFilter eventFilter = evaluatorContext.getStoredEventFilter();
if (!eventFilter.storeEvents()) {
if (!eventsToReport.isEmpty()) {
String tag = getTagFromKey();
for (Reportable event : eventsToReport) {
event.withTag(tag).reportTo(evaluatorContext.getReporter());
}
}
return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
}
GroupedDeps depKeys = entry.getTemporaryDirectDeps();
if (eventsToReport.isEmpty() && depKeys.isEmpty()) {
return NestedSetBuilder.emptySet(Order.STABLE_ORDER);
}
NestedSetBuilder<Reportable> eventBuilder = NestedSetBuilder.stableOrder();
if (!eventsToReport.isEmpty()) {
String tag = getTagFromKey();
eventBuilder.addAll(Lists.transform(eventsToReport, event -> event.withTag(tag)));
}
addTransitiveEventsFromDepValuesForDoneNode(
eventBuilder,
Iterables.filter(
depKeys.getAllElementsAsIterable(),
depKey -> eventFilter.shouldPropagate(depKey, skyKey)),
expectDoneDeps);
NestedSet<Reportable> events = eventBuilder.buildInterruptibly();
evaluatorContext.getReplayingNestedSetEventVisitor().visit(events);
return events;
}
/**
* Adds transitive events from done deps in {@code depKeys}, by looking in order at:
*
* <ol>
* <li>{@link #bubbleErrorInfo}
* <li>{@link #previouslyRequestedDepsValues}
* <li>{@link #newlyRequestedDepsValues}
* <li>{@link #evaluatorContext}'s graph accessing methods
* </ol>
*
* <p>Any key whose {@link NodeEntry}--or absence thereof--had to be read from the graph will also
* be entered into {@link #newlyRequestedDepsValues} with its value or a {@link #NULL_MARKER}.
*
* <p>This asserts that only keys manually registered via {@link #registerDependencies} require
* reading from the graph, because this node is done, and so all other deps must have been
* previously or newly requested.
*
* <p>If {@code assertDone}, this asserts that all deps in {@code depKeys} are done.
*/
private void addTransitiveEventsFromDepValuesForDoneNode(
NestedSetBuilder<Reportable> eventBuilder, Iterable<SkyKey> depKeys, boolean assertDone)
throws InterruptedException {
// depKeys may contain keys in newlyRegisteredDeps whose values have not yet been retrieved from
// the graph during this environment's lifetime.
List<SkyKey> missingKeys = null;
for (SkyKey key : depKeys) {
SkyValue value = maybeGetValueFromErrorOrDeps(key);
if (value == null) {
if (key == ErrorTransienceValue.KEY) {
continue;
}
checkState(
newlyRequestedDepsValues.get(key) == MANUALLY_REGISTERED_MARKER,
"Missing already declared dep %s (parent=%s)",
key,
skyKey);
if (missingKeys == null) {
missingKeys = new ArrayList<>();
}
missingKeys.add(key);
} else if (value == NULL_MARKER) {
checkState(!assertDone, "%s had not done %s", skyKey, key);
} else {
eventBuilder.addTransitive(ValueWithMetadata.getEvents(value));
}
}
if (missingKeys == null) {
return;
}
NodeBatch missingEntries =
evaluatorContext.getGraph().getBatch(skyKey, Reason.DEP_REQUESTED, missingKeys);
for (SkyKey key : missingKeys) {
NodeEntry depEntry = missingEntries.get(key);
SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry);
newlyRequestedDepsValues.put(key, valueOrNullMarker);
if (valueOrNullMarker == NULL_MARKER) {
// TODO(mschaller): handle registered deps that transitioned from done to dirty during eval
// But how? Resetting the current node may not help, because this dep was *registered*, not
// requested. For now, no node that gets registered as a dep is eligible for
// intra-evaluation dirtying, so let it crash.
checkState(!assertDone, "%s had not done: %s", skyKey, key);
continue;
}
maybeUpdateMaxTransitiveSourceVersion(depEntry);
eventBuilder.addTransitive(ValueWithMetadata.getEvents(valueOrNullMarker));
}
}
void setValue(SkyValue newValue) {
checkState(
errorInfo == null && bubbleErrorInfo == null,
"%s %s %s %s",
skyKey,
newValue,
errorInfo,
bubbleErrorInfo);
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.
*/
void setError(NodeEntry state, ErrorInfo errorInfo) throws InterruptedException {
checkState(value == null, "%s %s %s", skyKey, value, errorInfo);
checkState(this.errorInfo == null, "%s %s %s", skyKey, this.errorInfo, errorInfo);
if (errorInfo.isDirectlyTransient()) {
NodeEntry errorTransienceNode =
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);
}
checkState(triState == DependencyState.DONE, "%s %s %s", skyKey, triState, errorInfo);
state.addSingletonTemporaryDirectDep(ErrorTransienceValue.KEY);
state.signalDep(evaluatorContext.getGraphVersion(), ErrorTransienceValue.KEY);
maxTransitiveSourceVersion = null;
}
this.errorInfo = checkNotNull(errorInfo, skyKey);
}
/**
* Returns a value, {@code null}, or {@link #NULL_MARKER} for the given key by looking in order
* at:
*
* <ol>
* <li>{@link #bubbleErrorInfo}
* <li>{@link #previouslyRequestedDepsValues}
* <li>{@link #newlyRequestedDepsValues}
* </ol>
*
* <p>Returns {@code null} if no entries for {@code key} were found in any of those three maps, or
* if the key was manually registered via {@link #registerDependencies} but never requested.
*/
@Nullable
SkyValue maybeGetValueFromErrorOrDeps(SkyKey key) {
if (bubbleErrorInfo != null) {
ValueWithMetadata bubbleErrorInfoValue = bubbleErrorInfo.get(key);
if (bubbleErrorInfoValue != null) {
return bubbleErrorInfoValue;
}
}
SkyValue directDepsValue = getPreviouslyRequestedDepValue(key);
if (directDepsValue != null) {
return directDepsValue;
}
directDepsValue = newlyRequestedDepsValues.get(key);
return directDepsValue == MANUALLY_REGISTERED_MARKER ? null : directDepsValue;
}
/**
* Gets the value of previously requested dep from either the env-scoped map or the {@link
* #evaluatorContext}'s graph.
*
* <p>In {@link SkipsBatchPrefetch}, since previously requested deps values are not available
* after environment creation, so it needs to query the {@link #evaluatorContext}'s graph on
* demand.
*/
@Nullable
@ForOverride
SkyValue getPreviouslyRequestedDepValue(SkyKey key) {
return previouslyRequestedDepsValues.get(key);
}
@ForOverride
@Nullable
SkyValue lookupRequestedDep(SkyKey depKey) {
checkArgument(
!depKey.equals(ErrorTransienceValue.KEY),
"Error transience key cannot be in requested deps of %s",
skyKey);
if (bubbleErrorInfo != null) {
ValueWithMetadata bubbleErrorInfoValue = bubbleErrorInfo.get(depKey);
if (bubbleErrorInfoValue != null) {
newlyRequestedDepsValues.put(depKey, bubbleErrorInfoValue);
return bubbleErrorInfoValue;
}
}
SkyValue directDepsValue = previouslyRequestedDepsValues.get(depKey);
if (directDepsValue != null) {
return directDepsValue;
}
directDepsValue = newlyRequestedDepsValues.putIfAbsent(depKey, PENDING_MARKER);
return directDepsValue == MANUALLY_REGISTERED_MARKER ? null : directDepsValue;
}
private void endDepGroup(int sizeBeforeRequest) {
int newDeps = newlyRequestedDepsValues.size() - sizeBeforeRequest;
if (newDeps > 0) {
newlyRequestedDepGroupSizes.add(newDeps);
}
}
private static SkyValue getValueOrNullMarker(@Nullable NodeEntry nodeEntry)
throws InterruptedException {
if (nodeEntry == null) {
return NULL_MARKER;
}
SkyValue valueMaybeWithMetadata = nodeEntry.getValueMaybeWithMetadata();
if (valueMaybeWithMetadata == null) {
return NULL_MARKER;
}
return valueMaybeWithMetadata;
}
@Nullable
@Override
<E1 extends Exception, E2 extends Exception, E3 extends Exception, E4 extends Exception>
SkyValue getValueOrThrowInternal(
SkyKey depKey,
@Nullable Class<E1> exceptionClass1,
@Nullable Class<E2> exceptionClass2,
@Nullable Class<E3> exceptionClass3,
@Nullable Class<E4> exceptionClass4)
throws E1, E2, E3, E4, InterruptedException {
checkActive();
int sizeBeforeRequest = newlyRequestedDepsValues.size();
SkyValue depValue = lookupRequestedDep(depKey);
if (depValue != null) {
processDepValue(depKey, depValue);
} else {
NodeEntry depEntry = evaluatorContext.getGraph().get(skyKey, Reason.DEP_REQUESTED, depKey);
depValue = processDepEntry(depKey, depEntry);
}
endDepGroup(sizeBeforeRequest);
return unwrapOrThrow(
depKey, depValue, exceptionClass1, exceptionClass2, exceptionClass3, exceptionClass4);
}
@CanIgnoreReturnValue
@Override
public SkyframeLookupResult getValuesAndExceptions(Iterable<? extends SkyKey> depKeys)
throws InterruptedException {
checkActive();
// Lazily initialized when we encounter a missing key and the graph's lookup hint indicates that
// the key should be requested in a batch. If the graph supports efficient lookups of individual
// keys, we avoid constructing a list.
List<SkyKey> missingKeys = null;
int sizeBeforeRequest = newlyRequestedDepsValues.size();
for (SkyKey depKey : depKeys) {
SkyValue value = lookupRequestedDep(depKey);
if (value == PENDING_MARKER) {
continue; // Duplicate key in this request.
}
if (value != null) {
processDepValue(depKey, value);
} else if (evaluatorContext.getGraph().getLookupHint(depKey) == LookupHint.BATCH) {
if (missingKeys == null) {
missingKeys = new ArrayList<>();
}
missingKeys.add(depKey);
} else {
NodeEntry depEntry = evaluatorContext.getGraph().get(skyKey, Reason.DEP_REQUESTED, depKey);
processDepEntry(depKey, depEntry);
}
}
endDepGroup(sizeBeforeRequest);
if (missingKeys != null) {
NodeBatch missingEntries =
evaluatorContext.getGraph().getBatch(skyKey, Reason.DEP_REQUESTED, missingKeys);
for (SkyKey key : missingKeys) {
processDepEntry(key, missingEntries.get(key));
}
}
return this;
}
@ForOverride
@CanIgnoreReturnValue
SkyValue processDepEntry(SkyKey depKey, @Nullable NodeEntry depEntry)
throws InterruptedException {
SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry);
processDepValue(depKey, valueOrNullMarker);
newlyRequestedDepsValues.put(depKey, valueOrNullMarker);
if (valueOrNullMarker != NULL_MARKER) {
maybeUpdateMaxTransitiveSourceVersion(depEntry);
}
return valueOrNullMarker;
}
void processDepValue(SkyKey depKey, SkyValue depValue) {
if (depValue == NULL_MARKER) {
valuesMissing = true;
return;
}
ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(depValue);
if (errorInfo == null) {
return;
}
childErrorInfos.add(errorInfo);
if (bubbleErrorInfo != null) {
encounteredErrorDuringBubbling = true;
// 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 we get here, then the dep node is present and also (i) depends on a cycle or (ii) errorful
// or (iii) both. The remaining question is whether or not to convey to the SkyFunction that the
// dep node is missing.
//
// If the dep node depends on a cycle, then we always want the SkyFunction to act as though the
// dep is missing (cycles are not supposed to be observable by SkyFunctions), so we always set
// valuesMissing.
//
// If the dep node is errorful and we're in nokeep_going mode and not in error bubbling, then
// the SkyFunction is not supposed to be able to observe the error and is supposed to act like
// the dep is missing, so we set valuesMissing. In contrast, if we are in error bubbling, then
// the SkyFunction is supposed to be able to observe the error (so as to have the chance to
// produce an enriched error).
//
// If the dep node is errorful and we're in keep_going mode, then SkyFunction is supposed to be
// able to observe the error (say, with a followup SkyframeLookupResult#getOrThrow) so we don't
// set valuesMissing.
if (!errorInfo.getCycleInfo().isEmpty()
|| (errorInfo.getException() != null
&& !evaluatorContext.keepGoing()
&& bubbleErrorInfo == null)) {
valuesMissing = true;
// We arbitrarily record the first child error if we are about to abort.
if (!evaluatorContext.keepGoing() && depErrorKey == null) {
depErrorKey = depKey;
}
}
}
@Nullable
@Override // SkyframeLookupResult implementation.
public <E1 extends Exception, E2 extends Exception, E3 extends Exception> SkyValue getOrThrow(
SkyKey depKey,
@Nullable Class<E1> exceptionClass1,
@Nullable Class<E2> exceptionClass2,
@Nullable Class<E3> exceptionClass3)
throws E1, E2, E3 {
return unwrapOrThrow(
depKey,
maybeGetValueFromErrorOrDeps(depKey),
exceptionClass1,
exceptionClass2,
exceptionClass3,
null);
}
@Override // SkyframeLookupResult implementation.
public boolean queryDep(SkyKey depKey, QueryDepCallback resultCallback) {
SkyValue maybeWrappedValue = maybeGetValueFromErrorOrDeps(depKey);
if (maybeWrappedValue == null) {
BugReport.sendNonFatalBugReport(
new IllegalStateException(
String.format("Value for %s was missing, this should never happen", depKey)));
return false;
}
if (maybeWrappedValue == NULL_MARKER) {
valuesMissing = true;
return false;
}
if (!(maybeWrappedValue instanceof ValueWithMetadata wrappedValue)) {
resultCallback.acceptValue(depKey, maybeWrappedValue);
return true;
}
if (!wrappedValue.hasError()) {
resultCallback.acceptValue(depKey, wrappedValue.getValue());
return true;
}
// Otherwise, there's an error.
@Nullable Object result = handleError(depKey, wrappedValue);
if (result instanceof SkyValue skyValue) {
resultCallback.acceptValue(depKey, skyValue);
return true;
}
if (result instanceof Exception exception
&& resultCallback.tryHandleException(depKey, exception)) {
return true;
}
valuesMissing = true;
return false;
}
@Nullable
private <E1 extends Exception, E2 extends Exception, E3 extends Exception, E4 extends Exception>
SkyValue unwrapOrThrow(
SkyKey depKey,
SkyValue maybeWrappedValue,
@Nullable Class<E1> exceptionClass1,
@Nullable Class<E2> exceptionClass2,
@Nullable Class<E3> exceptionClass3,
@Nullable Class<E4> exceptionClass4)
throws E1, E2, E3, E4 {
if (maybeWrappedValue == null) {
BugReport.sendNonFatalBugReport(
new IllegalStateException(
String.format("Value for %s was missing, this should never happen", depKey)));
return null;
}
if (maybeWrappedValue == NULL_MARKER) {
valuesMissing = true;
return null;
}
if (!(maybeWrappedValue instanceof ValueWithMetadata wrappedValue)) {
return maybeWrappedValue;
}
if (!wrappedValue.hasError()) {
return wrappedValue.getValue();
}
// Otherwise, there's an error.
@Nullable Object result = handleError(depKey, wrappedValue);
if (result instanceof SkyValue skyValue) {
return skyValue;
}
if (result instanceof Exception) {
SkyFunctionException.throwIfInstanceOf(
(Exception) result, exceptionClass1, exceptionClass2, exceptionClass3, exceptionClass4);
}
valuesMissing = true;
return null;
}
/**
* Processes wrapped values containing errors.
*
* @param depKey the dependency key, used only for error messages.
* @param wrappedError an instance of ValueWithMetadata containing an error.
* @return A {@code SkyValue} when a value is available in keepGoing mode, an {@code Exception}
* when one should be propagated or null otherwise.
*/
@Nullable
private Object handleError(SkyKey depKey, ValueWithMetadata wrappedError) {
if (evaluatorContext.keepGoing()) {
// In keepGoing mode, returns any computed value to the caller.
SkyValue justValue = wrappedError.getValue();
if (justValue != null) {
return justValue;
}
}
ErrorInfo errorInfo = wrappedError.getErrorInfo();
@Nullable Exception exception = errorInfo.getException();
if (exception == null) {
// If there's no exception, there must be a cycle.
checkState(
!errorInfo.getCycleInfo().isEmpty(),
"%s %s %s %s",
skyKey,
depKey,
errorInfo,
wrappedError);
} else if (evaluatorContext.keepGoing() || bubbleErrorInfo != null) {
// The exception may only propagate in keepGoing mode or during error bubbling.
return exception;
}
return null;
}
/**
* 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 this;
}
@Override
public GroupedDeps getTemporaryDirectDeps() {
return previouslyRequestedDeps;
}
@Override
public void handle(Event event) {
if (event.getKind() == EventKind.WARNING) {
event = event.withTag(getTagFromKey());
if (!evaluatorContext.getEmittedEventState().addWarning(event)) {
return; // Duplicate warning.
}
}
reportEvent(event);
}
@Override
public void post(Postable obj) {
reportEvent(obj);
}
private void reportEvent(Reportable event) {
checkActive();
if (event.storeForReplay()) {
eventsToReport.add(event);
} else {
event.reportTo(evaluatorContext.getReporter());
}
}
void doneBuilding() {
building = false;
}
Set<SkyKey> getNewlyRequestedDeps() {
return newlyRequestedDepsValues.keySet();
}
/** Adds newly requested dep keys to the node's temporary direct deps. */
void addTemporaryDirectDepsTo(NodeEntry entry) {
entry.addTemporaryDirectDepsInGroups(
newlyRequestedDepsValues.keySet(), newlyRequestedDepGroupSizes);
}
void removeUndoneNewlyRequestedDeps() {
if (!valuesMissing) {
return;
}
Iterator<SkyValue> it = newlyRequestedDepsValues.values().iterator();
for (int i = 0; i < newlyRequestedDepGroupSizes.size(); i++) {
int groupSize = newlyRequestedDepGroupSizes.get(i);
int newGroupSize = groupSize;
for (int j = 0; j < groupSize; j++) {
if (it.next() == NULL_MARKER) {
it.remove();
newGroupSize--;
}
}
newlyRequestedDepGroupSizes.set(i, newGroupSize);
}
}
boolean isAnyDirectDepErrorTransitivelyTransient() {
checkState(
bubbleErrorInfo == null,
"Checking dep error transitive transience during error bubbling for: %s",
skyKey);
for (SkyValue skyValue : previouslyRequestedDepsValues.values()) {
ErrorInfo maybeErrorInfo = ValueWithMetadata.getMaybeErrorInfo(skyValue);
if (maybeErrorInfo != null && maybeErrorInfo.isTransitivelyTransient()) {
return true;
}
}
return false;
}
boolean isAnyNewlyRequestedDepErrorTransitivelyTransient() {
checkState(
bubbleErrorInfo == null,
"Checking dep error transitive transience during error bubbling for: %s",
skyKey);
for (SkyValue skyValue : newlyRequestedDepsValues.values()) {
ErrorInfo maybeErrorInfo = ValueWithMetadata.getMaybeErrorInfo(skyValue);
if (maybeErrorInfo != null && maybeErrorInfo.isTransitivelyTransient()) {
return true;
}
}
return false;
}
Set<ErrorInfo> getChildErrorInfos() {
return childErrorInfos;
}
/**
* Applies the change to the graph (mostly) atomically and returns parents to potentially signal
* and enqueue.
*
* <p>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 {@code --nokeep_going}, and so we are about to
* shut down the main evaluation anyway.
*/
Set<SkyKey> commitAndGetParents(NodeEntry primaryEntry) throws InterruptedException {
// Construct the definitive error info, if there is one.
if (errorInfo == null) {
errorInfo =
evaluatorContext
.getErrorInfoManager()
.getErrorInfoToUse(skyKey, value != null, childErrorInfos);
// TODO(b/166268889, b/172223413): remove when fixed.
if (errorInfo != null && errorInfo.getException() instanceof IOException) {
String skyFunctionName = skyKey.functionName().getName();
if (!skyFunctionName.startsWith("FILE")
&& !skyFunctionName.startsWith("DIRECTORY_LISTING")) {
logger.atInfo().withCause(errorInfo.getException()).log(
"Synthetic errorInfo for %s", skyKey);
}
}
}
// 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<Reportable> events =
reportEventsAndGetEventsToStore(
primaryEntry, /* expectDoneDeps= */ !skyKey.supportsPartialReevaluation());
SkyValue valueWithMetadata;
if (value == null) {
checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry);
valueWithMetadata = ValueWithMetadata.error(errorInfo, events);
} else {
valueWithMetadata = ValueWithMetadata.normal(value, errorInfo, events);
}
GroupedDeps temporaryDirectDeps = primaryEntry.getTemporaryDirectDeps();
ImmutableSet<SkyKey> resetDeps = primaryEntry.getResetDirectDeps();
if (!oldDeps.isEmpty() || !resetDeps.isEmpty()) {
// Remove the rdep on this entry for each of 1) its old deps from a prior evaluation that are
// no longer direct deps and 2) reset deps that were not requested again post-restart.
ImmutableList<SkyKey> depsToRemove =
ImmutableList.copyOf(
Sets.difference(Sets.union(oldDeps, resetDeps), temporaryDirectDeps.toSet()));
NodeBatch oldDepEntries =
evaluatorContext.getGraph().getBatch(skyKey, Reason.RDEP_REMOVAL, depsToRemove);
for (SkyKey key : depsToRemove) {
NodeEntry oldDepEntry = checkNotNull(oldDepEntries.get(key), key);
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 detect this case
// by comparing versions before and after setting the value.
Version previousVersion = primaryEntry.getVersion();
Set<SkyKey> reverseDeps =
primaryEntry.setValue(
valueWithMetadata, evaluatorContext.getGraphVersion(), maxTransitiveSourceVersion);
Version currentVersion = primaryEntry.getVersion();
boolean changed = !currentVersion.equals(previousVersion);
// Tell the receiver that this value was built. If currentVersion.equals(evaluationVersion), it
// was evaluated this run, and so was changed. Otherwise, it is less than evaluationVersion, 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.
evaluatorContext
.getProgressReceiver()
.evaluated(
skyKey,
EvaluationState.get(value, changed),
/* newValue= */ changed ? value : null,
/* newError= */ changed ? errorInfo : null,
temporaryDirectDeps);
return 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 inErrorBubblingForSkyFunctionsThatCanFullyRecoverFromErrors() {
return bubbleErrorInfo != null;
}
@Override
public void registerDependencies(Iterable<SkyKey> keys) {
checkState(
maxTransitiveSourceVersion == null,
"Dependency registration not supported when tracking max transitive source versions");
int sizeBeforeRequest = newlyRequestedDepsValues.size();
for (SkyKey key : keys) {
if (!previouslyRequestedDepsValues.containsKey(key)) {
newlyRequestedDepsValues.putIfAbsent(key, MANUALLY_REGISTERED_MARKER);
}
}
endDepGroup(sizeBeforeRequest);
}
@Override
public void injectVersionForNonHermeticFunction(Version version) {
checkState(skyKey.functionName().getHermeticity() == FunctionHermeticity.NONHERMETIC, skyKey);
checkState(
maxTransitiveSourceVersion == null,
"Multiple injected versions (%s, %s) for %s",
maxTransitiveSourceVersion,
version,
skyKey);
checkNotNull(version, skyKey);
checkState(
!evaluatorContext.getGraphVersion().lowerThan(version),
"Invalid injected version (%s > %s) for %s",
version,
evaluatorContext.getGraphVersion(),
skyKey);
maxTransitiveSourceVersion = version;
}
void maybeUpdateMaxTransitiveSourceVersion(NodeEntry depEntry) {
if (maxTransitiveSourceVersion == null
|| skyKey.functionName().getHermeticity() == FunctionHermeticity.NONHERMETIC) {
return;
}
Version depMtsv = depEntry.getMaxTransitiveSourceVersion();
if (depMtsv == null || maxTransitiveSourceVersion.atMost(depMtsv)) {
maxTransitiveSourceVersion = depMtsv;
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("skyKey", skyKey)
.add("oldDeps", oldDeps)
.add("value", value)
.add("errorInfo", errorInfo)
.add("previouslyRequestedDepsValues", previouslyRequestedDepsValues)
.add("newlyRequestedDepsValues", newlyRequestedDepsValues)
.add("newlyRequestedDepGroupSizes", newlyRequestedDepGroupSizes)
.add("childErrorInfos", childErrorInfos)
.add("depErrorKey", depErrorKey)
.add("maxTransitiveSourceVersion", maxTransitiveSourceVersion)
.add("bubbleErrorInfo", bubbleErrorInfo)
.add("evaluatorContext", evaluatorContext)
.toString();
}
@Override
public SkyframeLookupResult getLookupHandleForPreviouslyRequestedDeps() {
checkActive();
return this;
}
@SuppressWarnings("unchecked")
@Override
public <T extends SkyKeyComputeState> T getState(Supplier<T> stateSupplier) {
return (T) evaluatorContext.stateCache().get(skyKey, k -> stateSupplier.get());
}
boolean encounteredErrorDuringBubbling() {
return encounteredErrorDuringBubbling;
}
@Override
@Nullable
public Version getMaxTransitiveSourceVersionSoFar() {
return maxTransitiveSourceVersion;
}
void ensurePreviouslyRequestedDepsFetched()
throws UndonePreviouslyRequestedDeps, InterruptedException {
// Do nothing; previously requested deps were already fetched and checked for done-ness in
// batchPrefetch.
}
boolean wasNewlyRequestedDepNullForPartialReevaluation(SkyKey newlyRequestedDep) {
return false;
}
/**
* In the case when user intends to add a new parallelism, one approach is to aggregate the
* existing skyframe-evaluator one and a new type of thread pool in a {@link
* com.google.devtools.build.lib.concurrent.MultiThreadPoolsQuiescingExecutor} object, and inject
* it into {@link ParallelEvaluatorContext}.
*/
@Override
public final QuiescingExecutor getParallelEvaluationExecutor() {
return evaluatorContext.getExecutor();
}
/** Thrown during environment construction if a previously requested dep is no longer done. */
static final class UndonePreviouslyRequestedDeps extends Exception {
private final ImmutableList<SkyKey> depKeys;
private UndonePreviouslyRequestedDeps(ImmutableList<SkyKey> depKeys) {
this.depKeys = checkNotNull(depKeys);
}
ImmutableList<SkyKey> getDepKeys() {
return depKeys;
}
}
/**
* The environment that skips eagerly batch prefetching previously requested deps during creation.
* Instead, their values are read from the graph on demand, in the same way as newly requested
* deps.
*
* <p>This subclass is created if the {@link SkyKey} supports partial reevaluation or opts to skip
* batch prefetching previously requested deps values.
*
* <p>The {@link #ensurePreviouslyRequestedDepsFetched} method, which gets called prior to node
* completion, isn't a no-op, because they weren't prefetched. They're needed for version, error,
* and event data during node completion.
*
* <p>The {@link #wasNewlyRequestedDepNullForPartialReevaluation} method may return {@code true},
* when the evaluator checks for a newly requested done dep to which the current node is being
* added as an rdep, to ensure that dep's key gets delivered to this node's mailbox.
*/
private static final class SkipsBatchPrefetch extends SkyFunctionEnvironment {
private SkipsBatchPrefetch(
SkyKey skyKey,
GroupedDeps previouslyRequestedDeps,
Set<SkyKey> oldDeps,
ParallelEvaluatorContext evaluatorContext,
@Nullable Version maxTransitiveSourceVersion)
throws UndonePreviouslyRequestedDeps, InterruptedException {
super(
skyKey,
previouslyRequestedDeps,
/* bubbleErrorInfo= */ null,
oldDeps,
evaluatorContext,
false,
maxTransitiveSourceVersion);
}
@Override
Map<SkyKey, SkyValue> batchPrefetch(boolean throwIfPreviouslyRequestedDepsUndone) {
// Partial reevaluations don't prefetch all previously requested deps, because doing so is too
// expensive, with how many more times those nodes get reevaluated.
return new HashMap<>();
}
@Nullable
@Override
SkyValue getPreviouslyRequestedDepValue(SkyKey key) {
SkyFunctionEnvironment env = this;
if (!env.previouslyRequestedDeps.contains(key)) {
return null;
}
SkyValue possibleValueInMap = env.previouslyRequestedDepsValues.get(key);
if (possibleValueInMap != null) {
return possibleValueInMap;
}
try {
// TODO: b/324948927#comment14 - Figure out the approach to properly handle possible missing
// or undone deps before expanding the usage of `SkipsBatchPrefetch` or making
// `SkipsBatchPrefetch` as the default environment to create.
NodeEntry depEntry =
env.evaluatorContext.getGraph().get(env.skyKey, Reason.DEP_REQUESTED, key);
return processDepEntry(key, depEntry);
} catch (InterruptedException e) {
throw new IllegalStateException("No interruption when getting depEntry from depGraph", e);
}
}
@Nullable
@Override
SkyValue lookupRequestedDep(SkyKey depKey) {
SkyFunctionEnvironment env = this;
checkArgument(
!depKey.equals(ErrorTransienceValue.KEY),
"Error transience key cannot be in requested deps of %s",
env.skyKey);
if (env.previouslyRequestedDeps.contains(depKey)) {
return env.previouslyRequestedDepsValues.putIfAbsent(depKey, PENDING_MARKER);
}
SkyValue directDepsValue = env.newlyRequestedDepsValues.putIfAbsent(depKey, PENDING_MARKER);
return directDepsValue == MANUALLY_REGISTERED_MARKER ? null : directDepsValue;
}
@CanIgnoreReturnValue
@Override
SkyValue processDepEntry(SkyKey depKey, @Nullable NodeEntry depEntry)
throws InterruptedException {
SkyFunctionEnvironment env = this;
SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry);
processDepValue(depKey, valueOrNullMarker);
if (env.previouslyRequestedDeps.contains(depKey)) {
env.previouslyRequestedDepsValues.put(depKey, valueOrNullMarker);
} else {
env.newlyRequestedDepsValues.put(depKey, valueOrNullMarker);
}
if (valueOrNullMarker != NULL_MARKER) {
maybeUpdateMaxTransitiveSourceVersion(depEntry);
}
return valueOrNullMarker;
}
@Override
void ensurePreviouslyRequestedDepsFetched()
throws UndonePreviouslyRequestedDeps, InterruptedException {
SkyFunctionEnvironment env = this;
ImmutableList<SkyKey> keysToFetch =
env.previouslyRequestedDeps.toSet().stream()
.filter(k -> !env.previouslyRequestedDepsValues.containsKey(k))
.collect(toImmutableList());
NodeBatch batch =
env.evaluatorContext.getGraph().getBatch(env.skyKey, Reason.PREFETCH, keysToFetch);
ImmutableList.Builder<SkyKey> missingRequestedDeps = null;
for (SkyKey depKey : keysToFetch) {
NodeEntry entry = batch.get(depKey);
if (entry == null) {
if (missingRequestedDeps == null) {
missingRequestedDeps = ImmutableList.builder();
}
missingRequestedDeps.add(depKey);
continue;
}
SkyValue valueMaybeWithMetadata = entry.getValueMaybeWithMetadata();
boolean depDone = valueMaybeWithMetadata != null;
if (!depDone) {
// A previously requested dep may have transitioned from done to dirty between when the
// node was read during a previous attempt to build this node and now. Notify the graph
// inconsistency receiver so that we can crash if that's unexpected.
env.evaluatorContext
.getGraphInconsistencyReceiver()
.noteInconsistencyAndMaybeThrow(
env.skyKey,
ImmutableList.of(depKey),
Inconsistency.BUILDING_PARENT_FOUND_UNDONE_CHILD);
throw new UndonePreviouslyRequestedDeps(ImmutableList.of(depKey));
}
env.previouslyRequestedDepsValues.put(depKey, valueMaybeWithMetadata);
maybeUpdateMaxTransitiveSourceVersion(entry);
}
if (missingRequestedDeps != null) {
// Notify `GraphInconsistencyReceiver` when there are some dependencies missing from the
// graph to check whether this is expected.
ImmutableList<SkyKey> allMissingDeps = missingRequestedDeps.build();
env.evaluatorContext
.getGraphInconsistencyReceiver()
.noteInconsistencyAndMaybeThrow(
env.skyKey, allMissingDeps, Inconsistency.ALREADY_DECLARED_CHILD_MISSING);
throw new UndonePreviouslyRequestedDeps(allMissingDeps);
}
}
@Override
boolean wasNewlyRequestedDepNullForPartialReevaluation(SkyKey newlyRequestedDep) {
SkyFunctionEnvironment env = this;
return env.newlyRequestedDepsValues.get(newlyRequestedDep) == NULL_MARKER;
}
}
}