blob: b0e6a481bac0d3af9b85d8c4179a1e0d708447d6 [file] [log] [blame]
// Copyright 2022 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.state;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunction.Environment;
import com.google.devtools.build.skyframe.SkyframeLookupResult;
import java.util.ArrayDeque;
import java.util.ArrayList;
/**
* This class drives a {@link StateMachine} instance.
*
* <p>One recommended usage pattern for this class is to embed an instance within a top level {@link
* StateMachine} implementation and from there, re-export the {@link #drive} method. Then the
* results from the {@link StateMachine} will be readily retrievable from the {@link SkyFunction}
* state.
*/
// TODO(shahan); this is incompatible with partial re-evaluation, which causes the assumption that
// an unavailable previously requested dependency implies an error to no longer be true. This can be
// fixed by integrating with the partial re-evaluation mailbox.
public final class Driver {
private final ArrayDeque<TaskTreeNode> ready = new ArrayDeque<>();
/** A Skyframe lookup has not yet been made for the key. */
private final ArrayList<Lookup> newlyAdded = new ArrayList<>();
/** A Skyframe lookup has already been made for the key, but it was not available. */
private final ArrayList<Lookup> pending = new ArrayList<>();
public Driver(StateMachine root) {
ready.addLast(new TaskTreeNode(this, /* parent= */ null, root));
}
/**
* Drives the machine as far as it can go without a Skyframe restart.
*
* @return true if execution is complete, false if a restart is needed.
*/
public boolean drive(Environment env, ExtendedEventHandler listener) throws InterruptedException {
if (!pending.isEmpty()) {
// If pending is non-empty, it means there was a Skyframe restart. Either everything that was
// pending is available now or we are in error bubbling. In the latter case, this method
// returns early when it either observes an error or missing value.
//
// NB: this assumption does not hold under partial re-evaluation and likewise the inference
// below about unavailable values being errors.
SkyframeLookupResult result = env.getLookupHandleForPreviouslyRequestedDeps();
boolean hasExceptionOrMissingValue = false;
for (var lookup : pending) {
if (!result.queryDep(lookup.key(), lookup)) {
// Since the key was previously requested, unavailability here could be an unhandled
// exception or a missing value during error bubbling. It's not possible to determine
// which here. Requests the key to ensure that if it is an error, the environment
// instance knows that the failure is due to child error.
var unusedNull = env.getValue(lookup.key());
// Failing fast here would make behavior dependent on element ordering and possibly miss
// errors in error bubbling, so instead, flags the exception and fails after all lookups
// have been processed.
hasExceptionOrMissingValue = true;
}
}
if (hasExceptionOrMissingValue) {
return false;
}
pending.clear();
}
while (true) {
// Runs all ready tasks, including ones that may be added during execution.
TaskTreeNode next;
while ((next = ready.poll()) != null) {
next.run(listener);
}
// No more tasks are ready. If there are no newly added lookups, it isn't possible to drive
// this machine any further.
if (newlyAdded.isEmpty()) {
return pending.isEmpty(); // If there are no pending lookups, the machine is done.
}
// Performs lookups for any newly added keys.
if (newlyAdded.size() == 1) { // Uses a lower overhead lookup for the unary case.
var onlyLookup = newlyAdded.get(0);
if (!onlyLookup.doLookup(env)) {
pending.add(onlyLookup);
}
} else {
SkyframeLookupResult result =
env.getValuesAndExceptions(Lists.transform(newlyAdded, Lookup::key));
for (var lookup : newlyAdded) {
if (!result.queryDep(lookup.key(), lookup)) {
pending.add(lookup); // Unhandled exceptions also end up here.
}
}
}
newlyAdded.clear(); // Every entry is either done or has moved to pending.
}
}
void addReady(TaskTreeNode task) {
ready.addLast(task);
}
/**
* Adds a dependency to look up.
*
* <p>The callback could be deferred until the next Skyframe restart if the queried key is not
* immediately available.
*/
void addLookup(Lookup lookup) {
newlyAdded.add(lookup);
}
}