| // 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 |
| && !shouldKeepGoingWhenEvaluatingDep(depKey) |
| && bubbleErrorInfo == null)) { |
| valuesMissing = true; |
| // We arbitrarily record the first child error if we are about to abort. |
| if (!shouldKeepGoingWhenEvaluatingDep(depKey) && depErrorKey == null) { |
| depErrorKey = depKey; |
| } |
| } |
| } |
| |
| private boolean shouldKeepGoingWhenEvaluatingDep(SkyKey depKey) { |
| return evaluatorContext.keepGoing(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 (shouldKeepGoingWhenEvaluatingDep(depKey)) { |
| // 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 (shouldKeepGoingWhenEvaluatingDep(depKey) || 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 inErrorBubbling() { |
| 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; |
| } |
| } |
| } |