blob: 9429702d2ae60e1173d625643207064fd59047fe [file] [log] [blame]
// Copyright 2014 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.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.devtools.build.lib.testutil.EventIterableSubjectFactory.assertThatEvents;
import static com.google.devtools.build.skyframe.ErrorInfoSubjectFactory.assertThatErrorInfo;
import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult;
import static com.google.devtools.build.skyframe.GraphTester.CONCATENATE;
import static com.google.devtools.build.skyframe.GraphTester.COPY;
import static com.google.devtools.build.skyframe.GraphTester.NODE_TYPE;
import static com.google.devtools.build.skyframe.GraphTester.nonHermeticKey;
import static com.google.devtools.build.skyframe.GraphTester.skyKey;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
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.eventbus.EventBus;
import com.google.common.testing.GcFinalization;
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.bugreport.BugReport;
import com.google.devtools.build.lib.events.DelegatingEventHandler;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventCollector;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.testutil.TestThread;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.skyframe.Differencer.DiffWithDelta.Delta;
import com.google.devtools.build.skyframe.GraphTester.NotComparableStringValue;
import com.google.devtools.build.skyframe.GraphTester.StringValue;
import com.google.devtools.build.skyframe.GraphTester.TestFunction;
import com.google.devtools.build.skyframe.GraphTester.ValueComputer;
import com.google.devtools.build.skyframe.MemoizingEvaluator.GraphTransformerForTesting;
import com.google.devtools.build.skyframe.NodeEntry.DirtyType;
import com.google.devtools.build.skyframe.NotifyingHelper.EventType;
import com.google.devtools.build.skyframe.NotifyingHelper.Listener;
import com.google.devtools.build.skyframe.NotifyingHelper.Order;
import com.google.devtools.build.skyframe.QueryableGraph.Reason;
import com.google.devtools.build.skyframe.SkyFunction.Reset;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.proto.GraphInconsistency.Inconsistency;
import com.google.errorprone.annotations.ForOverride;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
/** Tests for a {@link MemoizingEvaluator}. */
public abstract class MemoizingEvaluatorTest {
protected MemoizingEvaluatorTester tester;
protected EventCollector eventCollector;
protected ExtendedEventHandler reporter;
protected EmittedEventState emittedEventState;
// Knobs that control the size / duration of larger tests.
private static final int TEST_NODE_COUNT = 100;
private static final int TESTED_NODES = 10;
private static final int RUNS = 10;
@Before
public void initializeTester() {
initializeTester(null);
initializeReporter();
}
private void initializeTester(@Nullable TrackingProgressReceiver customProgressReceiver) {
emittedEventState = new EmittedEventState();
tester = new MemoizingEvaluatorTester();
if (customProgressReceiver != null) {
tester.setProgressReceiver(customProgressReceiver);
}
tester.initialize();
}
protected TrackingProgressReceiver createTrackingProgressReceiver(
boolean checkEvaluationResults) {
return new TrackingProgressReceiver(checkEvaluationResults);
}
@After
public void assertNoTrackedErrors() {
TrackingAwaiter.INSTANCE.assertNoErrors();
}
protected RecordingDifferencer getRecordingDifferencer() {
return new SequencedRecordingDifferencer();
}
@ForOverride
protected abstract MemoizingEvaluator getMemoizingEvaluator(
ImmutableMap<SkyFunctionName, SkyFunction> functions,
Differencer differencer,
EvaluationProgressReceiver progressReceiver,
GraphInconsistencyReceiver graphInconsistencyReceiver,
EventFilter eventFilter);
/** Invoked immediately before each call to {@link MemoizingEvaluator#evaluate}. */
@ForOverride
protected void beforeEvaluation() {}
/**
* Invoked immediately after {@link MemoizingEvaluator#evaluate} with the {@link EvaluationResult}
* or {@code null} if an exception was thrown.
*/
@ForOverride
protected void afterEvaluation(@Nullable EvaluationResult<?> result, EvaluationContext context)
throws InterruptedException {}
@ForOverride
protected boolean cyclesDetected() {
return true;
}
@ForOverride
protected boolean resetSupported() {
return true;
}
// TODO(jhorvitz): Skip irrelevant test cases if this is false.
@ForOverride
protected boolean incrementalitySupported() {
return true;
}
private void initializeReporter() {
eventCollector = new EventCollector();
reporter = new Reporter(new EventBus(), eventCollector);
tester.resetPlayedEvents();
}
@Test
public void smoke() throws Exception {
tester.set("x", new StringValue("y"));
StringValue value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
}
@Test
public void evaluateEmptySet() throws InterruptedException {
tester.eval(false, new SkyKey[0]);
tester.eval(true, new SkyKey[0]);
}
@Test
public void injectGraphTransformer_transformedGraphUsedForInMemoryGraph() {
assume().that(tester.evaluator).isInstanceOf(AbstractInMemoryMemoizingEvaluator.class);
InMemoryGraph realGraph = tester.evaluator.getInMemoryGraph();
InMemoryGraph mockGraph = mock(InMemoryGraph.class);
tester.evaluator.injectGraphTransformerForTesting(
new GraphTransformerForTesting() {
@Override
public InMemoryGraph transform(InMemoryGraph graph) {
assertThat(graph).isSameInstanceAs(realGraph);
return mockGraph;
}
@Override
public ProcessableGraph transform(ProcessableGraph graph) {
throw new AssertionError(graph);
}
});
assertThat(tester.evaluator.getInMemoryGraph()).isSameInstanceAs(mockGraph);
}
@Test
public void injectGraphTransformer_transformedGraphUsedForEvaluation() throws Exception {
Listener listener = mock(Listener.class);
tester.evaluator.injectGraphTransformerForTesting(
NotifyingHelper.makeNotifyingTransformer(listener));
SkyKey key = skyKey("key");
SkyValue val = new StringValue("val");
tester.getOrCreate(key).setConstantValue(val);
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(val);
verify(listener).accept(key, EventType.GET_BATCH, Order.BEFORE, Reason.PRE_OR_POST_EVALUATION);
}
@Test
public void injectGraphTransformer_multipleTransformersAppliedInOrder() throws Exception {
Listener inner = mock(Listener.class);
Listener outer = mock(Listener.class);
tester.evaluator.injectGraphTransformerForTesting(
NotifyingHelper.makeNotifyingTransformer(inner));
tester.evaluator.injectGraphTransformerForTesting(
NotifyingHelper.makeNotifyingTransformer(outer));
SkyKey key = skyKey("key");
SkyValue val = new StringValue("val");
tester.getOrCreate(key).setConstantValue(val);
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(val);
InOrder inOrder = inOrder(inner, outer);
inOrder.verify(outer).accept(key, EventType.SET_VALUE, Order.BEFORE, val);
inOrder.verify(inner).accept(key, EventType.SET_VALUE, Order.BEFORE, val);
inOrder.verify(inner).accept(key, EventType.SET_VALUE, Order.AFTER, val);
inOrder.verify(outer).accept(key, EventType.SET_VALUE, Order.AFTER, val);
}
@Test
public void invalidationWithNothingChanged() throws Exception {
tester.set("x", new StringValue("y")).setWarning("fizzlepop");
StringValue value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
assertThatEvents(eventCollector).containsExactly("fizzlepop");
initializeReporter();
tester.invalidate();
value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
}
@Test
// Regression test for bug: "[skyframe-m1]: registerIfDone() crash".
public void bubbleRace() throws Exception {
// The top-level value declares dependencies on a "badValue" in error, and a "sleepyValue"
// which is very slow. After "badValue" fails, the builder interrupts the "sleepyValue" and
// attempts to re-run "top" for error bubbling. Make sure this doesn't cause a precondition
// failure because "top" still has an outstanding dep ("sleepyValue").
tester
.getOrCreate("top")
.setBuilder(
(skyKey, env) -> {
env.getValue(skyKey("sleepyValue"));
try {
env.getValueOrThrow(skyKey("badValue"), SomeErrorException.class);
} catch (SomeErrorException e) {
// In order to trigger this bug, we need to request a dep on an already computed
// value.
env.getValue(skyKey("otherValue1"));
}
if (!env.valuesMissing()) {
throw new AssertionError("SleepyValue should always be unavailable");
}
return null;
});
tester
.getOrCreate("sleepyValue")
.setBuilder(
(skyKey, env) -> {
Thread.sleep(99999);
throw new AssertionError("I should have been interrupted");
});
tester.getOrCreate("badValue").addDependency("otherValue1").setHasError(true);
tester.getOrCreate("otherValue1").setConstantValue(new StringValue("otherVal1"));
EvaluationResult<SkyValue> result = tester.eval(false, "top");
assertThatEvaluationResult(result).hasSingletonErrorThat(skyKey("top"));
}
@Test
public void testEnvProvidesTemporaryDirectDeps() throws Exception {
AtomicInteger counter = new AtomicInteger();
List<SkyKey> deps = Collections.synchronizedList(new ArrayList<>());
SkyKey topKey = skyKey("top");
SkyKey bottomKey = skyKey("bottom");
SkyValue bottomValue = new StringValue("bottom");
tester
.getOrCreate(topKey)
.setBuilder(
(skyKey, env) -> {
if (counter.getAndIncrement() > 0) {
deps.addAll(env.getTemporaryDirectDeps().getDepGroup(0));
} else {
assertThat(env.getTemporaryDirectDeps().numGroups()).isEqualTo(0);
}
return env.getValue(bottomKey);
});
tester.getOrCreate(bottomKey).setConstantValue(bottomValue);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ true, "top");
assertThat(result.get(topKey)).isEqualTo(bottomValue);
assertThat(deps).containsExactly(bottomKey);
}
@Test
public void cachedErrorShutsDownThreadpool() throws Exception {
// When a node throws an error on the first build,
SkyKey cachedErrorKey = skyKey("error");
tester.getOrCreate(cachedErrorKey).setHasError(true);
assertThat(tester.evalAndGetError(/*keepGoing=*/ true, cachedErrorKey)).isNotNull();
// And on the second build, it is requested as a dep,
SkyKey topKey = skyKey("top");
tester.getOrCreate(topKey).addDependency(cachedErrorKey).setComputedValue(CONCATENATE);
// And another node throws an error, but waits to throw until the child error is thrown,
SkyKey newErrorKey = skyKey("newError");
tester
.getOrCreate(newErrorKey)
.setBuilder(
new ChainedFunction.Builder()
.setWaitForException(true)
.setWaitToFinish(new CountDownLatch(0))
.setValue(null)
.build());
// Then when evaluation happens,
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, newErrorKey, topKey);
// The result has an error,
assertThatEvaluationResult(result).hasError();
// But the new error is not persisted to the graph, since the child error shut down evaluation.
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(newErrorKey).isNull();
}
@Test
public void interruptBitCleared() {
SkyKey interruptKey = skyKey("interrupt");
tester.getOrCreate(interruptKey).setBuilder(INTERRUPT_BUILDER);
assertThrows(
InterruptedException.class, () -> tester.eval(/* keepGoing= */ true, interruptKey));
assertThat(Thread.interrupted()).isFalse();
}
@Test
public void crashAfterInterruptCrashes() {
SkyKey failKey = skyKey("fail");
SkyKey badInterruptkey = skyKey("bad-interrupt");
// Given a SkyFunction implementation which is improperly coded to throw a runtime exception
// when it is interrupted,
CountDownLatch badInterruptStarted = new CountDownLatch(1);
tester
.getOrCreate(badInterruptkey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) {
badInterruptStarted.countDown();
try {
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
throw new AssertionError("Shouldn't have slept so long");
} catch (InterruptedException e) {
throw new RuntimeException("I don't like being woken up!", e);
}
}
});
// And another SkyFunction that waits for the first to start, and then throws,
tester
.getOrCreate(failKey)
.setBuilder(
new ChainedFunction(
null,
badInterruptStarted,
null,
/*waitForException=*/ false,
null,
ImmutableList.of()));
// When it is interrupted during evaluation (here, caused by the failure of the throwing
// SkyFunction during a no-keep-going evaluation), then the ParallelEvaluator#evaluate call
// throws a RuntimeException e where e.getCause() is the RuntimeException thrown by that
// SkyFunction.
RuntimeException e =
assertThrows(
RuntimeException.class,
() -> tester.eval(/*keepGoing=*/ false, badInterruptkey, failKey));
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("I don't like being woken up!");
}
@Test
public void interruptAfterFailFails() throws Exception {
SkyKey failKey = skyKey("fail");
SkyKey interruptedKey = skyKey("interrupted");
// Given a SkyFunction implementation that is properly coded to as not to throw a
// runtime exception when it is interrupted,
CountDownLatch interruptStarted = new CountDownLatch(1);
tester
.getOrCreate(interruptedKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
interruptStarted.countDown();
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
throw new AssertionError("Shouldn't have slept so long");
}
});
// And another SkyFunction that waits for the first to start, and then throws,
tester
.getOrCreate(failKey)
.setBuilder(
new ChainedFunction(
null,
interruptStarted,
null,
/*waitForException=*/ false,
null,
ImmutableList.of()));
// When it is interrupted during evaluation (here, caused by the failure of a sibling node
// during a no-keep-going evaluation),
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, interruptedKey, failKey);
// Then the ParallelEvaluator#evaluate call returns an EvaluationResult that has no error for
// the interrupted SkyFunction.
assertWithMessage(result.toString()).that(result.hasError()).isTrue();
assertWithMessage(result.toString()).that(result.getError(failKey)).isNotNull();
assertWithMessage(result.toString()).that(result.getError(interruptedKey)).isNull();
}
@Test
public void deleteValues() throws Exception {
tester
.getOrCreate("top")
.setComputedValue(CONCATENATE)
.addDependency("d1")
.addDependency("d2")
.addDependency("d3");
tester.set("d1", new StringValue("1"));
StringValue d2 = new StringValue("2");
tester.set("d2", d2);
StringValue d3 = new StringValue("3");
tester.set("d3", d3);
tester.eval(true, "top");
tester.delete("d1");
tester.eval(true, "d3");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEqualTo(ImmutableSet.of(skyKey("d1"), skyKey("top")));
assertThat(tester.getExistingValue("top")).isNull();
assertThat(tester.getExistingValue("d1")).isNull();
assertThat(tester.getExistingValue("d2")).isEqualTo(d2);
assertThat(tester.getExistingValue("d3")).isEqualTo(d3);
}
@Test
public void deleteOldNodesTest() throws Exception {
SkyKey d2Key = nonHermeticKey("d2");
tester
.getOrCreate("top")
.setComputedValue(CONCATENATE)
.addDependency("d1")
.addDependency(d2Key);
tester.set("d1", new StringValue("one"));
tester.set(d2Key, new StringValue("two"));
tester.eval(true, "top");
tester.set(d2Key, new StringValue("three"));
tester.invalidate();
tester.eval(true, d2Key);
// The graph now contains the three above nodes (and ERROR_TRANSIENCE).
assertThat(tester.evaluator.getValues().keySet())
.containsExactly(skyKey("top"), skyKey("d1"), d2Key, ErrorTransienceValue.KEY);
String[] noKeys = {};
tester.evaluator.deleteDirty(2);
tester.eval(true, noKeys);
// The top node's value is dirty, but less than two generations old, so it wasn't deleted.
assertThat(tester.evaluator.getValues().keySet())
.containsExactly(skyKey("top"), skyKey("d1"), d2Key, ErrorTransienceValue.KEY);
tester.evaluator.deleteDirty(2);
tester.eval(true, noKeys);
// The top node's value was dirty, and was two generations old, so it was deleted.
assertThat(tester.evaluator.getValues().keySet())
.containsExactly(skyKey("d1"), d2Key, ErrorTransienceValue.KEY);
}
@Test
public void deleteDirtyCleanedValue() throws Exception {
SkyKey leafKey = nonHermeticKey("leafKey");
tester.getOrCreate(leafKey).setConstantValue(new StringValue("value"));
SkyKey topKey = skyKey("topKey");
tester.getOrCreate(topKey).addDependency(leafKey).setComputedValue(CONCATENATE);
assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey)).isEqualTo(new StringValue("value"));
failBuildAndRemoveValue(leafKey);
tester.evaluator.deleteDirty(0);
}
@Test
public void deleteNonexistentValues() throws Exception {
tester.getOrCreate("d1").setConstantValue(new StringValue("1"));
tester.delete("d1");
tester.delete("d2");
tester.eval(true, "d1");
}
@Test
public void signalValueEnqueued() throws Exception {
tester
.getOrCreate("top1")
.setComputedValue(CONCATENATE)
.addDependency("d1")
.addDependency("d2");
tester.getOrCreate("top2").setComputedValue(CONCATENATE).addDependency("d3");
tester.getOrCreate("top3");
assertThat(tester.getEnqueuedValues()).isEmpty();
tester.set("d1", new StringValue("1"));
tester.set("d2", new StringValue("2"));
tester.set("d3", new StringValue("3"));
tester.eval(true, "top1");
assertThat(tester.getEnqueuedValues())
.containsExactlyElementsIn(MemoizingEvaluatorTester.toSkyKeys("top1", "d1", "d2"));
tester.eval(true, "top2");
assertThat(tester.getEnqueuedValues())
.containsExactlyElementsIn(
MemoizingEvaluatorTester.toSkyKeys("top1", "d1", "d2", "top2", "d3"));
}
@Test
public void warningViaMultiplePaths() throws Exception {
tester.set("d1", new StringValue("d1")).setWarning("warn-d1");
tester.set("d2", new StringValue("d2")).setWarning("warn-d2");
tester.getOrCreate("top").setComputedValue(CONCATENATE).addDependency("d1").addDependency("d2");
initializeReporter();
tester.evalAndGet("top");
assertThatEvents(eventCollector).containsExactly("warn-d1", "warn-d2");
}
@Test
public void warningBeforeErrorOnFailFastBuild() throws Exception {
tester.set("dep", new StringValue("dep")).setWarning("warn-dep");
SkyKey topKey = skyKey("top");
tester.getOrCreate(topKey).setHasError(true).addDependency("dep");
for (int i = 0; i < 2; i++) {
initializeReporter();
EvaluationResult<StringValue> result = tester.eval(false, "top");
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.hasMessageThat()
.isEqualTo(topKey.toString());
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.isInstanceOf(SomeErrorException.class);
if (i == 0) {
assertThatEvents(eventCollector).containsExactly("warn-dep");
}
}
}
@Test
public void warningAndErrorOnFailFastBuild() throws Exception {
SkyKey topKey = skyKey("top");
tester.set(topKey, new StringValue("top")).setWarning("warning msg").setHasError(true);
for (int i = 0; i < 2; i++) {
initializeReporter();
EvaluationResult<StringValue> result = tester.eval(false, "top");
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.hasMessageThat()
.isEqualTo(topKey.toString());
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.isInstanceOf(SomeErrorException.class);
if (i == 0) {
assertThatEvents(eventCollector).containsExactly("warning msg");
}
}
}
@Test
public void warningAndErrorOnFailFastBuildAfterKeepGoingBuild() throws Exception {
SkyKey topKey = skyKey("top");
tester.set(topKey, new StringValue("top")).setWarning("warning msg").setHasError(true);
for (int i = 0; i < 2; i++) {
initializeReporter();
EvaluationResult<StringValue> result = tester.eval(i == 0, "top");
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.hasMessageThat()
.isEqualTo(topKey.toString());
assertThatEvaluationResult(result)
.hasSingletonErrorThat(topKey)
.hasExceptionThat()
.isInstanceOf(SomeErrorException.class);
if (i == 0) {
assertThatEvents(eventCollector).containsExactly("warning msg");
}
}
}
@Test
public void twoTLTsOnOneWarningValue() throws Exception {
tester.set("t1", new StringValue("t1")).addDependency("dep");
tester.set("t2", new StringValue("t2")).addDependency("dep");
tester.set("dep", new StringValue("dep")).setWarning("look both ways before crossing");
initializeReporter();
tester.eval(/* keepGoing= */ false, "t1", "t2");
assertThatEvents(eventCollector).containsExactly("look both ways before crossing");
}
@Test
public void errorValueDepOnWarningValue() throws Exception {
tester.getOrCreate("error-value").setHasError(true).addDependency("warning-value");
tester
.set("warning-value", new StringValue("warning-value"))
.setWarning("don't chew with your mouth open");
initializeReporter();
tester.evalAndGetError(/* keepGoing= */ true, "error-value");
assertThatEvents(eventCollector).containsExactly("don't chew with your mouth open");
}
@Test
public void progressMessageOnlyPrintedTheFirstTime() throws Exception {
// Skyframe does not store progress messages. Here we only see the message on the first build.
tester.set("x", new StringValue("y")).setProgress("just letting you know");
StringValue value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
assertThatEvents(eventCollector).containsExactly("just letting you know");
initializeReporter();
value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
assertThatEvents(eventCollector).isEmpty();
}
@Test
public void depMessageBeforeNodeMessageOrNodeValue() throws Exception {
SkyKey top = skyKey("top");
AtomicBoolean depWarningEmitted = new AtomicBoolean(false);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (key.equals(top) && type == EventType.SET_VALUE) {
assertThat(depWarningEmitted.get()).isTrue();
}
},
/*deterministic=*/ false);
String depWarning = "dep warning";
Event topWarning = Event.warn("top warning");
reporter =
new DelegatingEventHandler(reporter) {
@Override
public void handle(Event e) {
if (e.getMessage().equals(depWarning)) {
depWarningEmitted.set(true);
}
if (e.equals(topWarning)) {
assertThat(depWarningEmitted.get()).isTrue();
}
super.handle(e);
}
};
SkyKey leaf = skyKey("leaf");
tester.getOrCreate(leaf).setWarning(depWarning).setConstantValue(new StringValue("leaf"));
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
SkyValue depValue = env.getValue(leaf);
if (depValue != null) {
// Default GraphTester implementation warns before requesting deps, which doesn't
// work
// for ordering assertions with memoizing evaluator subclsses that don't store
// events
// and instead just pass them through directly. By warning after the dep is done
// we
// avoid that issue.
env.getListener().handle(topWarning);
}
return depValue;
}
});
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("leaf"));
assertThatEvents(eventCollector).containsExactly(depWarning, topWarning.getMessage()).inOrder();
}
@Test
public void invalidationWithChangeAndThenNothingChanged() throws Exception {
SkyKey bKey = nonHermeticKey("b");
tester.getOrCreate("a").addDependency(bKey).setComputedValue(COPY);
tester.set(bKey, new StringValue("y"));
StringValue original = (StringValue) tester.evalAndGet("a");
assertThat(original.getValue()).isEqualTo("y");
tester.set(bKey, new StringValue("z"));
tester.invalidate();
StringValue old = (StringValue) tester.evalAndGet("a");
assertThat(old.getValue()).isEqualTo("z");
tester.invalidate();
StringValue current = (StringValue) tester.evalAndGet("a");
assertThat(current).isEqualTo(old);
}
@Test
public void noKeepGoingErrorAfterKeepGoingError() throws Exception {
SkyKey topKey = nonHermeticKey("top");
SkyKey errorKey = skyKey("error");
tester.getOrCreate(errorKey).setHasError(true);
tester.getOrCreate(topKey).addDependency(errorKey).setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
tester.getOrCreate(topKey, /* markAsModified= */ true);
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
}
@Test
public void transientErrorValueInvalidation() throws Exception {
// Verify that invalidating errors causes all transient error values to be rerun.
tester
.getOrCreate("error-value")
.setHasTransientError(true)
.setProgress("just letting you know");
tester.evalAndGetError(/*keepGoing=*/ true, "error-value");
assertThatEvents(eventCollector).containsExactly("just letting you know");
// Change the progress message.
tester
.getOrCreate("error-value")
.setHasTransientError(true)
.setProgress("letting you know more");
// Without invalidating errors, we shouldn't show the new progress message.
for (int i = 0; i < 2; i++) {
initializeReporter();
tester.evalAndGetError(/*keepGoing=*/ true, "error-value");
assertThatEvents(eventCollector).isEmpty();
}
// When invalidating errors, we should show the new progress message.
initializeReporter();
tester.invalidateTransientErrors();
tester.evalAndGetError(/*keepGoing=*/ true, "error-value");
assertThatEvents(eventCollector).containsExactly("letting you know more");
}
@Test
public void transientPruning() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
tester.getOrCreate("top").setHasTransientError(true).addDependency(leaf);
tester.set(leaf, new StringValue("leafy"));
tester.evalAndGetError(/*keepGoing=*/ true, "top");
tester.getOrCreate(leaf, /*markAsModified=*/ true);
tester.invalidate();
tester.evalAndGetError(/*keepGoing=*/ true, "top");
}
@Test
public void simpleDependency() throws Exception {
tester.getOrCreate("ab").addDependency("a").setComputedValue(COPY);
tester.set("a", new StringValue("me"));
StringValue value = (StringValue) tester.evalAndGet("ab");
assertThat(value.getValue()).isEqualTo("me");
}
@Test
public void incrementalSimpleDependency() throws Exception {
SkyKey aKey = nonHermeticKey("a");
tester.getOrCreate("ab").addDependency(aKey).setComputedValue(COPY);
tester.set(aKey, new StringValue("me"));
tester.evalAndGet("ab");
tester.set(aKey, new StringValue("other"));
tester.invalidate();
StringValue value = (StringValue) tester.evalAndGet("ab");
assertThat(value.getValue()).isEqualTo("other");
}
@Test
public void diamondDependency() throws Exception {
SkyKey diamondBase = setupDiamondDependency();
tester.set(diamondBase, new StringValue("me"));
StringValue value = (StringValue) tester.evalAndGet("a");
assertThat(value.getValue()).isEqualTo("meme");
}
@Test
public void incrementalDiamondDependency() throws Exception {
SkyKey diamondBase = setupDiamondDependency();
tester.set(diamondBase, new StringValue("me"));
tester.evalAndGet("a");
tester.set(diamondBase, new StringValue("other"));
tester.invalidate();
StringValue value = (StringValue) tester.evalAndGet("a");
assertThat(value.getValue()).isEqualTo("otherother");
}
private SkyKey setupDiamondDependency() {
SkyKey diamondBase = nonHermeticKey("d");
tester.getOrCreate("a").addDependency("b").addDependency("c").setComputedValue(CONCATENATE);
tester.getOrCreate("b").addDependency(diamondBase).setComputedValue(COPY);
tester.getOrCreate("c").addDependency(diamondBase).setComputedValue(COPY);
return diamondBase;
}
// ParallelEvaluator notifies ValueProgressReceiver of already-built top-level values in error: we
// built "top" and "mid" as top-level targets; "mid" contains an error. We make sure "mid" is
// built as a dependency of "top" before enqueuing mid as a top-level target (by using a latch),
// so that the top-level enqueuing finds that mid has already been built. The progress receiver
// should be notified that mid has been built.
@Test
public void alreadyAnalyzedBadTarget() throws Exception {
SkyKey mid = skyKey("zzmid");
CountDownLatch valueSet = new CountDownLatch(1);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!key.equals(mid)) {
return;
}
switch (type) {
case ADD_REVERSE_DEP:
if (context == null) {
// Context is null when we are enqueuing this value as a top-level job.
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(valueSet, "value not set");
}
break;
case SET_VALUE:
valueSet.countDown();
break;
default:
break;
}
},
/* deterministic= */ true);
SkyKey top = skyKey("aatop");
tester.getOrCreate(top).addDependency(mid).setComputedValue(CONCATENATE);
tester.getOrCreate(mid).setHasError(true);
tester.eval(/* keepGoing= */ false, top, mid);
assertThat(valueSet.getCount()).isEqualTo(0L);
assertThat(tester.progressReceiver.evaluated).containsExactly(mid);
}
@Test
public void receiverToldOfVerifiedValueDependingOnCycle() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey cycle = skyKey("cycle");
SkyKey top = skyKey("top");
tester.set(leaf, new StringValue("leaf"));
tester.getOrCreate(cycle).addDependency(cycle);
tester.getOrCreate(top).addDependency(leaf).addDependency(cycle);
tester.eval(/* keepGoing= */ true, top);
assertThat(tester.progressReceiver.evaluated).containsExactly(leaf, top, cycle);
tester.progressReceiver.clear();
tester.getOrCreate(leaf, /* markAsModified= */ true);
tester.invalidate();
tester.eval(/* keepGoing= */ true, top);
assertThat(tester.progressReceiver.evaluated).containsExactly(leaf, top);
}
@Test
public void incrementalAddedDependency() throws Exception {
SkyKey aKey = nonHermeticKey("a");
SkyKey bKey = nonHermeticKey("b");
tester.getOrCreate(aKey).addDependency(bKey).setComputedValue(CONCATENATE);
tester.set(bKey, new StringValue("first"));
tester.set("c", new StringValue("second"));
tester.evalAndGet(/* keepGoing= */ false, aKey);
tester.getOrCreate(aKey).addDependency("c");
tester.set(bKey, new StringValue("now"));
tester.invalidate();
StringValue value = (StringValue) tester.evalAndGet(/* keepGoing= */ false, aKey);
assertThat(value.getValue()).isEqualTo("nowsecond");
}
@Test
public void manyValuesDependOnSingleValue() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
String[] values = new String[TEST_NODE_COUNT];
for (int i = 0; i < values.length; i++) {
values[i] = Integer.toString(i);
tester.getOrCreate(values[i]).addDependency(leaf).setComputedValue(COPY);
}
tester.set(leaf, new StringValue("leaf"));
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ false, values);
for (String value : values) {
SkyValue actual = result.get(skyKey(value));
assertThat(actual).isEqualTo(new StringValue("leaf"));
}
for (int j = 0; j < TESTED_NODES; j++) {
tester.set(leaf, new StringValue("other" + j));
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, values);
for (int i = 0; i < values.length; i++) {
SkyValue actual = result.get(skyKey(values[i]));
assertWithMessage("Run " + j + ", value " + i)
.that(actual)
.isEqualTo(new StringValue("other" + j));
}
}
}
@Test
public void singleValueDependsOnManyValues() throws Exception {
SkyKey[] values = new SkyKey[TEST_NODE_COUNT];
StringBuilder expected = new StringBuilder();
for (int i = 0; i < values.length; i++) {
String iString = Integer.toString(i);
values[i] = nonHermeticKey(iString);
tester.set(values[i], new StringValue(iString));
expected.append(iString);
}
SkyKey rootKey = skyKey("root");
TestFunction value = tester.getOrCreate(rootKey).setComputedValue(CONCATENATE);
for (SkyKey skyKey : values) {
value.addDependency(skyKey);
}
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ false, rootKey);
assertThat(result.get(rootKey)).isEqualTo(new StringValue(expected.toString()));
for (int j = 0; j < 10; j++) {
expected.setLength(0);
for (int i = 0; i < values.length; i++) {
String s = "other" + i + " " + j;
tester.set(values[i], new StringValue(s));
expected.append(s);
}
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, rootKey);
assertThat(result.get(rootKey)).isEqualTo(new StringValue(expected.toString()));
}
}
@Test
public void twoRailLeftRightDependencies() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
String[] leftValues = new String[TEST_NODE_COUNT];
String[] rightValues = new String[TEST_NODE_COUNT];
for (int i = 0; i < leftValues.length; i++) {
leftValues[i] = "left-" + i;
rightValues[i] = "right-" + i;
if (i == 0) {
tester.getOrCreate(leftValues[i]).addDependency(leaf).setComputedValue(COPY);
tester.getOrCreate(rightValues[i]).addDependency(leaf).setComputedValue(COPY);
} else {
tester
.getOrCreate(leftValues[i])
.addDependency(leftValues[i - 1])
.addDependency(rightValues[i - 1])
.setComputedValue(new PassThroughSelected(skyKey(leftValues[i - 1])));
tester
.getOrCreate(rightValues[i])
.addDependency(leftValues[i - 1])
.addDependency(rightValues[i - 1])
.setComputedValue(new PassThroughSelected(skyKey(rightValues[i - 1])));
}
}
tester.set(leaf, new StringValue("leaf"));
String lastLeft = "left-" + (TEST_NODE_COUNT - 1);
String lastRight = "right-" + (TEST_NODE_COUNT - 1);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ false, lastLeft, lastRight);
assertThat(result.get(skyKey(lastLeft))).isEqualTo(new StringValue("leaf"));
assertThat(result.get(skyKey(lastRight))).isEqualTo(new StringValue("leaf"));
for (int j = 0; j < TESTED_NODES; j++) {
String value = "other" + j;
tester.set(leaf, new StringValue(value));
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, lastLeft, lastRight);
assertThat(result.get(skyKey(lastLeft))).isEqualTo(new StringValue(value));
assertThat(result.get(skyKey(lastRight))).isEqualTo(new StringValue(value));
}
}
@Test
public void noKeepGoingAfterKeepGoingCycle() throws Exception {
initializeTester();
SkyKey aKey = skyKey("a");
SkyKey bKey = skyKey("b");
SkyKey topKey = skyKey("top");
SkyKey midKey = skyKey("mid");
SkyKey goodKey = skyKey("good");
StringValue goodValue = new StringValue("good");
tester.set(goodKey, goodValue);
tester.getOrCreate(topKey).addDependency(midKey);
tester.getOrCreate(midKey).addDependency(aKey);
tester.getOrCreate(aKey).addDependency(bKey);
tester.getOrCreate(bKey).addDependency(aKey);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, topKey, goodKey);
assertThat(result.get(goodKey)).isEqualTo(goodValue);
assertThat(result.get(topKey)).isNull();
ErrorInfo errorInfo = result.getError(topKey);
CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder();
assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder();
}
tester.invalidate();
result = tester.eval(/*keepGoing=*/ false, topKey, goodKey);
assertThat(result.get(topKey)).isNull();
errorInfo = result.getError(topKey);
cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder();
assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder();
}
}
@Test
public void keepGoingCycleAlreadyPresent() throws Exception {
SkyKey selfEdge = skyKey("selfEdge");
tester.getOrCreate(selfEdge).addDependency(selfEdge).setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ true, selfEdge);
assertThatEvaluationResult(result).hasError();
CycleInfo cycleInfo = Iterables.getOnlyElement(result.getError(selfEdge).getCycleInfo());
if (cyclesDetected()) {
CycleInfoSubjectFactory.assertThat(cycleInfo).hasCycleThat().containsExactly(selfEdge);
CycleInfoSubjectFactory.assertThat(cycleInfo).hasPathToCycleThat().isEmpty();
}
SkyKey parent = skyKey("parent");
tester.getOrCreate(parent).addDependency(selfEdge).setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result2 = tester.eval(/*keepGoing=*/ true, parent);
assertThatEvaluationResult(result).hasError();
CycleInfo cycleInfo2 = Iterables.getOnlyElement(result2.getError(parent).getCycleInfo());
if (cyclesDetected()) {
CycleInfoSubjectFactory.assertThat(cycleInfo2).hasCycleThat().containsExactly(selfEdge);
CycleInfoSubjectFactory.assertThat(cycleInfo2).hasPathToCycleThat().containsExactly(parent);
}
}
private void changeCycle(boolean keepGoing) throws Exception {
SkyKey aKey = skyKey("a");
SkyKey bKey = nonHermeticKey("b");
SkyKey topKey = skyKey("top");
SkyKey midKey = skyKey("mid");
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(COPY);
tester.getOrCreate(midKey).addDependency(aKey).setComputedValue(COPY);
tester.getOrCreate(aKey).addDependency(bKey).setComputedValue(COPY);
tester.getOrCreate(bKey).addDependency(aKey);
EvaluationResult<StringValue> result = tester.eval(keepGoing, topKey);
assertThat(result.get(topKey)).isNull();
ErrorInfo errorInfo = result.getError(topKey);
CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder();
assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder();
}
tester.getOrCreate(bKey).removeDependency(aKey);
tester.set(bKey, new StringValue("bValue"));
tester.invalidate();
result = tester.eval(keepGoing, topKey);
assertThat(result.get(topKey)).isEqualTo(new StringValue("bValue"));
assertThat(result.getError(topKey)).isNull();
}
@Test
public void changeCycle_NoKeepGoing() throws Exception {
changeCycle(false);
}
@Test
public void changeCycle_KeepGoing() throws Exception {
changeCycle(true);
}
/**
* @see ParallelEvaluatorTest#cycleAboveIndependentCycle()
*/
@Test
public void cycleAboveIndependentCycle() throws Exception {
makeGraphDeterministic();
SkyKey aKey = skyKey("a");
SkyKey bKey = skyKey("b");
SkyKey cKey = nonHermeticKey("c");
SkyKey leafKey = nonHermeticKey("leaf");
// When aKey depends on leafKey and bKey,
tester
.getOrCreate(aKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
env.getValuesAndExceptions(ImmutableList.of(leafKey, bKey));
return null;
}
});
// And bKey depends on cKey,
tester.getOrCreate(bKey).addDependency(cKey);
// And cKey depends on aKey and bKey in that order,
tester.getOrCreate(cKey).addDependency(aKey).addDependency(bKey);
// And leafKey is a leaf node,
tester.set(leafKey, new StringValue("leafy"));
// Then when we evaluate,
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ true, aKey);
// aKey has an error,
assertThat(result.get(aKey)).isNull();
if (cyclesDetected()) {
// And both cycles were found underneath aKey: the (aKey->bKey->cKey) cycle, and the
// aKey->(bKey->cKey) cycle. This is because cKey depended on aKey and then bKey, so it pushed
// them down on the stack in that order, so bKey was processed first. It found its cycle, then
// popped off the stack, and then aKey was processed and found its cycle.
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(aKey)
.hasCycleInfoThat()
.containsExactly(
new CycleInfo(ImmutableList.of(aKey, bKey, cKey)),
new CycleInfo(ImmutableList.of(aKey), ImmutableList.of(bKey, cKey)));
} else {
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(aKey)
.hasCycleInfoThat()
.hasSize(1);
}
// When leafKey is changed, so that aKey will be marked as NEEDS_REBUILDING,
tester.set(leafKey, new StringValue("crunchy"));
// And cKey is invalidated, so that cycle checking will have to explore the full graph,
tester.getOrCreate(cKey, /*markAsModified=*/ true);
tester.invalidate();
// Then when we evaluate,
EvaluationResult<StringValue> result2 = tester.eval(/*keepGoing=*/ true, aKey);
// Things are just as before.
assertThat(result2.get(aKey)).isNull();
if (cyclesDetected()) {
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(aKey)
.hasCycleInfoThat()
.containsExactly(
new CycleInfo(ImmutableList.of(aKey, bKey, cKey)),
new CycleInfo(ImmutableList.of(aKey), ImmutableList.of(bKey, cKey)));
} else {
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(aKey)
.hasCycleInfoThat()
.hasSize(1);
}
}
/** Regression test: "crash in cycle checker with dirty values". */
@Test
public void cycleAndSelfEdgeWithDirtyValue() throws Exception {
initializeTester();
// The cycle detection algorithm non-deterministically traverses into children nodes, so
// use explicit determinism.
makeGraphDeterministic();
SkyKey cycleKey1 = nonHermeticKey("ZcycleKey1");
SkyKey cycleKey2 = skyKey("AcycleKey2");
tester
.getOrCreate(cycleKey1)
.addDependency(cycleKey2)
.addDependency(cycleKey1)
.setComputedValue(CONCATENATE);
tester.getOrCreate(cycleKey2).addDependency(cycleKey1).setComputedValue(COPY);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, cycleKey1);
assertThat(result.get(cycleKey1)).isNull();
ErrorInfo errorInfo = result.getError(cycleKey1);
CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1).inOrder();
assertThat(cycleInfo.getPathToCycle()).isEmpty();
}
tester.getOrCreate(cycleKey1, /*markAsModified=*/ true);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ true, cycleKey1, cycleKey2);
assertThat(result.get(cycleKey1)).isNull();
errorInfo = result.getError(cycleKey1);
cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1).inOrder();
assertThat(cycleInfo.getPathToCycle()).isEmpty();
}
cycleInfo =
Iterables.getOnlyElement(
tester.evaluator.getExistingErrorForTesting(cycleKey2).getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1).inOrder();
assertThat(cycleInfo.getPathToCycle()).containsExactly(cycleKey2).inOrder();
}
}
@Test
public void cycleAndSelfEdgeWithDirtyValueInSameGroup() throws Exception {
makeGraphDeterministic();
SkyKey cycleKey1 = skyKey("ZcycleKey1");
SkyKey cycleKey2 = skyKey("AcycleKey2");
tester.getOrCreate(cycleKey2).addDependency(cycleKey2).setComputedValue(CONCATENATE);
tester
.getOrCreate(cycleKey1)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
// The order here is important -- 2 before 1.
SkyframeLookupResult result =
env.getValuesAndExceptions(ImmutableList.of(cycleKey2, cycleKey1));
Preconditions.checkState(env.valuesMissing(), result);
return null;
}
});
// Evaluate twice to make sure nothing strange happens with invalidation the second time.
for (int i = 0; i < 2; i++) {
EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/ true, cycleKey1);
assertThat(result.get(cycleKey1)).isNull();
ErrorInfo errorInfo = result.getError(cycleKey1);
CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1).inOrder();
assertThat(cycleInfo.getPathToCycle()).isEmpty();
}
}
}
/** Regression test: "crash in cycle checker with dirty values". */
@Test
public void cycleWithDirtyValue() throws Exception {
SkyKey cycleKey1 = nonHermeticKey("cycleKey1");
SkyKey cycleKey2 = skyKey("cycleKey2");
tester.getOrCreate(cycleKey1).addDependency(cycleKey2).setComputedValue(COPY);
tester.getOrCreate(cycleKey2).addDependency(cycleKey1).setComputedValue(COPY);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, cycleKey1);
assertThat(result.get(cycleKey1)).isNull();
ErrorInfo errorInfo = result.getError(cycleKey1);
CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1, cycleKey2).inOrder();
assertThat(cycleInfo.getPathToCycle()).isEmpty();
}
tester.getOrCreate(cycleKey1, /*markAsModified=*/ true);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ true, cycleKey1);
assertThat(result.get(cycleKey1)).isNull();
errorInfo = result.getError(cycleKey1);
cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
if (cyclesDetected()) {
assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1, cycleKey2).inOrder();
assertThat(cycleInfo.getPathToCycle()).isEmpty();
}
}
/**
* {@link ParallelEvaluator} can be configured to not store errors alongside recovered values.
*
* @param errorsStoredAlongsideValues true if we expect Skyframe to store the error for the cycle
* in ErrorInfo. If true, supportsTransientExceptions must be true as well.
* @param supportsTransientExceptions true if we expect Skyframe to mark an ErrorInfo as transient
* for certain exception types.
* @param useTransientError true if the test should set the {@link TestFunction} it creates to
* throw a transient error.
*/
protected void parentOfCycleAndErrorInternal(
boolean errorsStoredAlongsideValues,
boolean supportsTransientExceptions,
boolean useTransientError)
throws Exception {
initializeTester();
if (errorsStoredAlongsideValues) {
Preconditions.checkArgument(supportsTransientExceptions);
}
SkyKey cycleKey1 = skyKey("cycleKey1");
SkyKey cycleKey2 = skyKey("cycleKey2");
SkyKey mid = skyKey("mid");
SkyKey errorKey = skyKey("errorKey");
tester.getOrCreate(cycleKey1).addDependency(cycleKey2).setComputedValue(COPY);
tester.getOrCreate(cycleKey2).addDependency(cycleKey1).setComputedValue(COPY);
TestFunction errorFunction = tester.getOrCreate(errorKey);
if (useTransientError) {
errorFunction.setHasTransientError(true);
} else {
errorFunction.setHasError(true);
}
tester
.getOrCreate(mid)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(COPY);
SkyKey top = skyKey("top");
CountDownLatch topEvaluated = new CountDownLatch(2);
tester
.getOrCreate(top)
.setBuilder(
new ChainedFunction(
topEvaluated,
null,
null,
false,
new StringValue("unused"),
ImmutableList.of(mid, cycleKey1)));
EvaluationResult<StringValue> evalResult = tester.eval(true, top);
assertThatEvaluationResult(evalResult).hasError();
ErrorInfo errorInfo = evalResult.getError(top);
assertThat(topEvaluated.getCount()).isEqualTo(1);
if (errorsStoredAlongsideValues) {
if (useTransientError) {
// The parent should be transitively transient, since it transitively depends on a transient
// error.
assertThat(errorInfo.isTransitivelyTransient()).isTrue();
} else {
assertThatErrorInfo(errorInfo).isNotTransient();
}
assertThat(errorInfo.getException())
.hasMessageThat()
.isEqualTo(NODE_TYPE.getName() + ":errorKey");
} else {
// When errors are not stored alongside values, transient errors that are recovered from do
// not make the parent transient
if (supportsTransientExceptions) {
assertThatErrorInfo(errorInfo).isTransient();
assertThatErrorInfo(errorInfo).hasExceptionThat().isNotNull();
} else {
assertThatErrorInfo(errorInfo).isNotTransient();
assertThatErrorInfo(errorInfo).hasExceptionThat().isNull();
}
}
if (cyclesDetected()) {
assertThatErrorInfo(errorInfo)
.hasCycleInfoThat()
.containsExactly(
new CycleInfo(ImmutableList.of(top), ImmutableList.of(cycleKey1, cycleKey2)));
} else {
assertThatErrorInfo(errorInfo).hasCycleInfoThat().hasSize(1);
}
// But the parent itself shouldn't have a direct dep on the special error transience node.
assertThatEvaluationResult(evalResult)
.hasDirectDepsInGraphThat(top)
.doesNotContain(ErrorTransienceValue.KEY);
}
@Test
public void parentOfCycleAndError() throws Exception {
parentOfCycleAndErrorInternal(
/*errorsStoredAlongsideValues=*/ true,
/*supportsTransientExceptions=*/ true,
/*useTransientError=*/ true);
}
/**
* Regression test: IllegalStateException in BuildingState.isReady(). The ParallelEvaluator used
* to assume during cycle-checking that all values had been built as fully as possible -- that
* evaluation had not been interrupted. However, we also do cycle-checking in nokeep-going mode
* when a value throws an error (possibly prematurely shutting down evaluation) but that error
* then bubbles up into a cycle.
*
* <p>We want to achieve the following state: we are checking for a cycle; the value we examine
* has not yet finished checking its children to see if they are dirty; but all children checked
* so far have been unchanged. This value is "otherTop". We first build otherTop, then mark its
* first child changed (without actually changing it), and then do a second build. On the second
* build, we also build "top", which requests a cycle that depends on an error. We wait to signal
* otherTop that its first child is done until the error throws and shuts down evaluation. The
* error then bubbles up to the cycle, and so the bubbling is aborted. Finally, cycle checking
* happens, and otherTop is examined, as desired.
*/
@Test
public void cycleAndErrorAndReady() throws Exception {
// This value will not have finished building on the second build when the error is thrown.
SkyKey otherTop = skyKey("otherTop");
SkyKey errorKey = skyKey("error");
// Is the graph state all set up and ready for the error to be thrown? The three values are
// exceptionMarker, cycle2Key, and dep1 (via signaling otherTop).
CountDownLatch valuesReady = new CountDownLatch(3);
// Is evaluation being shut down? This is counted down by the exceptionMarker's builder, after
// it has waited for the threadpool's exception latch to be released.
CountDownLatch errorThrown = new CountDownLatch(1);
// We don't do anything on the first build.
AtomicBoolean secondBuild = new AtomicBoolean(false);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!secondBuild.get()) {
return;
}
if (key.equals(otherTop) && type == EventType.SIGNAL) {
// otherTop is being signaled that dep1 is done. Tell the error value that it is ready,
// then wait until the error is thrown, so that otherTop's builder is not re-entered.
valuesReady.countDown();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(errorThrown, "error not thrown");
}
},
/* deterministic= */ true);
SkyKey dep1 = nonHermeticKey("dep1");
tester.set(dep1, new StringValue("dep1"));
SkyKey dep2 = skyKey("dep2");
tester.set(dep2, new StringValue("dep2"));
// otherTop should request the deps one at a time, so that it can be in the CHECK_DEPENDENCIES
// state even after one dep is re-evaluated.
tester
.getOrCreate(otherTop)
.setBuilder(
(skyKey, env) -> {
env.getValue(dep1);
if (env.valuesMissing()) {
return null;
}
env.getValue(dep2);
return env.valuesMissing() ? null : new StringValue("otherTop");
});
// Prime the graph with otherTop, so we can dirty it next build.
assertThat(tester.evalAndGet(/* keepGoing= */ false, otherTop))
.isEqualTo(new StringValue("otherTop"));
// Mark dep1 changed, so otherTop will be dirty and request re-evaluation of dep1.
tester.getOrCreate(dep1, /* markAsModified= */ true);
SkyKey topKey = skyKey("top");
// Note that since DeterministicHelper alphabetizes reverse deps, it is important that
// "cycle2" comes before "top".
SkyKey cycle1Key = skyKey("cycle1");
SkyKey cycle2Key = skyKey("cycle2");
tester.getOrCreate(topKey).addDependency(cycle1Key).setComputedValue(CONCATENATE);
tester
.getOrCreate(cycle1Key)
.addDependency(errorKey)
.addDependency(cycle2Key)
.setComputedValue(CONCATENATE);
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/* notifyStart= */ null,
/* waitToFinish= */ valuesReady,
/* notifyFinish= */ null,
/* waitForException= */ false,
/* value= */ null,
ImmutableList.of()));
// Make sure cycle2Key has declared its dependence on cycle1Key before error throws.
tester
.getOrCreate(cycle2Key)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ valuesReady,
null,
null,
false,
new StringValue("never returned"),
ImmutableList.of(cycle1Key)));
// Value that waits until an exception is thrown to finish building. We use it just to be
// informed when the threadpool is shutting down.
SkyKey exceptionMarker = skyKey("exceptionMarker");
tester
.getOrCreate(exceptionMarker)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ valuesReady,
/*waitToFinish=*/ new CountDownLatch(0),
/*notifyFinish=*/ errorThrown,
/*waitForException=*/ true,
new StringValue("exception marker"),
ImmutableList.of()));
tester.invalidate();
secondBuild.set(true);
// otherTop must be first, since we check top-level values for cycles in the order in which
// they appear here.
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, otherTop, topKey, exceptionMarker);
Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo();
assertWithMessage(result.toString()).that(cycleInfos).isNotEmpty();
CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos);
if (cyclesDetected()) {
assertThat(result.errorMap().keySet()).containsExactly(topKey);
assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey);
assertThat(cycleInfo.getCycle()).containsExactly(cycle1Key, cycle2Key);
}
}
@Test
public void breakCycle() throws Exception {
SkyKey aKey = nonHermeticKey("a");
SkyKey bKey = nonHermeticKey("b");
// When aKey and bKey depend on each other,
tester.getOrCreate(aKey).addDependency(bKey);
tester.getOrCreate(bKey).addDependency(aKey);
// And they are evaluated,
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, aKey, bKey);
// Then the evaluation is in error,
assertThatEvaluationResult(result).hasError();
// And each node has the expected cycle.
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(aKey)
.hasCycleInfoThat()
.isNotEmpty();
CycleInfo aCycleInfo = Iterables.getOnlyElement(result.getError(aKey).getCycleInfo());
if (cyclesDetected()) {
assertThat(aCycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder();
assertThat(aCycleInfo.getPathToCycle()).isEmpty();
}
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(bKey)
.hasCycleInfoThat()
.isNotEmpty();
CycleInfo bCycleInfo = Iterables.getOnlyElement(result.getError(bKey).getCycleInfo());
if (cyclesDetected()) {
assertThat(bCycleInfo.getCycle()).containsExactly(bKey, aKey).inOrder();
assertThat(bCycleInfo.getPathToCycle()).isEmpty();
}
// When both dependencies are broken,
tester.getOrCreate(bKey).removeDependency(aKey);
tester.set(bKey, new StringValue("bValue"));
tester.getOrCreate(aKey).removeDependency(bKey);
tester.set(aKey, new StringValue("aValue"));
tester.invalidate();
// And the nodes are re-evaluated,
result = tester.eval(/*keepGoing=*/ true, aKey, bKey);
// Then evaluation is successful and the nodes have the expected values.
assertThatEvaluationResult(result).hasEntryThat(aKey).isEqualTo(new StringValue("aValue"));
assertThatEvaluationResult(result).hasEntryThat(bKey).isEqualTo(new StringValue("bValue"));
}
@Test
public void nodeInvalidatedThenDoubleCycle() throws InterruptedException {
makeGraphDeterministic();
// When topKey depends on depKey, and both are top-level nodes in the graph,
SkyKey topKey = nonHermeticKey("bKey");
SkyKey depKey = nonHermeticKey("aKey");
tester.getOrCreate(topKey).addDependency(depKey).setConstantValue(new StringValue("a"));
tester.getOrCreate(depKey).setConstantValue(new StringValue("b"));
// Then evaluation is as expected.
EvaluationResult<StringValue> result1 = tester.eval(/* keepGoing= */ true, topKey, depKey);
assertThatEvaluationResult(result1).hasEntryThat(topKey).isEqualTo(new StringValue("a"));
assertThatEvaluationResult(result1).hasEntryThat(depKey).isEqualTo(new StringValue("b"));
assertThatEvaluationResult(result1).hasNoError();
// When both nodes acquire self-edges, with topKey still also depending on depKey, in the same
// group,
tester.getOrCreate(depKey, /* markAsModified= */ true).addDependency(depKey);
tester
.getOrCreate(topKey, /* markAsModified= */ true)
.setConstantValue(null)
.removeDependency(depKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
// Order depKey first - makeGraphDeterministic() only makes the batch maps returned
// by the graph deterministic, not the order of temporary direct deps. This makes
// the order of deps match (alphabetized by the string representation).
env.getValuesAndExceptions(ImmutableList.of(depKey, topKey));
assertThat(env.valuesMissing()).isTrue();
return null;
}
});
tester.invalidate();
// Then evaluation is as expected -- topKey has removed its dep on depKey (since depKey was not
// done when topKey found its cycle), and both topKey and depKey have cycles.
EvaluationResult<StringValue> result2 = tester.eval(/*keepGoing=*/ true, topKey, depKey);
if (cyclesDetected()) {
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(topKey)
.hasCycleInfoThat()
.containsExactly(new CycleInfo(ImmutableList.of(topKey)));
assertThatEvaluationResult(result2).hasDirectDepsInGraphThat(topKey).containsExactly(topKey);
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(depKey)
.hasCycleInfoThat()
.containsExactly(new CycleInfo(ImmutableList.of(depKey)));
} else {
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(topKey)
.hasCycleInfoThat()
.hasSize(1);
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(depKey)
.hasCycleInfoThat()
.hasSize(1);
}
// When the nodes return to their original, error-free state,
tester
.getOrCreate(topKey, /*markAsModified=*/ true)
.setBuilder(null)
.addDependency(depKey)
.setConstantValue(new StringValue("a"));
tester.getOrCreate(depKey, /*markAsModified=*/ true).removeDependency(depKey);
tester.invalidate();
// Then evaluation is as expected.
EvaluationResult<StringValue> result3 = tester.eval(/*keepGoing=*/ true, topKey, depKey);
assertThatEvaluationResult(result3).hasEntryThat(topKey).isEqualTo(new StringValue("a"));
assertThatEvaluationResult(result3).hasEntryThat(depKey).isEqualTo(new StringValue("b"));
assertThatEvaluationResult(result3).hasNoError();
}
@SuppressWarnings("PreferJavaTimeOverload")
@Test
public void limitEvaluatorThreads() throws Exception {
initializeTester();
int numKeys = 10;
Object lock = new Object();
AtomicInteger inProgressCount = new AtomicInteger();
int[] maxValue = {0};
SkyKey topLevel = skyKey("toplevel");
TestFunction topLevelBuilder = tester.getOrCreate(topLevel);
for (int i = 0; i < numKeys; i++) {
topLevelBuilder.addDependency("subKey" + i);
tester
.getOrCreate("subKey" + i)
.setComputedValue(
(deps, env) -> {
int val = inProgressCount.incrementAndGet();
synchronized (lock) {
if (val > maxValue[0]) {
maxValue[0] = val;
}
}
Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
inProgressCount.decrementAndGet();
return new StringValue("abc");
});
}
topLevelBuilder.setConstantValue(new StringValue("xyz"));
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ true, /*numThreads=*/ 5, topLevel);
assertThat(result.hasError()).isFalse();
assertThat(maxValue[0]).isEqualTo(5);
}
@Test
public void nodeIsChangedWithoutBeingEvaluated() throws Exception {
SkyKey buildFile = nonHermeticKey("buildfile");
SkyKey top = skyKey("top");
SkyKey dep = nonHermeticKey("dep");
tester.set(buildFile, new StringValue("depend on dep"));
StringValue depVal = new StringValue("this is dep");
tester.set(dep, depVal);
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SkyFunctionException, InterruptedException {
StringValue val = (StringValue) env.getValue(buildFile);
if (env.valuesMissing()) {
return null;
}
if (val.getValue().equals("depend on dep")) {
StringValue result = (StringValue) env.getValue(dep);
return env.valuesMissing() ? null : result;
}
throw new GenericFunctionException(
new SomeErrorException("bork"), Transience.PERSISTENT);
}
});
assertThat(tester.evalAndGet("top")).isEqualTo(depVal);
StringValue newDepVal = new StringValue("this is new dep");
tester.set(dep, newDepVal);
tester.set(buildFile, new StringValue("don't depend on dep"));
tester.invalidate();
tester.eval(/*keepGoing=*/ false, top);
tester.set(buildFile, new StringValue("depend on dep"));
tester.invalidate();
assertThat(tester.evalAndGet("top")).isEqualTo(newDepVal);
}
/**
* Regression test: error on clearMaybeDirtyValue. We do an evaluation of topKey, which registers
* dependencies on midKey and errorKey. midKey enqueues slowKey, and waits. errorKey throws an
* error, which bubbles up to topKey. If topKey does not unregister its dependence on midKey, it
* will have a dangling reference to midKey after unfinished values are cleaned from the graph.
* Note that slowKey will wait until errorKey has thrown and the threadpool has caught the
* exception before returning, so the Evaluator will already have stopped enqueuing new jobs, so
* midKey is not evaluated.
*/
@Test
public void incompleteDirectDepsAreClearedBeforeInvalidation() throws Exception {
CountDownLatch slowStart = new CountDownLatch(1);
CountDownLatch errorFinish = new CountDownLatch(1);
SkyKey errorKey = nonHermeticKey("error");
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ slowStart,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
SkyKey slowKey = skyKey("slow");
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ slowStart,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("slow"),
/*deps=*/ ImmutableList.of()));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
SkyKey topKey = skyKey("top");
tester
.getOrCreate(topKey)
.addDependency(midKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
// slowKey starts -> errorKey finishes, written to graph -> slowKey finishes & (Visitor aborts)
// -> topKey builds.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
// Make sure midKey didn't finish building.
assertThat(tester.getExistingValue(midKey)).isNull();
// Give slowKey a nice ordinary builder.
tester
.getOrCreate(slowKey, /*markAsModified=*/ false)
.setBuilder(null)
.setConstantValue(new StringValue("slow"));
// Put midKey into the graph. It won't have a reverse dependence on topKey.
tester.evalAndGet(/*keepGoing=*/ false, midKey);
tester.differencer.invalidate(ImmutableList.of(errorKey));
// topKey should not access midKey as if it were already registered as a dependency.
tester.eval(/*keepGoing=*/ false, topKey);
}
/**
* Regression test: error on clearMaybeDirtyValue. Same as the previous test, but the second
* evaluation is keepGoing, which should cause an access of the children of topKey.
*/
@Test
public void incompleteDirectDepsAreClearedBeforeKeepGoing() throws Exception {
initializeTester();
CountDownLatch slowStart = new CountDownLatch(1);
CountDownLatch errorFinish = new CountDownLatch(1);
SkyKey errorKey = skyKey("error");
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ slowStart,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
SkyKey slowKey = skyKey("slow");
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ slowStart,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("slow"),
/*deps=*/ ImmutableList.of()));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
SkyKey topKey = skyKey("top");
tester
.getOrCreate(topKey)
.addDependency(midKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
// slowKey starts -> errorKey finishes, written to graph -> slowKey finishes & (Visitor aborts)
// -> topKey builds.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
// Make sure midKey didn't finish building.
assertThat(tester.getExistingValue(midKey)).isNull();
// Give slowKey a nice ordinary builder.
tester
.getOrCreate(slowKey, /*markAsModified=*/ false)
.setBuilder(null)
.setConstantValue(new StringValue("slow"));
// Put midKey into the graph. It won't have a reverse dependence on topKey.
tester.evalAndGet(/*keepGoing=*/ false, midKey);
// topKey should not access midKey as if it were already registered as a dependency.
// We don't invalidate errors, but because topKey wasn't actually written to the graph last
// build, it should be rebuilt here.
tester.eval(/*keepGoing=*/ true, topKey);
}
/**
* Regression test: tests that pass before other build actions fail yield crash in non -k builds.
*/
private void passThenFailToBuild(boolean successFirst) throws Exception {
CountDownLatch blocker = new CountDownLatch(1);
SkyKey successKey = skyKey("success");
tester
.getOrCreate(successKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ null,
/*notifyFinish=*/ blocker,
/*waitForException=*/ false,
new StringValue("yippee"),
/*deps=*/ ImmutableList.of()));
SkyKey slowFailKey = skyKey("slow_then_fail");
tester
.getOrCreate(slowFailKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ blocker,
/*notifyFinish=*/ null,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
EvaluationResult<StringValue> result;
if (successFirst) {
result = tester.eval(/*keepGoing=*/ false, successKey, slowFailKey);
} else {
result = tester.eval(/*keepGoing=*/ false, slowFailKey, successKey);
}
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(slowFailKey);
assertThat(result.values()).containsExactly(new StringValue("yippee"));
}
@Test
public void passThenFailToBuild() throws Exception {
passThenFailToBuild(true);
}
@Test
public void passThenFailToBuildAlternateOrder() throws Exception {
passThenFailToBuild(false);
}
@Test
public void incompleteDirectDepsForDirtyValue() throws Exception {
SkyKey topKey = nonHermeticKey("top");
tester.set(topKey, new StringValue("initial"));
// Put topKey into graph so it will be dirtied on next run.
assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey))
.isEqualTo(new StringValue("initial"));
CountDownLatch slowStart = new CountDownLatch(1);
CountDownLatch errorFinish = new CountDownLatch(1);
SkyKey errorKey = skyKey("error");
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ slowStart,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
SkyKey slowKey = skyKey("slow");
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ slowStart,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("slow"),
/*deps=*/ ImmutableList.of()));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
tester.set(topKey, null);
tester
.getOrCreate(topKey)
.addDependency(midKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
tester.invalidate();
// slowKey starts -> errorKey finishes, written to graph -> slowKey finishes & (Visitor aborts)
// -> topKey builds.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
// Make sure midKey didn't finish building.
assertThat(tester.getExistingValue(midKey)).isNull();
// Give slowKey a nice ordinary builder.
tester
.getOrCreate(slowKey, /*markAsModified=*/ false)
.setBuilder(null)
.setConstantValue(new StringValue("slow"));
// Put midKey into the graph. It won't have a reverse dependence on topKey.
tester.evalAndGet(/*keepGoing=*/ false, midKey);
// topKey should not access midKey as if it were already registered as a dependency.
// We don't invalidate errors, but since topKey wasn't actually written to the graph before, it
// will be rebuilt.
tester.eval(/*keepGoing=*/ true, topKey);
}
@Test
public void continueWithErrorDep() throws Exception {
SkyKey afterKey = nonHermeticKey("after");
SkyKey errorKey = skyKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
tester.set(afterKey, new StringValue("after"));
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE)
.addDependency(afterKey);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredafter");
tester.set(afterKey, new StringValue("before"));
tester.invalidate();
result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredbefore");
}
@Test
public void continueWithErrorDepTurnedGood() throws Exception {
SkyKey errorKey = nonHermeticKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
tester.set("after", new StringValue("after"));
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE)
.addDependency("after");
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredafter");
tester.set(errorKey, new StringValue("reformed")).setHasError(false);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("reformedafter");
}
@Test
public void errorDepAlreadyThereThenTurnedGood() throws Exception {
SkyKey errorKey = nonHermeticKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setHasError(true);
// Prime the graph by putting the error value in it beforehand.
assertThat(tester.evalAndGetError(/*keepGoing=*/ true, errorKey)).isNotNull();
// Request the parent.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(parentKey);
// Change the error value to no longer throw.
tester.set(errorKey, new StringValue("reformed")).setHasError(false);
tester
.getOrCreate(parentKey, /*markAsModified=*/ false)
.setHasError(false)
.setComputedValue(COPY);
tester.differencer.invalidate(ImmutableList.of(errorKey));
tester.invalidate();
// Request the parent again. This time it should succeed.
result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("reformed");
// Confirm that the parent no longer depends on the error transience value -- make it
// unbuildable again, but without invalidating it, and invalidate transient errors. The parent
// should not be rebuilt.
tester.getOrCreate(parentKey, /*markAsModified=*/ false).setHasError(true);
tester.invalidateTransientErrors();
result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.errorMap()).isEmpty();
assertThat(result.get(parentKey).getValue()).isEqualTo("reformed");
}
/**
* Regression test for 2014 bug: error transience value is registered before newly requested deps.
* A value requests a child, gets it back immediately, and then throws, causing the error
* transience value to be registered as a dep. The following build, the error is invalidated via
* that child.
*/
@Test
public void doubleDepOnErrorTransienceValue() throws Exception {
SkyKey leafKey = nonHermeticKey("leaf");
tester.set(leafKey, new StringValue("leaf"));
// Prime the graph by putting leaf in beforehand.
assertThat(tester.evalAndGet(/*keepGoing=*/ false, leafKey)).isEqualTo(new StringValue("leaf"));
SkyKey topKey = skyKey("top");
tester.getOrCreate(topKey).addDependency(leafKey).setHasError(true);
// Build top -- it has an error.
tester.evalAndGetError(/*keepGoing=*/ true, topKey);
// Invalidate top via leaf, and rebuild.
tester.set(leafKey, new StringValue("leaf2"));
tester.invalidate();
tester.evalAndGetError(/*keepGoing=*/ true, topKey);
}
/** Regression test for crash bug. */
@Test
public void errorTransienceDepCleared() throws Exception {
SkyKey top = skyKey("top");
SkyKey leaf = nonHermeticKey("leaf");
tester.set(leaf, new StringValue("leaf"));
tester.getOrCreate(top).addDependency(leaf).setHasTransientError(true);
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ false, top);
assertWithMessage(result.toString()).that(result.hasError()).isTrue();
tester.getOrCreate(leaf, /* markAsModified= */ true);
tester.invalidate();
SkyKey irrelevant = skyKey("irrelevant");
tester.set(irrelevant, new StringValue("irrelevant"));
tester.eval(/* keepGoing= */ true, irrelevant);
tester.invalidateTransientErrors();
result = tester.eval(/*keepGoing=*/ true, top);
assertWithMessage(result.toString()).that(result.hasError()).isTrue();
}
@Test
public void incompleteValueAlreadyThereNotUsed() throws Exception {
initializeTester();
SkyKey errorKey = skyKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
SkyKey midKey = skyKey("mid");
tester
.getOrCreate(midKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(COPY);
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(midKey, new StringValue("don't use this"))
.setComputedValue(COPY);
// Prime the graph by evaluating the mid-level value. It shouldn't be stored in the graph
// because it was only called during the bubbling-up phase.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, midKey);
assertThat(result.get(midKey)).isNull();
assertThatEvaluationResult(result).hasSingletonErrorThat(midKey);
// In a keepGoing build, midKey should be re-evaluated.
assertThat(((StringValue) tester.evalAndGet(/*keepGoing=*/ true, parentKey)).getValue())
.isEqualTo("recovered");
}
/**
* "top" requests a dependency group in which the first value, called "error", throws an
* exception, so "mid" and "mid2", which depend on "slow", never get built.
*/
@Test
public void errorInDependencyGroup() throws Exception {
SkyKey topKey = skyKey("top");
CountDownLatch slowStart = new CountDownLatch(1);
CountDownLatch errorFinish = new CountDownLatch(1);
SkyKey errorKey = nonHermeticKey("error");
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ slowStart,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
// ChainedFunction throws when value is null.
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
SkyKey slowKey = skyKey("slow");
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ slowStart,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("slow"),
/*deps=*/ ImmutableList.of()));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
SkyKey mid2Key = skyKey("mid2");
tester.getOrCreate(mid2Key).addDependency(slowKey).setComputedValue(COPY);
tester.set(topKey, null);
tester
.getOrCreate(topKey)
.setBuilder(
(skyKey, env) -> {
env.getValuesAndExceptions(ImmutableList.of(errorKey, midKey, mid2Key));
if (env.valuesMissing()) {
return null;
}
return new StringValue("top");
});
// Assert that build fails and "error" really is in error.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThat(result.hasError()).isTrue();
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey);
// Ensure that evaluation succeeds if errorKey does not throw an error.
tester.getOrCreate(errorKey).setBuilder(null);
tester.set(errorKey, new StringValue("ok"));
tester.invalidate();
assertThat(tester.evalAndGet("top")).isEqualTo(new StringValue("top"));
}
/**
* Regression test -- if value top requests {depA, depB}, depC, with depA and depC there and depB
* absent, and then throws an exception, the stored deps should be depA, depC (in different
* groups), not {depA, depC} (same group).
*/
@Test
public void valueInErrorWithGroups() throws Exception {
SkyKey topKey = skyKey("top");
SkyKey groupDepA = nonHermeticKey("groupDepA");
SkyKey groupDepB = skyKey("groupDepB");
SkyKey depC = nonHermeticKey("depC");
tester.set(groupDepA, new SkyKeyValue(depC));
tester.set(groupDepB, new StringValue(""));
tester.getOrCreate(depC).setHasError(true);
tester
.getOrCreate(topKey)
.setBuilder(
(skyKey, env) -> {
SkyKeyValue val =
(SkyKeyValue)
env.getValuesAndExceptions(ImmutableList.of(groupDepA, groupDepB))
.get(groupDepA);
if (env.valuesMissing()) {
return null;
}
try {
env.getValueOrThrow(val.key, SomeErrorException.class);
} catch (SomeErrorException e) {
throw new GenericFunctionException(e, Transience.PERSISTENT);
}
return env.valuesMissing() ? null : new StringValue("top");
});
EvaluationResult<SkyValue> evaluationResult = tester.eval(/*keepGoing=*/ true, groupDepA, depC);
assertThat(((SkyKeyValue) evaluationResult.get(groupDepA)).key).isEqualTo(depC);
assertThatEvaluationResult(evaluationResult).hasErrorEntryForKeyThat(depC);
evaluationResult = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(evaluationResult).hasErrorEntryForKeyThat(topKey);
tester.set(groupDepA, new SkyKeyValue(groupDepB));
tester.getOrCreate(depC, /*markAsModified=*/ true);
tester.invalidate();
evaluationResult = tester.eval(/*keepGoing=*/ false, topKey);
assertWithMessage(evaluationResult.toString()).that(evaluationResult.hasError()).isFalse();
assertThat(evaluationResult.get(topKey)).isEqualTo(new StringValue("top"));
}
private static class SkyKeyValue implements SkyValue {
private final SkyKey key;
private SkyKeyValue(SkyKey key) {
this.key = key;
}
}
@Test
public void errorOnlyEmittedOnce() throws Exception {
initializeTester();
tester.set("x", new StringValue("y")).setWarning("fizzlepop");
StringValue value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
assertThatEvents(eventCollector).containsExactly("fizzlepop");
tester.invalidate();
value = (StringValue) tester.evalAndGet("x");
assertThat(value.getValue()).isEqualTo("y");
// No new events emitted.
}
/**
* We are checking here that we are resilient to a race condition in which a value that is
* checking its children for dirtiness is signaled by all of its children, putting it in a ready
* state, before the thread has terminated. Optionally, one of its children may throw an error,
* shutting down the threadpool. The essential race is that a child about to throw signals its
* parent and the parent's builder restarts itself before the exception is thrown. Here, the
* signaling happens while dirty dependencies are being checked. We control the timing by blocking
* "top"'s registering itself on its deps.
*/
private void dirtyChildEnqueuesParentDuringCheckDependencies(boolean throwError)
throws Exception {
// Value to be built. It will be signaled to rebuild before it has finished checking its deps.
SkyKey top = skyKey("a_top");
// otherTop is alphabetically after top.
SkyKey otherTop = skyKey("z_otherTop");
// Dep that blocks before it acknowledges being added as a dep by top, so the firstKey value has
// time to signal top. (Importantly its key is alphabetically after 'firstKey').
SkyKey slowAddingDep = skyKey("slowDep");
// Value that is modified on the second build. Its thread won't finish until it signals top,
// which will wait for the signal before it enqueues its next dep. We prevent the thread from
// finishing by having the graph listener block on the second reverse dep to signal.
SkyKey firstKey = nonHermeticKey("first");
tester.set(firstKey, new StringValue("biding"));
// Don't perform any blocking on the first build.
AtomicBoolean delayTopSignaling = new AtomicBoolean(false);
CountDownLatch topSignaled = new CountDownLatch(1);
CountDownLatch topRequestedDepOrRestartedBuild = new CountDownLatch(1);
CountDownLatch parentsRequested = new CountDownLatch(2);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!delayTopSignaling.get()) {
return;
}
if (key.equals(otherTop) && type == EventType.SIGNAL) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
topRequestedDepOrRestartedBuild, "top's builder did not start in time");
return;
}
if (key.equals(firstKey) && type == EventType.ADD_REVERSE_DEP && order == Order.AFTER) {
parentsRequested.countDown();
return;
}
if (key.equals(firstKey) && type == EventType.CHECK_IF_DONE && order == Order.AFTER) {
parentsRequested.countDown();
if (throwError) {
topRequestedDepOrRestartedBuild.countDown();
}
return;
}
if (key.equals(top) && type == EventType.SIGNAL && order == Order.AFTER) {
// top is signaled by firstKey (since slowAddingDep is blocking), so slowAddingDep
// is now free to acknowledge top as a parent.
topSignaled.countDown();
return;
}
if (key.equals(firstKey) && type == EventType.SET_VALUE && order == Order.BEFORE) {
// Make sure both parents add themselves as rdeps.
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
parentsRequested, "parents did not request dep in time");
}
if (key.equals(slowAddingDep)
&& type == EventType.CHECK_IF_DONE
&& top.equals(context)
&& order == Order.BEFORE) {
// If top is trying to declare a dep on slowAddingDep, wait until firstKey has
// signaled top. Then this add dep will return DONE and top will be signaled,
// making it ready, so it will be enqueued.
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
topSignaled, "first key didn't signal top in time");
}
},
/* deterministic= */ true);
tester.set(slowAddingDep, new StringValue("dep"));
AtomicInteger numTopInvocations = new AtomicInteger(0);
tester
.getOrCreate(top)
.setBuilder(
(key, env) -> {
numTopInvocations.incrementAndGet();
if (delayTopSignaling.get()) {
// The graph listener will block on firstKey's signaling of otherTop above until
// this thread starts running.
topRequestedDepOrRestartedBuild.countDown();
}
// top's builder just requests both deps in a group.
env.getValuesAndExceptions(ImmutableList.of(firstKey, slowAddingDep));
return env.valuesMissing() ? null : new StringValue("top");
});
// First build : just prime the graph.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, top);
assertThat(result.hasError()).isFalse();
assertThat(result.get(top)).isEqualTo(new StringValue("top"));
assertThat(numTopInvocations.get()).isEqualTo(2);
// Now dirty the graph, and maybe have firstKey throw an error.
if (throwError) {
tester
.getOrCreate(firstKey, /*markAsModified=*/ true)
.setConstantValue(null)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SkyFunctionException {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
parentsRequested, "both parents didn't request in time");
throw new GenericFunctionException(
new SomeErrorException(firstKey.toString()), Transience.PERSISTENT);
}
});
} else {
tester
.getOrCreate(firstKey, /*markAsModified=*/ true)
.setConstantValue(new StringValue("new"));
}
tester.getOrCreate(otherTop).addDependency(firstKey).setComputedValue(CONCATENATE);
tester.invalidate();
delayTopSignaling.set(true);
result = tester.eval(/*keepGoing=*/ false, top, otherTop);
if (throwError) {
assertThatEvaluationResult(result).hasError();
assertThat(result.keyNames()).isEmpty(); // No successfully evaluated values.
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(top);
assertWithMessage(
"on the incremental build, top's builder should have only been used in error "
+ "bubbling")
.that(numTopInvocations.get())
.isEqualTo(3);
} else {
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("top"));
assertThatEvaluationResult(result).hasNoError();
assertWithMessage(
"on the incremental build, top's builder should have only been executed once in "
+ "normal evaluation")
.that(numTopInvocations.get())
.isEqualTo(3);
}
assertThat(topSignaled.getCount()).isEqualTo(0);
assertThat(topRequestedDepOrRestartedBuild.getCount()).isEqualTo(0);
}
@Test
public void dirtyChildEnqueuesParentDuringCheckDependencies_ThrowDoesntEnqueue()
throws Exception {
dirtyChildEnqueuesParentDuringCheckDependencies(/*throwError=*/ true);
}
@Test
public void dirtyChildEnqueuesParentDuringCheckDependencies_NoThrow() throws Exception {
dirtyChildEnqueuesParentDuringCheckDependencies(/*throwError=*/ false);
}
@Test
public void removeReverseDepFromRebuildingNode() throws Exception {
SkyKey topKey = skyKey("top");
SkyKey midKey = nonHermeticKey("mid");
SkyKey changedKey = nonHermeticKey("changed");
tester.getOrCreate(changedKey).setConstantValue(new StringValue("first"));
// When top depends on mid,
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
// And mid depends on changed,
tester.getOrCreate(midKey).addDependency(changedKey).setComputedValue(CONCATENATE);
CountDownLatch changedKeyStarted = new CountDownLatch(1);
CountDownLatch changedKeyCanFinish = new CountDownLatch(1);
AtomicBoolean controlTiming = new AtomicBoolean(false);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!controlTiming.get()) {
return;
}
if (key.equals(midKey) && type == EventType.CHECK_IF_DONE && order == Order.BEFORE) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
changedKeyStarted, "changed key didn't start");
} else if (key.equals(changedKey)
&& type == EventType.REMOVE_REVERSE_DEP
&& order == Order.AFTER
&& midKey.equals(context)) {
changedKeyCanFinish.countDown();
}
},
/* deterministic= */ false);
// Then top builds as expected.
assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey)).isEqualTo(new StringValue("first"));
// When changed is modified,
tester
.getOrCreate(changedKey, /*markAsModified=*/ true)
.setConstantValue(null)
.setBuilder(
// And changed is not allowed to finish building until it is released,
new ChainedFunction(
changedKeyStarted,
changedKeyCanFinish,
null,
false,
new StringValue("second"),
ImmutableList.of()));
// And mid is independently marked as modified,
tester
.getOrCreate(midKey, /*markAsModified=*/ true)
.removeDependency(changedKey)
.setComputedValue(null)
.setConstantValue(new StringValue("mid"));
tester.invalidate();
SkyKey newTopKey = skyKey("newTop");
// And changed will start rebuilding independently of midKey, because it's requested directly by
// newTop
tester.getOrCreate(newTopKey).addDependency(changedKey).setComputedValue(CONCATENATE);
// And we control the timing using the graph listener above to make sure that:
// (1) before we do anything with mid, changed has already started, and
// (2) changed key can't finish until mid tries to remove its reverse dep from changed,
controlTiming.set(true);
// Then this evaluation completes without crashing.
tester.eval(/*keepGoing=*/ false, newTopKey, topKey);
}
@Test
public void dirtyThenDeleted() throws Exception {
SkyKey topKey = nonHermeticKey("top");
SkyKey leafKey = nonHermeticKey("leaf");
tester.getOrCreate(topKey).addDependency(leafKey).setComputedValue(CONCATENATE);
tester.set(leafKey, new StringValue("leafy"));
assertThat(tester.evalAndGet(/* keepGoing= */ false, topKey))
.isEqualTo(new StringValue("leafy"));
tester.getOrCreate(topKey, /* markAsModified= */ true);
tester.invalidate();
assertThat(tester.evalAndGet(/* keepGoing= */ false, leafKey))
.isEqualTo(new StringValue("leafy"));
tester.delete("top");
tester.getOrCreate(leafKey, /* markAsModified= */ true);
tester.invalidate();
assertThat(tester.evalAndGet(/* keepGoing= */ false, leafKey))
.isEqualTo(new StringValue("leafy"));
}
/**
* Basic test for a {@link SkyFunction.Reset} with no rewinding of dependencies.
*
* <p>Ensures that {@link NodeEntry#getResetDirectDeps} is used correctly by Skyframe to avoid
* registering duplicate rdep edges when {@code dep} is requested both before and after a reset.
*
* <p>This test covers the case where {@code dep} is newly requested post-reset during a {@link
* SkyFunction#compute} invocation that returns a {@link SkyValue}, which exercises a different
* {@link AbstractParallelEvaluator} code path than the scenario covered by {@link
* #resetSelfOnly_extraDepMissingAfterReset_initialBuild}.
*/
@Test
public void resetSelfOnly_singleDep_initialBuild() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey dep = skyKey("dep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
var depValue = env.getValue(dep);
if (depValue == null) {
return null;
}
if (!alreadyReset) {
alreadyReset = true;
return Reset.selfOnly(top);
}
return new StringValue("topVal");
}
});
tester.getOrCreate(dep).setConstantValue(new StringValue("depVal"));
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top).containsExactly(dep);
}
/**
* Similar to {@link #resetSelfOnly_singleDep_initialBuild} except that the reset occurs on the
* node's incremental build.
*/
@Test
public void resetSelfOnly_singleDep_incrementalBuild() throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey dep = nonHermeticKey("dep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
if (depValue.getValue().equals("depVal2") && !alreadyReset) {
alreadyReset = true;
return Reset.selfOnly(top);
}
return new StringValue("topVal");
}
});
tester.getOrCreate(dep).setConstantValue(new StringValue("depVal1"));
tester.eval(/* keepGoing= */ false, top);
assertThat(inconsistencyReceiver).isEmpty();
tester.set(dep, new StringValue("depVal2"));
tester.invalidate();
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top).containsExactly(dep);
}
/**
* Test for a {@link SkyFunction.Reset} with no rewinding of dependencies, with a missing
* dependency requested post-reset.
*
* <p>Ensures that {@link NodeEntry#getResetDirectDeps} is used correctly by Skyframe to avoid
* registering duplicate rdep edges when {@code dep} is requested both before and after a reset.
*
* <p>This test covers the case where {@code dep} is newly requested post-reset in a {@link
* SkyFunction#compute} invocation that returns {@code null} (because {@code extraDep} is
* missing), which exercises a different {@link AbstractParallelEvaluator} code path than the
* scenario covered by {@link #resetSelfOnly_singleDep_initialBuild}.
*/
@Test
public void resetSelfOnly_extraDepMissingAfterReset_initialBuild() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey dep = skyKey("dep");
SkyKey extraDep = skyKey("extraDep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
var depValue = env.getValue(dep);
if (depValue == null) {
return null;
}
if (!alreadyReset) {
alreadyReset = true;
return Reset.selfOnly(top);
}
var extraDepValue = env.getValue(extraDep);
if (extraDepValue == null) {
return null;
}
return new StringValue("topVal");
}
});
tester.getOrCreate(dep).setConstantValue(new StringValue("depVal"));
tester.getOrCreate(extraDep).setConstantValue(new StringValue("extraDepVal"));
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top);
assertThatEvaluationResult(result)
.hasDirectDepsInGraphThat(top)
.containsExactly(dep, extraDep)
.inOrder();
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(extraDep).containsExactly(top);
}
/**
* Similar to {@link #resetSelfOnly_extraDepMissingAfterReset_initialBuild} except that the reset
* occurs on the node's incremental build.
*/
@Test
public void resetSelfOnly_extraDepMissingAfterReset_incrementalBuild() throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey dep = nonHermeticKey("dep");
SkyKey extraDep = skyKey("extraDep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
if (depValue.getValue().equals("depVal2") && !alreadyReset) {
alreadyReset = true;
return Reset.selfOnly(top);
}
var extraDepValue = env.getValue(extraDep);
if (extraDepValue == null) {
return null;
}
return new StringValue("topVal");
}
});
tester.getOrCreate(dep).setConstantValue(new StringValue("depVal1"));
tester.getOrCreate(extraDep).setConstantValue(new StringValue("extraDepVal"));
tester.eval(/* keepGoing= */ false, top);
assertThat(inconsistencyReceiver).isEmpty();
tester.set(dep, new StringValue("depVal2"));
tester.invalidate();
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top);
assertThatEvaluationResult(result)
.hasDirectDepsInGraphThat(top)
.containsExactly(dep, extraDep)
.inOrder();
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(extraDep).containsExactly(top);
}
/**
* Tests that if a dependency is requested prior to a {@link SkyFunction.Reset} but not after,
* then the corresponding reverse dep edge is removed.
*
* <p>This happens in practice with input-discovering actions, which use mutable state to track
* input discovery, resulting in unstable dependencies.
*/
@Test
public void resetSelfOnly_depNotRequestedAgainAfterReset() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey flakyDep = skyKey("flakyDep");
SkyKey stableDep = skyKey("stableDep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
env.getValuesAndExceptions(
alreadyReset
? ImmutableList.of(stableDep)
: ImmutableList.of(stableDep, flakyDep));
if (env.valuesMissing()) {
return null;
}
if (!alreadyReset) {
alreadyReset = true;
return Reset.selfOnly(top);
}
return new StringValue("topVal");
}
});
tester.getOrCreate(stableDep).setConstantValue(new StringValue("stableDepVal"));
tester.getOrCreate(flakyDep).setConstantValue(new StringValue("flakyDepVal"));
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top).containsExactly(stableDep);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(stableDep).containsExactly(top);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(flakyDep).isEmpty();
}
/**
* Tests that reset nodes are properly handled during invalidation after an aborted evaluation.
*
* <p>Invalidation deletes any nodes that are incomplete from the prior evaluation (in this case
* {@code top}). It should also remove the corresponding reverse dep edge from {@code dep} even
* though {@code top} does not have {@code dep} as a temporary direct dep when the evaluation is
* aborted.
*
* <p>An aborted evaluation can happen in practice when there is an error on a {@code
* --nokeep_going} build or if the user hits ctrl+c.
*/
@Test
public void resetSelfOnly_evaluationAborted() throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
SkyKey top = skyKey("top");
SkyKey dep = skyKey("dep");
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
if (alreadyReset) {
throw new InterruptedException("Evaluation aborted");
}
var depValue = env.getValue(dep);
if (depValue == null) {
return null;
}
alreadyReset = true;
return Reset.selfOnly(top);
}
});
tester.getOrCreate(dep).setConstantValue(new StringValue("depVal"));
assertThrows(InterruptedException.class, () -> tester.eval(/* keepGoing= */ false, top));
assertThat(inconsistencyReceiver).containsExactly(InconsistencyData.resetRequested(top));
inconsistencyReceiver.clear();
var result = tester.eval(/* keepGoing= */ false, dep);
assertThatEvaluationResult(result).hasEntryThat(dep).isEqualTo(new StringValue("depVal"));
assertThat(inconsistencyReceiver).isEmpty();
assertThat(tester.evaluator.getValues()).doesNotContainKey(top);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).isEmpty();
}
/** Basic test of rewinding. */
@Test
public void rewindOneDep() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableFunction = new RewindableFunction();
SkyKey top = skyKey("top");
SkyKey dep = skyKey("dep");
tester.getOrCreate(dep).setBuilder(rewindableFunction);
tester
.getOrCreate(top)
.setBuilder(
(skyKey, env) -> {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
if (depValue.equals(RewindableFunction.STALE_VALUE)) {
var rewindGraph = Reset.newRewindGraphFor(top);
rewindGraph.putEdge(top, dep);
return Reset.of(rewindGraph);
}
assertThat(depValue).isEqualTo(RewindableFunction.FRESH_VALUE);
return new StringValue("topVal");
});
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(top),
InconsistencyData.rewind(top, ImmutableSet.of(dep)));
assertThat(rewindableFunction.calls).isEqualTo(2);
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top).containsExactly(dep);
}
/**
* Tests the case where multiple parents attempt to rewind the same node concurrently, one
* successfully dirties the node, and the other observes the node as already dirty.
*/
@Test
public void twoParentsRewindSameDep_markedDirtyOnce() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableFunction = new RewindableFunction();
CyclicBarrier parentBarrier = new CyclicBarrier(2);
SkyKey top1 = skyKey("top1");
SkyKey top2 = skyKey("top2");
SkyKey dep = skyKey("dep");
tester.getOrCreate(dep).setBuilder(rewindableFunction);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (type == EventType.MARK_DIRTY && order == Order.AFTER) {
awaitUnchecked(parentBarrier);
}
},
/* deterministic= */ false);
SkyFunction parentFunction =
(skyKey, env) -> {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
if (depValue.equals(RewindableFunction.STALE_VALUE)) {
awaitUnchecked(parentBarrier);
var rewindGraph = Reset.newRewindGraphFor(skyKey);
rewindGraph.putEdge(skyKey, dep);
return Reset.of(rewindGraph);
}
assertThat(depValue).isEqualTo(RewindableFunction.FRESH_VALUE);
return new StringValue("topVal");
};
tester.getOrCreate(top1).setBuilder(parentFunction);
tester.getOrCreate(top2).setBuilder(parentFunction);
var result = tester.eval(/* keepGoing= */ false, top1, top2);
assertThatEvaluationResult(result).hasEntryThat(top1).isEqualTo(new StringValue("topVal"));
assertThatEvaluationResult(result).hasEntryThat(top2).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(top1),
InconsistencyData.rewind(top1, ImmutableSet.of(dep)),
InconsistencyData.resetRequested(top2),
InconsistencyData.rewind(top2, ImmutableSet.of(dep)));
assertThat(rewindableFunction.calls).isEqualTo(2);
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top1).containsExactly(dep);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top2).containsExactly(dep);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top1, top2);
}
/**
* Tests the case where multiple parents attempt to rewind the same node concurrently and one
* successfully dirties the node, which then completes before the second parent dirties the node
* again.
*/
@Test
public void twoParentsRewindSameDep_markedDirtyTwice() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableFunction = new RewindableFunction();
CyclicBarrier parentBarrier = new CyclicBarrier(2);
AtomicBoolean isFirstParent = new AtomicBoolean(true);
CountDownLatch firstParentDone = new CountDownLatch(1);
SkyKey top1 = skyKey("top1");
SkyKey top2 = skyKey("top2");
SkyKey dep = skyKey("dep");
tester.getOrCreate(dep).setBuilder(rewindableFunction);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (type == EventType.MARK_DIRTY
&& order == Order.BEFORE
&& !isFirstParent.getAndSet(false)) {
// Lost the race. Wait for the first parent to finish so we rewind again. We could just
// wait for dep to finish, but then we might mark it dirty before the first parent uses
// it, which would lead to flaky BUILDING_PARENT_FOUND_UNDONE_CHILD inconsistencies.
Uninterruptibles.awaitUninterruptibly(firstParentDone);
}
},
/* deterministic= */ false);
SkyFunction parentFunction =
(skyKey, env) -> {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
if (depValue.equals(RewindableFunction.STALE_VALUE)) {
awaitUnchecked(parentBarrier);
var rewindGraph = Reset.newRewindGraphFor(skyKey);
rewindGraph.putEdge(skyKey, dep);
return Reset.of(rewindGraph);
}
assertThat(depValue).isEqualTo(RewindableFunction.FRESH_VALUE);
firstParentDone.countDown();
return new StringValue("topVal");
};
tester.getOrCreate(top1).setBuilder(parentFunction);
tester.getOrCreate(top2).setBuilder(parentFunction);
var result = tester.eval(/* keepGoing= */ false, top1, top2);
assertThatEvaluationResult(result).hasEntryThat(top1).isEqualTo(new StringValue("topVal"));
assertThatEvaluationResult(result).hasEntryThat(top2).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(top1),
InconsistencyData.rewind(top1, ImmutableSet.of(dep)),
InconsistencyData.resetRequested(top2),
InconsistencyData.rewind(top2, ImmutableSet.of(dep)));
assertThat(rewindableFunction.calls).isEqualTo(3);
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top1).containsExactly(dep);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top2).containsExactly(dep);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).containsExactly(top1, top2);
}
/**
* Regression test for b/315301248.
*
* <p>In a {@code --nokeep_going} build, multiple parents attempt to rewind the same node
* concurrently. One successfully dirties the node, which then completes with an error before the
* second parent attempts to dirty the node again. If the second rewinding attempt actually
* transitions the node from done (in error) to dirty, we would crash during error bubbling, which
* reasonably expects the errorful node to be done.
*
* <p>The solution is to ignore rewinding attempts on errorful nodes.
*/
@Test
public void twoParentsRewindSameDep_depEvaluatesToErrorAfterRewind() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableErrorFunction =
new SkyFunction() {
private int calls = 0;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
if (++calls == 1) {
return RewindableFunction.STALE_VALUE;
}
throw new GenericFunctionException(new SomeErrorException("error"));
}
};
CyclicBarrier parentBarrier = new CyclicBarrier(2);
AtomicBoolean isFirstParent = new AtomicBoolean(true);
CountDownLatch depErrorSet = new CountDownLatch(1);
SkyKey top1 = skyKey("top1");
SkyKey top2 = skyKey("top2");
SkyKey dep = skyKey("dep");
tester.getOrCreate(dep).setBuilder(rewindableErrorFunction);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (type == EventType.MARK_DIRTY
&& order == Order.BEFORE
&& !isFirstParent.getAndSet(false)) {
// Lost the race. Wait for dep have its error set so that we attempt to rewind a done
// node in error.
Uninterruptibles.awaitUninterruptibly(depErrorSet);
} else if (key.equals(dep)
&& type == EventType.SET_VALUE
&& order == Order.AFTER
&& ValueWithMetadata.getMaybeErrorInfo((SkyValue) context) != null) {
depErrorSet.countDown();
}
},
/* deterministic= */ false);
SkyFunction parentFunction =
(skyKey, env) -> {
var depValue = (StringValue) env.getValue(dep);
if (depValue == null) {
return null;
}
assertThat(depValue).isEqualTo(RewindableFunction.STALE_VALUE);
awaitUnchecked(parentBarrier);
var rewindGraph = Reset.newRewindGraphFor(skyKey);
rewindGraph.putEdge(skyKey, dep);
return Reset.of(rewindGraph);
};
tester.getOrCreate(top1).setBuilder(parentFunction);
tester.getOrCreate(top2).setBuilder(parentFunction);
var result = tester.eval(/* keepGoing= */ false, top1, top2);
assertThatEvaluationResult(result).hasError();
assertThat(result.errorMap().keySet()).containsAnyOf(top1, top2);
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(top1),
InconsistencyData.rewind(top1, ImmutableSet.of(dep)),
InconsistencyData.resetRequested(top2),
InconsistencyData.rewind(top2, ImmutableSet.of(dep)));
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
result = tester.eval(/* keepGoing= */ false, dep);
// The parents never completed, so an incremental build deletes them. Check that they are no
// longer in the graph and that rdeps are removed from dep.
assertThatEvaluationResult(result).hasSingletonErrorThat(dep).isNotNull();
assertThat(tester.evaluator.getValues().keySet()).containsNoneOf(top1, top2);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(dep).isEmpty();
}
/**
* Tests that when a node is rewound and evaluates to an error, its reverse transitive closure is
* deleted from the graph, including parents that were done before rewinding took place.
*/
@Test
public void depWithDoneParentEvaluatesToErrorAfterRewind_reverseTransitiveClosureDeleted()
throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableErrorFunction =
new SkyFunction() {
private int calls = 0;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
if (++calls == 1) {
return RewindableFunction.STALE_VALUE;
}
throw new GenericFunctionException(new SomeErrorException("error"));
}
};
CountDownLatch goodTopDone = new CountDownLatch(1);
SkyKey goodTop = skyKey("goodTop");
SkyKey badTop = skyKey("badTop");
SkyKey dep = nonHermeticKey("dep");
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (key.equals(goodTop) && type == EventType.SET_VALUE && order == Order.AFTER) {
goodTopDone.countDown();
}
},
/* deterministic= */ false);
tester.getOrCreate(dep).setBuilder(rewindableErrorFunction);
tester.getOrCreate(goodTop).addDependency(dep).setComputedValue(COPY);
tester
.getOrCreate(badTop)
.setBuilder(
(skyKey, env) -> {
if (env.getValue(dep) == null) {
return null;
}
goodTopDone.await();
var rewindGraph = Reset.newRewindGraphFor(badTop);
rewindGraph.putEdge(badTop, dep);
return Reset.of(rewindGraph);
});
var result = tester.eval(/* keepGoing= */ false, goodTop, badTop);
assertThatEvaluationResult(result).hasError();
assertThat(result.errorMap().keySet()).containsExactly(badTop);
assertThatEvaluationResult(result)
.hasEntryThat(goodTop)
.isEqualTo(RewindableFunction.STALE_VALUE);
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(badTop),
InconsistencyData.rewind(badTop, ImmutableSet.of(dep)));
result = tester.eval(/* keepGoing= */ false, new SkyKey[0]);
assertThatEvaluationResult(result).hasNoError();
assertThat(tester.getEvaluator().getValues().keySet()).containsNoneOf(dep, badTop, goodTop);
}
private static void awaitUnchecked(CyclicBarrier barrier) {
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new IllegalStateException(e);
}
}
/**
* Test for a rewind graph with depth > 1.
*
* <p>Since {@code mid} simply propagates the value of {@code bottom}, {@code top} must rewind
* both {@code mid} and {@code bottom}. See {@link
* com.google.devtools.build.lib.actions.ActionExecutionMetadata#mayInsensitivelyPropagateInputs}
* for the case that this test is simulating.
*/
@Test
public void rewindTransitiveDep() throws Exception {
assume().that(resetSupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
var rewindableFunction = new RewindableFunction();
SkyKey top = skyKey("top");
SkyKey mid = skyKey("mid");
SkyKey bottom = skyKey("bottom");
tester.getOrCreate(bottom).setBuilder(rewindableFunction);
tester.getOrCreate(mid).addDependency(bottom).setComputedValue(COPY);
tester
.getOrCreate(top)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
var midValue = (StringValue) env.getValue(mid);
if (midValue == null) {
return null;
}
if (midValue.equals(RewindableFunction.STALE_VALUE)) {
var rewindGraph = Reset.newRewindGraphFor(top);
rewindGraph.putEdge(top, mid);
rewindGraph.putEdge(mid, bottom);
return Reset.of(rewindGraph);
}
assertThat(midValue).isEqualTo(RewindableFunction.FRESH_VALUE);
return new StringValue("topVal");
}
});
var result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(new StringValue("topVal"));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(top),
InconsistencyData.rewind(top, ImmutableSet.of(mid, bottom)));
assertThat(rewindableFunction.calls).isEqualTo(2);
if (!incrementalitySupported()) {
return; // Skip assertions on dependency edges when they aren't kept.
}
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(top).containsExactly(mid);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(mid).containsExactly(top);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(mid).containsExactly(bottom);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(bottom).containsExactly(mid);
}
/**
* Tests that incompletely rewound nodes are properly handled during invalidation after an aborted
* evaluation.
*
* <p>Covers the concern described at b/149243918#comment9: without rewinding, we have an
* invariant that after an evaluation, a done node cannot depend on a dirty node. The invalidator
* leverges this invariant by short-circuiting when it visits a dirty node, under the assumption
* that any rdeps are either already dirty or present in the invalidation frontier.
*
* <p>In this test, a value propagates from {@code bottom} to {@code mid} to {@code goodTop}.
* However, after {@code goodTop} is done, {@code badTop} rewinds {@code mid}, and then the
* evaluation is aborted. On the incremental build, {@code bottom} changes. We must recompute
* {@code goodTop}, but the only path from {@code bottom} to {@code goodTop} goes through {@code
* mid}, which is dirty, and so the invalidator will never visit {@code goodTop}.
*
* <p>The solution: instead of relying on bottom-up invalidation, rewound nodes are treated like
* inflight nodes and deleted (along with their reverse transitive closure) prior to the next
* evaluation.
*/
@Test
public void evaluationAbortedWithRewoundNodeOnInvalidationPath_dirty() throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
CountDownLatch goodTopDone = new CountDownLatch(1);
SkyKey goodTop = skyKey("goodTop");
SkyKey badTop = skyKey("badTop");
SkyKey mid = skyKey("mid");
SkyKey bottom = nonHermeticKey("bottom");
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (key.equals(goodTop) && type == EventType.SET_VALUE && order == Order.AFTER) {
goodTopDone.countDown();
}
},
/* deterministic= */ false);
tester.getOrCreate(goodTop).addDependency(mid).setComputedValue(COPY);
tester
.getOrCreate(badTop)
.setBuilder(
new SkyFunction() {
private boolean alreadyReset = false;
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
if (alreadyReset) {
throw new InterruptedException("Evaluation aborted");
}
if (env.getValue(mid) == null) {
return null;
}
goodTopDone.await();
alreadyReset = true;
var rewindGraph = Reset.newRewindGraphFor(badTop);
rewindGraph.putEdge(badTop, mid);
return Reset.of(rewindGraph);
}
});
tester.getOrCreate(mid).addDependency(bottom).setComputedValue(COPY);
tester.getOrCreate(bottom).setConstantValue(new StringValue("val1"));
assertThrows(
InterruptedException.class, () -> tester.eval(/* keepGoing= */ false, goodTop, badTop));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(badTop),
InconsistencyData.rewind(badTop, ImmutableSet.of(mid)));
tester.set(bottom, new StringValue("val2"));
tester.invalidate();
var result = tester.eval(/* keepGoing= */ false, goodTop);
assertThatEvaluationResult(result).hasEntryThat(goodTop).isEqualTo(new StringValue("val2"));
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(goodTop).containsExactly(mid);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(mid).containsExactly(goodTop);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(mid).containsExactly(bottom);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(bottom).containsExactly(mid);
}
/**
* Similar to {@link #evaluationAbortedWithRewoundNodeOnInvalidationPath_dirty} except that the
* rewound node is inflight when the evaluation is aborted, so this actually works without the
* special handling added for rewound nodes.
*/
@Test
public void evaluationAbortedWithRewoundNodeOnInvalidationPath_inflight() throws Exception {
assume().that(resetSupported()).isTrue();
assume().that(incrementalitySupported()).isTrue();
var inconsistencyReceiver = recordInconsistencies();
CountDownLatch goodTopDone = new CountDownLatch(1);
AtomicBoolean rewindingInProgress = new AtomicBoolean(false);
SkyKey goodTop = skyKey("goodTop");
SkyKey badTop = skyKey("badTop");
SkyKey mid = nonHermeticKey("mid");
SkyKey bottom = nonHermeticKey("bottom");
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (key.equals(goodTop) && type == EventType.SET_VALUE && order == Order.AFTER) {
goodTopDone.countDown();
}
},
/* deterministic= */ false);
tester.getOrCreate(goodTop).addDependency(mid).setComputedValue(COPY);
tester
.getOrCreate(badTop)
.setBuilder(
(skyKey, env) -> {
if (env.getValue(mid) == null) {
return null;
}
goodTopDone.await();
rewindingInProgress.set(true);
var rewindGraph = Reset.newRewindGraphFor(badTop);
rewindGraph.putEdge(badTop, mid);
return Reset.of(rewindGraph);
});
tester
.getOrCreate(mid)
.setBuilder(
(skyKey, env) -> {
if (rewindingInProgress.get()) {
throw new InterruptedException("Evaluation aborted");
}
return env.getValue(bottom);
});
tester.getOrCreate(bottom).setConstantValue(new StringValue("val1"));
assertThrows(
InterruptedException.class, () -> tester.eval(/* keepGoing= */ false, goodTop, badTop));
assertThat(inconsistencyReceiver)
.containsExactly(
InconsistencyData.resetRequested(badTop),
InconsistencyData.rewind(badTop, ImmutableSet.of(mid)));
rewindingInProgress.set(false);
tester.set(bottom, new StringValue("val2"));
tester.invalidate();
var result = tester.eval(/* keepGoing= */ false, goodTop);
assertThatEvaluationResult(result).hasEntryThat(goodTop).isEqualTo(new StringValue("val2"));
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(goodTop).containsExactly(mid);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(mid).containsExactly(goodTop);
assertThatEvaluationResult(result).hasDirectDepsInGraphThat(mid).containsExactly(bottom);
assertThatEvaluationResult(result).hasReverseDepsInGraphThat(bottom).containsExactly(mid);
}
private RecordingInconsistencyReceiver recordInconsistencies() {
var inconsistencyReceiver = new RecordingInconsistencyReceiver();
tester.setGraphInconsistencyReceiver(inconsistencyReceiver);
tester.initialize();
return inconsistencyReceiver;
}
private static final class RecordingInconsistencyReceiver
implements GraphInconsistencyReceiver, Iterable<InconsistencyData> {
private final List<InconsistencyData> inconsistencies = new ArrayList<>();
@Override
public synchronized void noteInconsistencyAndMaybeThrow(
SkyKey key, @Nullable Collection<SkyKey> otherKeys, Inconsistency inconsistency) {
inconsistencies.add(InconsistencyData.create(key, otherKeys, inconsistency));
}
@Override
public Iterator<InconsistencyData> iterator() {
return inconsistencies.iterator();
}
void clear() {
inconsistencies.clear();
}
}
/**
* {@link SkyFunction} for rewinding tests that returns {@link #STALE_VALUE} the first time and
* {@link #FRESH_VALUE} thereafter.
*/
private static final class RewindableFunction implements SkyFunction {
static final StringValue STALE_VALUE = new StringValue("stale");
static final StringValue FRESH_VALUE = new StringValue("fresh");
private int calls = 0;
@Override
public SkyValue compute(SkyKey skyKey, Environment env) {
return ++calls == 1 ? STALE_VALUE : FRESH_VALUE;
}
}
/**
* The same dep is requested in two groups, but its value determines what the other dep in the
* second group is. When it changes, the other dep in the second group should not be requested.
*/
@Test
public void sameDepInTwoGroups() throws Exception {
initializeTester();
// leaf4 should not be built in the second build.
SkyKey leaf4 = skyKey("leaf4");
AtomicBoolean shouldNotBuildLeaf4 = new AtomicBoolean(false);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (shouldNotBuildLeaf4.get()
&& key.equals(leaf4)
&& type != EventType.REMOVE_REVERSE_DEP
&& type != EventType.GET_BATCH) {
throw new IllegalStateException(
"leaf4 should not have been considered this build: "
+ type
+ ", "
+ order
+ ", "
+ context);
}
},
/* deterministic= */ false);
tester.set(leaf4, new StringValue("leaf4"));
// Create leaf0, leaf1 and leaf2 values with values "leaf2", "leaf3", "leaf4" respectively.
// These will be requested as one dependency group. In the second build, leaf2 will have the
// value "leaf5".
List<SkyKey> leaves = new ArrayList<>();
for (int i = 0; i <= 2; i++) {
SkyKey leaf = i == 2 ? nonHermeticKey("leaf" + i) : skyKey("leaf" + i);
leaves.add(leaf);
tester.set(leaf, new StringValue("leaf" + (i + 2)));
}
// Create "top" value. It depends on all leaf values in two overlapping dependency groups.
SkyKey topKey = skyKey("top");
SkyValue topValue = new StringValue("top");
tester
.getOrCreate(topKey)
.setBuilder(
(skyKey, env) -> {
// Request the first group, [leaf0, leaf1, leaf2].
// In the first build, it has values ["leaf2", "leaf3", "leaf4"].
// In the second build it has values ["leaf2", "leaf3", "leaf5"]
SkyframeLookupResult values = env.getValuesAndExceptions(leaves);
if (env.valuesMissing()) {
return null;
}
// Request the second group. In the first build it's [leaf2, leaf4].
// In the second build it's [leaf2, leaf5]
env.getValuesAndExceptions(
ImmutableList.of(
leaves.get(2), skyKey(((StringValue) values.get(leaves.get(2))).getValue())));
if (env.valuesMissing()) {
return null;
}
return topValue;
});
// First build: assert we can evaluate "top".
assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey)).isEqualTo(topValue);
// Second build: replace "leaf4" by "leaf5" in leaf2's value. Assert leaf4 is not requested.
SkyKey leaf5 = skyKey("leaf5");
tester.set(leaf5, new StringValue("leaf5"));
tester.set(leaves.get(2), new StringValue("leaf5"));
tester.invalidate();
shouldNotBuildLeaf4.set(true);
assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey)).isEqualTo(topValue);
}
@Test
public void dirtyAndChanged() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey mid = nonHermeticKey("mid");
SkyKey top = skyKey("top");
tester.getOrCreate(top).addDependency(mid).setComputedValue(COPY);
tester.getOrCreate(mid).addDependency(leaf).setComputedValue(COPY);
tester.set(leaf, new StringValue("leafy"));
// For invalidation.
tester.set("dummy", new StringValue("dummy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("leafy");
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
// For invalidation.
tester.evalAndGet("dummy");
tester.getOrCreate(mid, /*markAsModified=*/ true);
tester.invalidate();
topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("crunchy");
}
/**
* Test whether a value that was already marked changed will be incorrectly marked dirty, not
* changed, if another thread tries to mark it just dirty. To exercise this, we need to have a
* race condition where both threads see that the value is not dirty yet, then the "changed"
* thread marks the value changed before the "dirty" thread marks the value dirty. To accomplish
* this, we use a countdown latch to make the "dirty" thread wait until the "changed" thread is
* done, and another countdown latch to make both of them wait until they have both checked if the
* value is currently clean.
*/
@Test
public void dirtyAndChangedValueIsChanged() throws Exception {
SkyKey parent = nonHermeticKey("parent");
AtomicBoolean blockingEnabled = new AtomicBoolean(false);
CountDownLatch waitForChanged = new CountDownLatch(1);
// changed thread checks value entry once (to see if it is changed). dirty thread checks twice,
// to see if it is changed, and if it is dirty.
CountDownLatch threadsStarted = new CountDownLatch(3);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!blockingEnabled.get()) {
return;
}
if (!key.equals(parent)) {
return;
}
if (type == EventType.IS_CHANGED && order == Order.BEFORE) {
threadsStarted.countDown();
}
// Dirtiness only checked by dirty thread.
if (type == EventType.IS_DIRTY && order == Order.BEFORE) {
threadsStarted.countDown();
}
if (type == EventType.MARK_DIRTY) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
threadsStarted, "Both threads did not query if value isChanged in time");
if (order == Order.BEFORE) {
DirtyType dirtyType = (DirtyType) context;
if (dirtyType.equals(DirtyType.DIRTY)) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
waitForChanged, "'changed' thread did not mark value changed in time");
return;
}
}
if (order == Order.AFTER) {
DirtyType dirtyType = ((NotifyingHelper.MarkDirtyAfterContext) context).dirtyType();
if (dirtyType.equals(DirtyType.CHANGE)) {
waitForChanged.countDown();
}
}
}
},
/* deterministic= */ false);
SkyKey leaf = nonHermeticKey("leaf");
tester.set(leaf, new StringValue("leaf"));
tester.getOrCreate(parent).addDependency(leaf).setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result;
result = tester.eval(/* keepGoing= */ false, parent);
assertThat(result.get(parent).getValue()).isEqualTo("leaf");
// Invalidate leaf, but don't actually change it. It will transitively dirty parent
// concurrently with parent directly dirtying itself.
tester.getOrCreate(leaf, /* markAsModified= */ true);
SkyKey other2 = skyKey("other2");
tester.set(other2, new StringValue("other2"));
// Invalidate parent, actually changing it.
tester.getOrCreate(parent, /* markAsModified= */ true).addDependency(other2);
tester.invalidate();
blockingEnabled.set(true);
result = tester.eval(/* keepGoing= */ false, parent);
assertThat(result.get(parent).getValue()).isEqualTo("leafother2");
assertThat(waitForChanged.getCount()).isEqualTo(0);
assertThat(threadsStarted.getCount()).isEqualTo(0);
}
@Test
public void hermeticSkyFunctionCanThrowTransientErrorThenRecover() throws Exception {
SkyKey leaf = skyKey("leaf");
SkyKey top = skyKey("top");
// When top depends on leaf, but throws a transient error,
tester
.getOrCreate(top)
.addDependency(leaf)
.setHasTransientError(true)
.setComputedValue(CONCATENATE);
StringValue value = new StringValue("value");
tester.getOrCreate(leaf).setConstantValue(value);
// And the first build throws a transient error (as expected),
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ true, top);
assertThatEvaluationResult(result).hasError();
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(top).hasExceptionThat().isNotNull();
// And then top's transient error is removed,
tester.getOrCreate(top, /*markAsModified=*/ false).setHasTransientError(false);
tester.invalidateTransientErrors();
// Then top evaluates successfully, even though it was hermetic and didn't give the same result
// on successive evaluations with the same inputs.
result = tester.eval(/*keepGoing=*/ true, top);
assertThatEvaluationResult(result).hasNoError();
assertThatEvaluationResult(result).hasEntryThat(top).isEqualTo(value);
}
@Test
public void singleValueDependsOnManyDirtyValues() throws Exception {
SkyKey[] values = new SkyKey[TEST_NODE_COUNT];
StringBuilder expected = new StringBuilder();
for (int i = 0; i < values.length; i++) {
String valueName = Integer.toString(i);
values[i] = nonHermeticKey(valueName);
tester.set(values[i], new StringValue(valueName));
expected.append(valueName);
}
SkyKey topKey = skyKey("top");
TestFunction value = tester.getOrCreate(topKey).setComputedValue(CONCATENATE);
for (SkyKey skyKey : values) {
value.addDependency(skyKey);
}
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThat(result.get(topKey)).isEqualTo(new StringValue(expected.toString()));
for (int j = 0; j < RUNS; j++) {
for (SkyKey skyKey : values) {
tester.getOrCreate(skyKey, /*markAsModified=*/ true);
}
// This value has an error, but we should never discover it because it is not marked changed
// and all of its dependencies re-evaluate to the same thing.
tester.getOrCreate(topKey, /*markAsModified=*/ false).setHasError(true);
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, topKey);
assertThat(result.get(topKey)).isEqualTo(new StringValue(expected.toString()));
}
}
/**
* Tests scenario where we have dirty values in the graph, and then one of them is deleted since
* its evaluation did not complete before an error was thrown. Can either test the graph via an
* evaluation of that deleted value, or an invalidation of a child, and can either remove the
* thrown error or throw it again on that evaluation.
*/
private void dirtyValueChildrenProperlyRemovedOnEarlyBuildAbort(
boolean reevaluateMissingValue, boolean removeError) throws Exception {
SkyKey errorKey = nonHermeticKey("error");
tester.set(errorKey, new StringValue("biding time"));
SkyKey slowKey = nonHermeticKey("slow");
tester.set(slowKey, new StringValue("slow"));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
SkyKey lastKey = nonHermeticKey("last");
tester.set(lastKey, new StringValue("last"));
SkyKey motherKey = skyKey("mother");
tester
.getOrCreate(motherKey)
.addDependency(errorKey)
.addDependency(midKey)
.addDependency(lastKey)
.setComputedValue(CONCATENATE);
SkyKey fatherKey = skyKey("father");
tester
.getOrCreate(fatherKey)
.addDependency(errorKey)
.addDependency(midKey)
.addDependency(lastKey)
.setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, motherKey, fatherKey);
assertThat(result.get(motherKey).getValue()).isEqualTo("biding timeslowlast");
assertThat(result.get(fatherKey).getValue()).isEqualTo("biding timeslowlast");
tester.set(slowKey, null);
// Each parent depends on errorKey, midKey, lastKey. We keep slowKey waiting until errorKey is
// finished. So there is no way lastKey can be enqueued by either parent. Thus, the parent that
// is cleaned has not interacted with lastKey this build. Still, lastKey's reverse dep on that
// parent should be removed.
CountDownLatch errorFinish = new CountDownLatch(1);
tester.set(errorKey, null);
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ null,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("leaf2"),
/*deps=*/ ImmutableList.of()));
tester.invalidate();
// errorKey finishes, written to graph -> leafKey maybe starts+finishes & (Visitor aborts)
// -> one of mother or father builds. The other one should be cleaned, and no references to it
// left in the graph.
result = tester.eval(/*keepGoing=*/ false, motherKey, fatherKey);
assertThat(result.hasError()).isTrue();
// Only one of mother or father should be in the graph.
assertWithMessage(result.getError(motherKey) + ", " + result.getError(fatherKey))
.that((result.getError(motherKey) == null) != (result.getError(fatherKey) == null))
.isTrue();
SkyKey parentKey =
(reevaluateMissingValue == (result.getError(motherKey) == null)) ? motherKey : fatherKey;
// Give slowKey a nice ordinary builder.
tester
.getOrCreate(slowKey, /*markAsModified=*/ false)
.setBuilder(null)
.setConstantValue(new StringValue("leaf2"));
if (removeError) {
tester
.getOrCreate(errorKey, /*markAsModified=*/ true)
.setBuilder(null)
.setConstantValue(new StringValue("reformed"));
}
String lastString = "last";
if (!reevaluateMissingValue) {
// Mark the last key modified if we're not trying the absent value again. This invalidation
// will test if lastKey still has a reference to the absent value.
lastString = "last2";
tester.set(lastKey, new StringValue(lastString));
}
tester.invalidate();
result = tester.eval(/*keepGoing=*/ false, parentKey);
if (removeError) {
assertThat(result.get(parentKey).getValue()).isEqualTo("reformedleaf2" + lastString);
} else {
assertThat(result.getError(parentKey)).isNotNull();
}
}
/**
* The following four tests (dirtyChildrenProperlyRemovedWith*) test the consistency of the graph
* after a failed build in which a dirty value should have been deleted from the graph. The
* consistency is tested via either evaluating the missing value, or the re-evaluating the present
* value, and either clearing the error or keeping it. To evaluate the present value, we
* invalidate the error value to force re-evaluation. Related to bug "skyframe m1: graph may not
* be properly cleaned on interrupt or failure".
*/
@Test
public void dirtyChildrenProperlyRemovedWithInvalidateRemoveError() throws Exception {
dirtyValueChildrenProperlyRemovedOnEarlyBuildAbort(
/*reevaluateMissingValue=*/ false, /*removeError=*/ true);
}
@Test
public void dirtyChildrenProperlyRemovedWithInvalidateKeepError() throws Exception {
dirtyValueChildrenProperlyRemovedOnEarlyBuildAbort(
/*reevaluateMissingValue=*/ false, /*removeError=*/ false);
}
@Test
public void dirtyChildrenProperlyRemovedWithReevaluateRemoveError() throws Exception {
dirtyValueChildrenProperlyRemovedOnEarlyBuildAbort(
/*reevaluateMissingValue=*/ true, /*removeError=*/ true);
}
@Test
public void dirtyChildrenProperlyRemovedWithReevaluateKeepError() throws Exception {
dirtyValueChildrenProperlyRemovedOnEarlyBuildAbort(
/*reevaluateMissingValue=*/ true, /*removeError=*/ false);
}
/**
* Regression test: enqueue so many values that some of them won't have started processing, and
* then either interrupt processing or have a child throw an error. In the latter case, this also
* tests that a value that hasn't started processing can still have a child error bubble up to it.
* In both cases, it tests that the graph is properly cleaned of the dirty values and references
* to them.
*/
private void manyDirtyValuesClearChildrenOnFail(boolean interrupt) throws Exception {
SkyKey leafKey = nonHermeticKey("leaf");
tester.set(leafKey, new StringValue("leafy"));
SkyKey lastKey = nonHermeticKey("last");
tester.set(lastKey, new StringValue("last"));
List<SkyKey> tops = new ArrayList<>();
// Request far more top-level values than there are threads, so some of them will block until
// the
// leaf child is enqueued for processing.
for (int i = 0; i < 10000; i++) {
SkyKey topKey = skyKey("top" + i);
tester
.getOrCreate(topKey)
.addDependency(leafKey)
.addDependency(lastKey)
.setComputedValue(CONCATENATE);
tops.add(topKey);
}
tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0]));
CountDownLatch notifyStart = new CountDownLatch(1);
tester.set(leafKey, null);
if (interrupt) {
// leaf will wait for an interrupt if desired. We cannot use the usual ChainedFunction
// because we need to actually throw the interrupt.
AtomicBoolean shouldSleep = new AtomicBoolean(true);
tester
.getOrCreate(leafKey, /*markAsModified=*/ true)
.setBuilder(
(skyKey, env) -> {
notifyStart.countDown();
if (shouldSleep.get()) {
// Should be interrupted within 5 seconds.
Thread.sleep(5000);
throw new AssertionError("leaf was not interrupted");
}
return new StringValue("crunchy");
});
tester.invalidate();
TestThread evalThread =
new TestThread(
() ->
assertThrows(
InterruptedException.class,
() -> tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0]))));
evalThread.start();
assertThat(notifyStart.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue();
evalThread.interrupt();
evalThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
// Free leafKey to compute next time.
shouldSleep.set(false);
} else {
// Non-interrupt case. Just throw an error in the child.
tester.getOrCreate(leafKey, /*markAsModified=*/ true).setHasError(true);
tester.invalidate();
// The error thrown may non-deterministically bubble up to a parent that has not yet started
// processing, but has been enqueued for processing.
tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0]));
tester.getOrCreate(leafKey, /*markAsModified=*/ true).setHasError(false);
tester.set(leafKey, new StringValue("crunchy"));
}
// lastKey was not touched during the previous build, but its reverse deps on its parents should
// still be accurate.
tester.set(lastKey, new StringValue("new last"));
tester.invalidate();
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0]));
for (SkyKey topKey : tops) {
assertWithMessage(topKey.toString())
.that(result.get(topKey).getValue())
.isEqualTo("crunchynew last");
}
}
/**
* Regression test: make sure that if an evaluation fails before a dirty value starts evaluation
* (in particular, before it is reset), the graph remains consistent.
*/
@Test
public void manyDirtyValuesClearChildrenOnError() throws Exception {
manyDirtyValuesClearChildrenOnFail(/*interrupt=*/ false);
}
/**
* Regression test: Make sure that if an evaluation is interrupted before a dirty value starts
* evaluation (in particular, before it is reset), the graph remains consistent.
*/
@Test
public void manyDirtyValuesClearChildrenOnInterrupt() throws Exception {
manyDirtyValuesClearChildrenOnFail(/*interrupt=*/ true);
}
private SkyKey makeTestKey(SkyKey node0) {
SkyKey key = null;
// Create a long chain of nodes. Most of them will not actually be dirtied, but the last one to
// be dirtied will enqueue its parent for dirtying, so it will be in the queue for the next run.
for (int i = 0; i < TEST_NODE_COUNT; i++) {
key = i == 0 ? node0 : skyKey("node" + i);
if (i > 1) {
tester.getOrCreate(key).addDependency("node" + (i - 1)).setComputedValue(COPY);
} else if (i == 1) {
tester.getOrCreate(key).addDependency(node0).setComputedValue(COPY);
} else {
tester.set(key, new StringValue("node0"));
}
}
return key;
}
/**
* Regression test for case where the user requests that we delete nodes that are already in the
* queue to be dirtied. We should handle that gracefully and not complain.
*/
@Test
public void deletingDirtyNodes() throws Exception {
SkyKey node0 = nonHermeticKey("node0");
SkyKey key = makeTestKey(node0);
// Seed the graph.
assertThat(((StringValue) tester.evalAndGet(/*keepGoing=*/ false, key)).getValue())
.isEqualTo("node0");
// Start the dirtying process.
tester.set(node0, new StringValue("new"));
tester.invalidate();
// Interrupt current thread on a next invalidate call
Thread thread = Thread.currentThread();
tester.progressReceiver.setNextInvalidationCallback(thread::interrupt);
assertThrows(InterruptedException.class, () -> tester.eval(/*keepGoing=*/ false, key));
// Cleanup + paranoid check
tester.progressReceiver.setNextInvalidationCallback(null);
// Now delete all the nodes. The node that was going to be dirtied is also deleted, which we
// should handle.
tester.evaluator.delete(Predicates.alwaysTrue());
assertThat(((StringValue) tester.evalAndGet(/*keepGoing=*/ false, key)).getValue())
.isEqualTo("new");
}
@Test
public void changePruning() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey mid = skyKey("mid");
SkyKey top = skyKey("top");
tester.getOrCreate(top).addDependency(mid).setComputedValue(COPY);
tester.getOrCreate(mid).addDependency(leaf).setComputedValue(COPY);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("leafy");
// Mark leaf changed, but don't actually change it.
tester.getOrCreate(leaf, /* markAsModified= */ true);
// mid will give an error if re-evaluated, but it shouldn't be because it is not marked changed,
// and its dirty child will evaluate to the same element.
tester.getOrCreate(mid, /* markAsModified= */ false).setHasError(true);
tester.invalidate();
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, top);
assertThat(result.hasError()).isFalse();
topValue = result.get(top);
assertThat(topValue.getValue()).isEqualTo("leafy");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
}
@Test
public void changePruningWithDoneValue() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey mid = skyKey("mid");
SkyKey top = skyKey("top");
SkyKey suffix = skyKey("suffix");
StringValue suffixValue = new StringValue("suffix");
tester.set(suffix, suffixValue);
tester.getOrCreate(top).addDependency(mid).addDependency(suffix).setComputedValue(CONCATENATE);
tester.getOrCreate(mid).addDependency(leaf).addDependency(suffix).setComputedValue(CONCATENATE);
SkyValue leafyValue = new StringValue("leafy");
tester.set(leaf, leafyValue);
StringValue value = (StringValue) tester.evalAndGet("top");
assertThat(value.getValue()).isEqualTo("leafysuffixsuffix");
// Mark leaf changed, but don't actually change it.
tester.getOrCreate(leaf, /* markAsModified= */ true);
// mid will give an error if re-evaluated, but it shouldn't be because it is not marked changed,
// and its dirty child will evaluate to the same element.
tester.getOrCreate(mid, /*markAsModified=*/ false).setHasError(true);
tester.invalidate();
value = (StringValue) tester.evalAndGet(/*keepGoing=*/ false, leaf);
assertThat(value.getValue()).isEqualTo("leafy");
assertThat(tester.getDirtyKeys()).containsExactly(mid, top);
assertThat(tester.getDeletedKeys()).isEmpty();
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, top);
assertWithMessage(result.toString()).that(result.hasError()).isFalse();
value = result.get(top);
assertThat(value.getValue()).isEqualTo("leafysuffixsuffix");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
}
@Test
public void changePruningAfterParentPrunes() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey top = skyKey("top");
tester.set(leaf, new StringValue("leafy"));
// When top depends on leaf, but always returns the same value,
StringValue fixedTopValue = new StringValue("top");
AtomicBoolean topEvaluated = new AtomicBoolean(false);
tester
.getOrCreate(top)
.setBuilder(
(skyKey, env) -> {
topEvaluated.set(true);
return env.getValue(leaf) == null ? null : fixedTopValue;
});
// And top is evaluated,
StringValue topValue = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue).isEqualTo(fixedTopValue);
// And top was actually evaluated.
assertThat(topEvaluated.get()).isTrue();
// When leaf is changed,
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
topEvaluated.set(false);
// And top is evaluated,
StringValue topValue2 = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue2).isEqualTo(fixedTopValue);
// And top was actually evaluated.
assertThat(topEvaluated.get()).isTrue();
// When leaf is invalidated but not actually changed,
tester.getOrCreate(leaf, /*markAsModified=*/ true);
tester.invalidate();
topEvaluated.set(false);
// And top is evaluated,
StringValue topValue3 = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue3).isEqualTo(fixedTopValue);
// And top was *not* actually evaluated, because change pruning cut off evaluation.
assertThat(topEvaluated.get()).isFalse();
}
@Test
public void changePruningFromOtherNodeAfterParentPrunes() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey other = nonHermeticKey("other");
SkyKey top = skyKey("top");
tester.set(leaf, new StringValue("leafy"));
tester.set(other, new StringValue("other"));
// When top depends on leaf and other, but always returns the same value,
StringValue fixedTopValue = new StringValue("top");
AtomicBoolean topEvaluated = new AtomicBoolean(false);
tester
.getOrCreate(top)
.setBuilder(
(skyKey, env) -> {
topEvaluated.set(true);
return env.getValue(other) == null || env.getValue(leaf) == null
? null
: fixedTopValue;
});
// And top is evaluated,
StringValue topValue = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue).isEqualTo(fixedTopValue);
// And top was actually evaluated.
assertThat(topEvaluated.get()).isTrue();
// When leaf is changed,
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
topEvaluated.set(false);
// And top is evaluated,
StringValue topValue2 = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue2).isEqualTo(fixedTopValue);
// And top was actually evaluated.
assertThat(topEvaluated.get()).isTrue();
// When other is invalidated but not actually changed,
tester.getOrCreate(other, /*markAsModified=*/ true);
tester.invalidate();
topEvaluated.set(false);
// And top is evaluated,
StringValue topValue3 = (StringValue) tester.evalAndGet("top");
// Then top's value is as expected,
assertThat(topValue3).isEqualTo(fixedTopValue);
// And top was *not* actually evaluated, because change pruning cut off evaluation.
assertThat(topEvaluated.get()).isFalse();
}
@Test
public void changedChildChangesDepOfParent() throws Exception {
SkyKey buildFile = nonHermeticKey("buildFile");
ValueComputer authorDrink =
(deps, env) -> {
String author = ((StringValue) deps.get(buildFile)).getValue();
StringValue beverage;
switch (author) {
case "hemingway":
beverage = (StringValue) env.getValue(skyKey("absinthe"));
break;
case "joyce":
beverage = (StringValue) env.getValue(skyKey("whiskey"));
break;
default:
throw new IllegalStateException(author);
}
if (beverage == null) {
return null;
}
return new StringValue(author + " drank " + beverage.getValue());
};
tester.set(buildFile, new StringValue("hemingway"));
SkyKey absinthe = skyKey("absinthe");
tester.set(absinthe, new StringValue("absinthe"));
SkyKey whiskey = skyKey("whiskey");
tester.set(whiskey, new StringValue("whiskey"));
SkyKey top = skyKey("top");
tester.getOrCreate(top).addDependency(buildFile).setComputedValue(authorDrink);
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("hemingway drank absinthe");
tester.set(buildFile, new StringValue("joyce"));
// Don't evaluate absinthe successfully anymore.
tester.getOrCreate(absinthe, /*markAsModified=*/ false).setHasError(true);
tester.invalidate();
topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("joyce drank whiskey");
assertThat(tester.getDirtyKeys()).containsExactly(buildFile, top);
assertThat(tester.getDeletedKeys()).isEmpty();
}
@Test
public void dirtyDepIgnoresChildren() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey mid = skyKey("mid");
SkyKey top = skyKey("top");
tester.set(mid, new StringValue("ignore"));
tester.getOrCreate(top).addDependency(mid).setComputedValue(COPY);
tester.getOrCreate(mid).addDependency(leaf);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("ignore");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
// Change leaf.
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("ignore");
assertThat(tester.getDirtyKeys()).containsExactly(leaf);
assertThat(tester.getDeletedKeys()).isEmpty();
tester.set(leaf, new StringValue("smushy"));
tester.invalidate();
topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("ignore");
assertThat(tester.getDirtyKeys()).containsExactly(leaf);
assertThat(tester.getDeletedKeys()).isEmpty();
}
private static final SkyFunction INTERRUPT_BUILDER =
(skyKey, env) -> {
throw new InterruptedException();
};
/**
* Utility function to induce a graph clean of whatever value is requested, by trying to build
* this value and interrupting the build as soon as this value's function evaluation starts.
*/
private void failBuildAndRemoveValue(SkyKey value) throws InterruptedException {
tester.set(value, null);
// Evaluator will think leaf was interrupted because it threw, so it will be cleaned from graph.
tester.getOrCreate(value, /* markAsModified= */ true).setBuilder(INTERRUPT_BUILDER);
tester.invalidate();
assertThrows(InterruptedException.class, () -> tester.eval(/* keepGoing= */ false, value));
tester.getOrCreate(value, /* markAsModified= */ false).setBuilder(null);
}
/**
* Make sure that when a dirty value is building, the fact that a child may no longer exist in the
* graph doesn't cause problems.
*/
@Test
public void dirtyBuildAfterFailedBuild() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey top = skyKey("top");
tester.getOrCreate(top).addDependency(leaf).setComputedValue(COPY);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("leafy");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
failBuildAndRemoveValue(leaf);
// Leaf should no longer exist in the graph. Check that this doesn't cause problems.
tester.set(leaf, null);
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("crunchy");
}
/**
* Regression test: error when clearing reverse deps on dirty value about to be rebuilt, because
* child values were deleted and recreated in interim, forgetting they had reverse dep on dirty
* value in the first place.
*/
@Test
public void changedBuildAfterFailedThenSuccessfulBuild() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey top = nonHermeticKey("top");
tester.getOrCreate(top).addDependency(leaf).setComputedValue(COPY);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet(/* keepGoing= */ false, top);
assertThat(topValue.getValue()).isEqualTo("leafy");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
failBuildAndRemoveValue(leaf);
tester.set(leaf, new StringValue("crunchy"));
tester.invalidate();
tester.eval(/* keepGoing= */ false, leaf);
// Leaf no longer has reverse dep on top. Check that this doesn't cause problems, even if the
// top value is evaluated unconditionally.
tester.getOrCreate(top, /*markAsModified=*/ true);
tester.invalidate();
topValue = (StringValue) tester.evalAndGet(/*keepGoing=*/ false, top);
assertThat(topValue.getValue()).isEqualTo("crunchy");
}
/**
* Regression test: child value that has been deleted since it and its parent were marked dirty no
* longer knows it has a reverse dep on its parent.
*
* <p>Start with:
*
* <pre>
* top0 ... top1000
* \ | /
* leaf
* </pre>
*
* Then fail to build leaf. Now the entry for leaf should have no "memory" that it was ever
* depended on by tops. Now build tops, but fail again.
*/
@Test
public void manyDirtyValuesClearChildrenOnSecondFail() throws Exception {
SkyKey leafKey = nonHermeticKey("leaf");
tester.set(leafKey, new StringValue("leafy"));
SkyKey lastKey = skyKey("last");
tester.set(lastKey, new StringValue("last"));
List<SkyKey> tops = new ArrayList<>();
// Request far more top-level values than there are threads, so some of them will block until
// the leaf child is enqueued for processing.
for (int i = 0; i < 10000; i++) {
SkyKey topKey = skyKey("top" + i);
tester
.getOrCreate(topKey)
.addDependency(leafKey)
.addDependency(lastKey)
.setComputedValue(CONCATENATE);
tops.add(topKey);
}
tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0]));
failBuildAndRemoveValue(leafKey);
// Request the tops. Since leaf was deleted from the graph last build, it no longer knows that
// its parents depend on it. When leaf throws, at least one of its parents (hopefully) will not
// have re-informed leaf that the parent depends on it, exposing the bug, since the parent
// should then not try to clean the reverse dep from leaf.
tester.set(leafKey, null);
// Evaluator will think leaf was interrupted because it threw, so it will be cleaned from graph.
tester.getOrCreate(leafKey, /*markAsModified=*/ true).setBuilder(INTERRUPT_BUILDER);
tester.invalidate();
assertThrows(
InterruptedException.class,
() -> tester.eval(/*keepGoing=*/ false, tops.toArray(new SkyKey[0])));
}
@Test
public void failedDirtyBuild() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey top = skyKey("top");
tester
.getOrCreate(top)
.addErrorDependency(leaf, new StringValue("recover"))
.setComputedValue(COPY);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("leafy");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
// Change leaf.
tester.getOrCreate(leaf, /* markAsModified= */ true).setHasError(true);
tester.getOrCreate(top, /* markAsModified= */ false).setHasError(true);
tester.invalidate();
EvaluationResult<StringValue> result = tester.eval(/* keepGoing= */ false, top);
assertThatEvaluationResult(result).hasSingletonErrorThat(top);
}
@Test
public void failedDirtyBuildInBuilder() throws Exception {
SkyKey leaf = nonHermeticKey("leaf");
SkyKey secondError = nonHermeticKey("secondError");
SkyKey top = skyKey("top");
tester
.getOrCreate(top)
.addDependency(leaf)
.addErrorDependency(secondError, new StringValue("recover"))
.setComputedValue(CONCATENATE);
tester.set(secondError, new StringValue("secondError")).addDependency(leaf);
tester.set(leaf, new StringValue("leafy"));
StringValue topValue = (StringValue) tester.evalAndGet("top");
assertThat(topValue.getValue()).isEqualTo("leafysecondError");
assertThat(tester.getDirtyKeys()).isEmpty();
assertThat(tester.getDeletedKeys()).isEmpty();
// Invalidate leaf.
tester.getOrCreate(leaf, /*markAsModified=*/ true);
tester.set(leaf, new StringValue("crunchy"));
tester.getOrCreate(secondError, /*markAsModified=*/ true).setHasError(true);
tester.getOrCreate(top, /*markAsModified=*/ false).setHasError(true);
tester.invalidate();
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, top);
assertThatEvaluationResult(result).hasSingletonErrorThat(top);
}
@Test
public void dirtyErrorTransienceValue() throws Exception {
initializeTester();
SkyKey error = skyKey("error");
tester.getOrCreate(error).setHasError(true);
assertThat(tester.evalAndGetError(/*keepGoing=*/ true, error)).isNotNull();
tester.invalidateTransientErrors();
SkyKey secondError = skyKey("secondError");
tester.getOrCreate(secondError).setHasError(true);
// secondError declares a new dependence on ErrorTransienceValue, but not until it has already
// thrown an error.
assertThat(tester.evalAndGetError(/*keepGoing=*/ true, secondError)).isNotNull();
}
@Test
public void dirtyDependsOnErrorTurningGood() throws Exception {
SkyKey error = nonHermeticKey("error");
tester.getOrCreate(error).setHasError(true);
SkyKey topKey = skyKey("top");
tester.getOrCreate(topKey).addDependency(error).setComputedValue(COPY);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
tester.getOrCreate(error).setHasError(false);
StringValue val = new StringValue("reformed");
tester.set(error, val);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasEntryThat(topKey).isEqualTo(val);
assertThatEvaluationResult(result).hasNoError();
}
/** Regression test for crash bug. */
@Test
public void dirtyWithOwnErrorDependsOnTransientErrorTurningGood() throws Exception {
SkyKey error = nonHermeticKey("error");
tester.getOrCreate(error).setHasTransientError(true);
SkyKey topKey = skyKey("top");
SkyFunction errorFunction =
(skyKey, env) -> {
try {
return env.getValueOrThrow(error, SomeErrorException.class);
} catch (SomeErrorException e) {
throw new GenericFunctionException(e, Transience.PERSISTENT);
}
};
tester.getOrCreate(topKey).setBuilder(errorFunction);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
tester.invalidateTransientErrors();
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
tester.getOrCreate(error).setHasTransientError(false);
StringValue reformed = new StringValue("reformed");
tester.set(error, reformed);
tester
.getOrCreate(topKey, /*markAsModified=*/ false)
.setBuilder(null)
.addDependency(error)
.setComputedValue(COPY);
tester.invalidate();
tester.invalidateTransientErrors();
result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasEntryThat(topKey).isEqualTo(reformed);
assertThatEvaluationResult(result).hasNoError();
}
/**
* Make sure that when an error is thrown, it is given for handling only to parents that have
* already registered a dependence on the value that threw the error.
*
* <pre>
* topBubbleKey topErrorFirstKey
* | \ /
* midKey errorKey
* |
* slowKey
* </pre>
*
* On the second build, errorKey throws, and the threadpool aborts before midKey finishes.
* topBubbleKey therefore has not yet requested errorKey this build. If errorKey bubbles up to it,
* topBubbleKey must be able to handle that. (The evaluator can deal with this either by not
* allowing errorKey to bubble up to topBubbleKey, or by dealing with that case.)
*/
@Test
public void errorOnlyBubblesToRequestingParents() throws Exception {
// We need control over the order of reverse deps, so use a deterministic graph.
makeGraphDeterministic();
SkyKey errorKey = nonHermeticKey("error");
tester.set(errorKey, new StringValue("biding time"));
SkyKey slowKey = nonHermeticKey("slow");
tester.set(slowKey, new StringValue("slow"));
SkyKey midKey = skyKey("mid");
tester.getOrCreate(midKey).addDependency(slowKey).setComputedValue(COPY);
SkyKey topErrorFirstKey = skyKey("2nd top alphabetically");
tester.getOrCreate(topErrorFirstKey).addDependency(errorKey).setComputedValue(CONCATENATE);
SkyKey topBubbleKey = skyKey("1st top alphabetically");
tester
.getOrCreate(topBubbleKey)
.addDependency(midKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
// First error-free evaluation, to put all values in graph.
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, topErrorFirstKey, topBubbleKey);
assertThat(result.get(topErrorFirstKey).getValue()).isEqualTo("biding time");
assertThat(result.get(topBubbleKey).getValue()).isEqualTo("slowbiding time");
// Set up timing of child values: slowKey waits to finish until errorKey has thrown an
// exception that has been caught by the threadpool.
tester.set(slowKey, null);
CountDownLatch errorFinish = new CountDownLatch(1);
tester.set(errorKey, null);
tester
.getOrCreate(errorKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ null,
/*notifyFinish=*/ errorFinish,
/*waitForException=*/ false,
/*value=*/ null,
/*deps=*/ ImmutableList.of()));
tester
.getOrCreate(slowKey)
.setBuilder(
new ChainedFunction(
/*notifyStart=*/ null,
/*waitToFinish=*/ errorFinish,
/*notifyFinish=*/ null,
/*waitForException=*/ true,
new StringValue("leaf2"),
/*deps=*/ ImmutableList.of()));
tester.invalidate();
// errorKey finishes, written to graph -> slowKey maybe starts+finishes & (Visitor aborts)
// -> some top key builds.
result = tester.eval(/*keepGoing=*/ false, topErrorFirstKey, topBubbleKey);
assertThat(result.hasError()).isTrue();
assertWithMessage(result.toString()).that(result.getError(topErrorFirstKey)).isNotNull();
}
@Test
public void dirtyWithRecoveryErrorDependsOnErrorTurningGood() throws Exception {
SkyKey error = nonHermeticKey("error");
tester.getOrCreate(error).setHasError(true);
SkyKey topKey = skyKey("top");
SkyFunction recoveryErrorFunction =
(skyKey, env) -> {
try {
env.getValueOrThrow(error, SomeErrorException.class);
} catch (SomeErrorException e) {
throw new GenericFunctionException(e, Transience.PERSISTENT);
}
return null;
};
tester.getOrCreate(topKey).setBuilder(recoveryErrorFunction);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(topKey);
tester.getOrCreate(error).setHasError(false);
StringValue reformed = new StringValue("reformed");
tester.set(error, reformed);
tester.getOrCreate(topKey).setBuilder(null).addDependency(error).setComputedValue(COPY);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ false, topKey);
assertThatEvaluationResult(result).hasEntryThat(topKey).isEqualTo(reformed);
assertThatEvaluationResult(result).hasNoError();
}
/**
* Similar to {@link ParallelEvaluatorTest#errorTwoLevelsDeep}, except here we request multiple
* toplevel values.
*/
@Test
public void errorPropagationToTopLevelValues() throws Exception {
SkyKey topKey = skyKey("top");
SkyKey midKey = skyKey("mid");
SkyKey badKey = skyKey("bad");
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
tester.getOrCreate(midKey).addDependency(badKey).setComputedValue(CONCATENATE);
tester.getOrCreate(badKey).setHasError(true);
EvaluationResult<SkyValue> result = tester.eval(/* keepGoing= */ false, topKey, midKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey);
// Do it again with keepGoing. We should also see an error for the top key this time.
result = tester.eval(/* keepGoing= */ true, topKey, midKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey);
}
@Test
public void breakWithInterruptibleErrorDep() throws Exception {
SkyKey errorKey = skyKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE);
// When the error value throws, the propagation will cause an interrupted exception in parent.
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.keyNames()).isEmpty();
assertThatEvaluationResult(result).hasErrorMapThat().hasSize(1);
assertThatEvaluationResult(result).hasErrorMapThat().containsKey(parentKey);
assertThat(Thread.interrupted()).isFalse();
result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThatEvaluationResult(result).hasNoError();
assertThatEvaluationResult(result)
.hasEntryThat(parentKey)
.isEqualTo(new StringValue("recovered"));
}
/**
* Regression test: "clearing incomplete values on --keep_going build is racy". Tests that if a
* value is requested on the first (non-keep-going) build and its child throws an error, when the
* second (keep-going) build runs, there is not a race that keeps it as a reverse dep of its
* children.
*/
@Test
public void raceClearingIncompleteValues() throws Exception {
// Make sure top is enqueued before mid, to avoid a deadlock.
SkyKey topKey = skyKey("aatop");
SkyKey midKey = skyKey("zzmid");
SkyKey badKey = skyKey("bad");
AtomicBoolean waitForSecondCall = new AtomicBoolean(false);
CountDownLatch otherThreadWinning = new CountDownLatch(1);
AtomicReference<Thread> firstThread = new AtomicReference<>();
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!waitForSecondCall.get()) {
return;
}
if (key.equals(midKey)) {
if (type == EventType.CREATE_IF_ABSENT) {
// The first thread to create midKey will not be the first thread to add a reverse dep
// to it.
firstThread.compareAndSet(null, Thread.currentThread());
return;
}
if (type == EventType.ADD_REVERSE_DEP) {
if (order == Order.BEFORE && Thread.currentThread().equals(firstThread.get())) {
// If this thread created midKey, block until the other thread adds a dep on it.
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
otherThreadWinning, "other thread didn't pass this one");
} else if (order == Order.AFTER
&& !Thread.currentThread().equals(firstThread.get())) {
// This thread has added a dep. Allow the other thread to proceed.
otherThreadWinning.countDown();
}
}
}
},
/* deterministic= */ true);
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
tester.getOrCreate(midKey).addDependency(badKey).setComputedValue(CONCATENATE);
tester.getOrCreate(badKey).setHasError(true);
EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/ false, topKey, midKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey);
waitForSecondCall.set(true);
result = tester.eval(/*keepGoing=*/ true, topKey, midKey);
assertThat(firstThread.get()).isNotNull();
assertThat(otherThreadWinning.getCount()).isEqualTo(0);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey);
}
@Test
public void breakWithErrorDep() throws Exception {
SkyKey errorKey = skyKey("my_error_value");
tester.getOrCreate(errorKey).setHasError(true);
tester.set("after", new StringValue("after"));
SkyKey parentKey = skyKey("parent");
tester
.getOrCreate(parentKey)
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE)
.addDependency("after");
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThatEvaluationResult(result).hasSingletonErrorThat(parentKey);
result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThatEvaluationResult(result).hasNoError();
assertThatEvaluationResult(result)
.hasEntryThat(parentKey)
.isEqualTo(new StringValue("recoveredafter"));
}
@Test
public void raceConditionWithNoKeepGoingErrors_InflightError() throws Exception {
// Given a graph of two nodes, errorKey and otherErrorKey,
SkyKey errorKey = skyKey("errorKey");
SkyKey otherErrorKey = skyKey("otherErrorKey");
CountDownLatch errorCommitted = new CountDownLatch(1);
CountDownLatch otherStarted = new CountDownLatch(1);
CountDownLatch otherDone = new CountDownLatch(1);
AtomicInteger numOtherInvocations = new AtomicInteger(0);
AtomicReference<String> bogusInvocationMessage = new AtomicReference<>(null);
AtomicReference<String> nonNullValueMessage = new AtomicReference<>(null);
tester
.getOrCreate(errorKey)
.setBuilder(
(skyKey, env) -> {
// Given that errorKey waits for otherErrorKey to begin evaluation before completing
// its evaluation,
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
otherStarted, "otherErrorKey's SkyFunction didn't start in time.");
// And given that errorKey throws an error,
throw new GenericFunctionException(
new SomeErrorException("error"), Transience.PERSISTENT);
});
tester
.getOrCreate(otherErrorKey)
.setBuilder(
(skyKey, env) -> {
otherStarted.countDown();
int invocations = numOtherInvocations.incrementAndGet();
// And given that otherErrorKey waits for errorKey's error to be committed before
// trying to get errorKey's value,
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
errorCommitted, "errorKey's error didn't get committed to the graph in time");
try {
SkyValue value = env.getValueOrThrow(errorKey, SomeErrorException.class);
if (value != null) {
nonNullValueMessage.set("bogus non-null value " + value);
}
if (invocations != 1) {
bogusInvocationMessage.set("bogus invocation count: " + invocations);
}
otherDone.countDown();
// And given that otherErrorKey throws an error,
throw new GenericFunctionException(
new SomeErrorException("other"), Transience.PERSISTENT);
} catch (SomeErrorException e) {
fail();
return null;
}
});
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (key.equals(errorKey) && type == EventType.SET_VALUE && order == Order.AFTER) {
errorCommitted.countDown();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
otherDone, "otherErrorKey's SkyFunction didn't finish in time.");
}
},
/*deterministic=*/ false);
// When the graph is evaluated in noKeepGoing mode,
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, errorKey, otherErrorKey);
// Then the result reports that an error occurred,
assertThat(result.hasError()).isTrue();
// And no value is committed for otherErrorKey,
assertThat(tester.evaluator.getExistingErrorForTesting(otherErrorKey)).isNull();
assertThat(tester.evaluator.getExistingValue(otherErrorKey)).isNull();
// And no value was committed for errorKey,
assertWithMessage(nonNullValueMessage.get()).that(nonNullValueMessage.get()).isNull();
// And the SkyFunction for otherErrorKey was evaluated exactly once.
assertThat(numOtherInvocations.get()).isEqualTo(1);
assertWithMessage(bogusInvocationMessage.get()).that(bogusInvocationMessage.get()).isNull();
}
@Test
public void absentParent() throws Exception {
SkyKey errorKey = nonHermeticKey("my_error_value");
tester.set(errorKey, new StringValue("biding time"));
SkyKey absentParentKey = skyKey("absentParent");
tester.getOrCreate(absentParentKey).addDependency(errorKey).setComputedValue(CONCATENATE);
assertThat(tester.evalAndGet(/*keepGoing=*/ false, absentParentKey))
.isEqualTo(new StringValue("biding time"));
tester.getOrCreate(errorKey, /*markAsModified=*/ true).setHasError(true);
SkyKey newParent = skyKey("newParent");
tester.getOrCreate(newParent).addDependency(errorKey).setComputedValue(CONCATENATE);
tester.invalidate();
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, newParent);
assertThatEvaluationResult(result).hasSingletonErrorThat(newParent);
}
@Test
public void notComparableNotPrunedNoEvent() throws Exception {
checkNotComparableNotPruned(false);
}
@Test
public void notComparableNotPrunedEvent() throws Exception {
checkNotComparableNotPruned(true);
}
private void checkNotComparableNotPruned(boolean hasEvent) throws Exception {
SkyKey parent = skyKey("parent");
SkyKey child = nonHermeticKey("child");
NotComparableStringValue notComparableString = new NotComparableStringValue("not comparable");
if (hasEvent) {
tester.getOrCreate(child).setConstantValue(notComparableString).setWarning("shmoop");
} else {
tester.getOrCreate(child).setConstantValue(notComparableString);
}
AtomicInteger parentEvaluated = new AtomicInteger();
StringValue val = new StringValue("some val");
tester
.getOrCreate(parent)
.addDependency(child)
.setComputedValue(
(deps, env) -> {
parentEvaluated.incrementAndGet();
return val;
});
assertThat(tester.evalAndGet(/* keepGoing= */ false, parent)).isEqualTo(val);
assertThat(parentEvaluated.get()).isEqualTo(1);
if (hasEvent) {
assertThatEvents(eventCollector).containsExactly("shmoop");
} else {
assertThatEvents(eventCollector).isEmpty();
}
eventCollector.clear();
tester.resetPlayedEvents();
tester.getOrCreate(child, /*markAsModified=*/ true);
tester.invalidate();
assertThat(tester.evalAndGet(/* keepGoing= */ false, parent)).isEqualTo(val);
assertThat(parentEvaluated.get()).isEqualTo(2);
if (hasEvent) {
assertThatEvents(eventCollector).containsExactly("shmoop");
} else {
assertThatEvents(eventCollector).isEmpty();
}
}
@Test
public void changePruningWithEvent() throws Exception {
SkyKey parent = skyKey("parent");
SkyKey child = nonHermeticKey("child");
tester.getOrCreate(child).setConstantValue(new StringValue("child")).setWarning("bloop");
// Restart once because child isn't ready.
CountDownLatch parentEvaluated = new CountDownLatch(3);
StringValue parentVal = new StringValue("parent");
tester
.getOrCreate(parent)
.setBuilder(
new ChainedFunction(
parentEvaluated, null, null, false, parentVal, ImmutableList.of(child)));
assertThat(tester.evalAndGet(/* keepGoing= */ false, parent)).isEqualTo(parentVal);
assertThat(parentEvaluated.getCount()).isEqualTo(1);
assertThatEvents(eventCollector).containsExactly("bloop");
eventCollector.clear();
tester.resetPlayedEvents();
tester.getOrCreate(child, /*markAsModified=*/ true);
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ false, parent)).isEqualTo(parentVal);
assertThatEvents(eventCollector).containsExactly("bloop");
assertThat(parentEvaluated.getCount()).isEqualTo(1);
}
@Test
public void changePruningWithIntermittentEvent() throws Exception {
String parentEvent = "parent_event";
String waitEvent = "wait_event";
String childEvent = "child_event";
SkyKey wait = skyKey("wait_key");
SkyKey parent = skyKey("parent_key");
SkyKey child = nonHermeticKey("child_key");
StringValue parentStringValue = new StringValue("parent_value");
StringValue waitStringValue = new StringValue("wait_value");
CountDownLatch parentEvaluated = new CountDownLatch(2);
reporter =
new DelegatingEventHandler(reporter) {
@Override
public void handle(Event e) {
super.handle(e);
// Release the CountDownLatch every time the parent node fires the event
if (e.getMessage().equals(parentEvent)) {
parentEvaluated.countDown();
}
}
};
tester
.getOrCreate(wait)
.setBuilder(
(skyKey, env) -> {
// Wait for the parent and child actions to complete before computing wait node
parentEvaluated.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertThatEvents(eventCollector).containsExactly(childEvent, parentEvent);
env.getListener().handle(Event.progress(waitEvent));
return waitStringValue;
});
tester
.getOrCreate(child)
.setConstantValue(new StringValue("child_value"))
.setWarning(childEvent);
tester
.getOrCreate(parent)
.addDependency(child)
.setConstantValue(parentStringValue)
.setErrorEvent(parentEvent);
assertThat(tester.evalAndGet(/*keepGoing=*/ false, parent)).isEqualTo(parentStringValue);
assertThatEvents(eventCollector).containsExactly(childEvent, parentEvent);
assertThat(parentEvaluated.getCount()).isEqualTo(1);
// Reset the event collector and mark the child as modified without actually changing values
eventCollector.clear();
tester.resetPlayedEvents();
tester.getOrCreate(child, /*markAsModified=*/ true);
tester.invalidate();
EvaluationResult<StringValue> result = tester.eval(false, parent, wait);
assertThat(result.values()).containsExactly(parentStringValue, waitStringValue);
// These assertions are to check that all events fired at the end of evaluation.
assertThat(parentEvaluated.getCount()).isEqualTo(0);
assertThatEvents(eventCollector).containsExactly(childEvent, parentEvent, waitEvent);
}
@Test
public void depEventPredicate() throws Exception {
SkyKey parent = skyKey("parent");
SkyKey excludedDep = skyKey("excludedDep");
SkyKey includedDep = skyKey("includedDep");
tester.setEventFilter(
new EventFilter() {
@Override
public boolean storeEvents() {
return true;
}
@Override
public boolean shouldPropagate(SkyKey depKey, SkyKey primaryKey) {
return !primaryKey.equals(parent) || depKey.equals(includedDep);
}
});
tester.initialize();
tester
.getOrCreate(parent)
.addDependency(excludedDep)
.addDependency(includedDep)
.setComputedValue(CONCATENATE);
tester
.getOrCreate(excludedDep)
.setErrorEvent("excludedDep error message")
.setConstantValue(new StringValue("excludedDep"));
tester
.getOrCreate(includedDep)
.setErrorEvent("includedDep error message")
.setConstantValue(new StringValue("includedDep"));
tester.eval(/* keepGoing= */ false, includedDep, excludedDep);
assertThatEvents(eventCollector)
.containsExactly("excludedDep error message", "includedDep error message");
eventCollector.clear();
emittedEventState.clear();
tester.eval(/* keepGoing= */ true, parent);
assertThatEvents(eventCollector).containsExactly("includedDep error message");
assertThat(
ValueWithMetadata.getEvents(
tester
.evaluator
.getExistingEntryAtCurrentlyEvaluatingVersion(parent)
.getValueMaybeWithMetadata())
.toList())
.containsExactly(Event.error("includedDep error message"));
}
// Tests that we have a sane implementation of error transience.
@Test
public void errorTransienceBug() throws Exception {
tester.getOrCreate("key").setHasTransientError(true);
assertThat(tester.evalAndGetError(/*keepGoing=*/ true, "key").getException()).isNotNull();
StringValue value = new StringValue("hi");
tester.getOrCreate("key").setHasTransientError(false).setConstantValue(value);
tester.invalidateTransientErrors();
assertThat(tester.evalAndGet("key")).isEqualTo(value);
// This works because the version of the ValueEntry for the ErrorTransience value is always
// increased on each InMemoryMemoizingEvaluator#evaluate call. But that's not the only way to
// implement error transience; another valid implementation would be to unconditionally mark
// values depending on the ErrorTransience value as being changed (rather than merely dirtied)
// during invalidation.
}
@Test
public void transientErrorTurningGoodHasNoError() throws Exception {
initializeTester();
SkyKey errorKey = skyKey("my_error_value");
tester.getOrCreate(errorKey).setHasTransientError(true);
ErrorInfo errorInfo = tester.evalAndGetError(/*keepGoing=*/ true, errorKey);
assertThat(errorInfo).isNotNull();
// Re-evaluates to same thing when errors are invalidated
tester.invalidateTransientErrors();
errorInfo = tester.evalAndGetError(/*keepGoing=*/ true, errorKey);
assertThat(errorInfo).isNotNull();
StringValue value = new StringValue("reformed");
tester
.getOrCreate(errorKey, /*markAsModified=*/ false)
.setHasTransientError(false)
.setConstantValue(value);
tester.invalidateTransientErrors();
StringValue stringValue = (StringValue) tester.evalAndGet(/*keepGoing=*/ true, errorKey);
assertThat(value).isSameInstanceAs(stringValue);
// Value builder will now throw, but we should never get to it because it isn't dirty.
tester.getOrCreate(errorKey, /*markAsModified=*/ false).setHasTransientError(true);
tester.invalidateTransientErrors();
stringValue = (StringValue) tester.evalAndGet(/*keepGoing=*/ true, errorKey);
assertThat(stringValue).isEqualTo(value);
}
@Test
public void transientErrorTurnsGoodOnSecondTry() throws Exception {
SkyKey leafKey = skyKey("leaf");
SkyKey errorKey = skyKey("error");
SkyKey topKey = skyKey("top");
StringValue value = new StringValue("val");
tester.getOrCreate(topKey).addDependency(errorKey).setConstantValue(value);
tester
.getOrCreate(errorKey)
.addDependency(leafKey)
.setConstantValue(value)
.setHasTransientError(true);
tester.getOrCreate(leafKey).setConstantValue(new StringValue("leaf"));
ErrorInfo errorInfo = tester.evalAndGetError(/* keepGoing= */ true, topKey);
assertThat(errorInfo).isNotNull();
assertThatErrorInfo(errorInfo).isTransient();
tester.invalidateTransientErrors();
errorInfo = tester.evalAndGetError(/*keepGoing=*/ true, topKey);
assertThat(errorInfo).isNotNull();
assertThatErrorInfo(errorInfo).isTransient();
tester.invalidateTransientErrors();
tester.getOrCreate(errorKey, /*markAsModified=*/ false).setHasTransientError(false);
assertThat(tester.evalAndGet(/*keepGoing=*/ true, topKey)).isEqualTo(value);
}
@Test
public void deleteInvalidatedValue() throws Exception {
SkyKey top = skyKey("top");
SkyKey toDelete = nonHermeticKey("toDelete");
// Must be a concatenation -- COPY doesn't actually copy.
tester.getOrCreate(top).addDependency(toDelete).setComputedValue(CONCATENATE);
tester.set(toDelete, new StringValue("toDelete"));
SkyValue value = tester.evalAndGet("top");
SkyKey forceInvalidation = skyKey("forceInvalidation");
tester.set(forceInvalidation, new StringValue("forceInvalidation"));
tester.getOrCreate(toDelete, /* markAsModified= */ true);
tester.invalidate();
tester.eval(/* keepGoing= */ false, forceInvalidation);
tester.delete("toDelete");
WeakReference<SkyValue> ref = new WeakReference<>(value);
value = null;
tester.eval(/*keepGoing=*/ false, forceInvalidation);
tester.invalidate(); // So that invalidation receiver doesn't hang on to reference.
GcFinalization.awaitClear(ref);
}
/**
* General stress/fuzz test of the evaluator with failure. Construct a large graph, and then throw
* exceptions during building at various points.
*/
@Test
public void twoRailLeftRightDependenciesWithFailure() throws Exception {
initializeTester();
SkyKey[] leftValues = new SkyKey[TEST_NODE_COUNT];
SkyKey[] rightValues = new SkyKey[TEST_NODE_COUNT];
for (int i = 0; i < TEST_NODE_COUNT; i++) {
leftValues[i] = nonHermeticKey("left-" + i);
rightValues[i] = skyKey("right-" + i);
if (i == 0) {
tester.getOrCreate(leftValues[i]).addDependency("leaf").setComputedValue(COPY);
tester.getOrCreate(rightValues[i]).addDependency("leaf").setComputedValue(COPY);
} else {
tester
.getOrCreate(leftValues[i])
.addDependency(leftValues[i - 1])
.addDependency(rightValues[i - 1])
.setComputedValue(new PassThroughSelected(leftValues[i - 1]));
tester
.getOrCreate(rightValues[i])
.addDependency(leftValues[i - 1])
.addDependency(rightValues[i - 1])
.setComputedValue(new PassThroughSelected(rightValues[i - 1]));
}
}
tester.set("leaf", new StringValue("leaf"));
SkyKey lastLeft = nonHermeticKey("left-" + (TEST_NODE_COUNT - 1));
SkyKey lastRight = skyKey("right-" + (TEST_NODE_COUNT - 1));
for (int i = 0; i < TESTED_NODES; i++) {
try {
tester.getOrCreate(leftValues[i], /* markAsModified= */ true).setHasError(true);
tester.invalidate();
EvaluationResult<StringValue> result =
tester.eval(/* keepGoing= */ false, lastLeft, lastRight);
assertThat(result.hasError()).isTrue();
tester.differencer.invalidate(ImmutableList.of(leftValues[i]));
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, lastLeft, lastRight);
assertThat(result.hasError()).isTrue();
tester.getOrCreate(leftValues[i], /*markAsModified=*/ true).setHasError(false);
tester.invalidate();
result = tester.eval(/* keepGoing= */ false, lastLeft, lastRight);
assertThat(result.get(lastLeft)).isEqualTo(new StringValue("leaf"));
assertThat(result.get(lastRight)).isEqualTo(new StringValue("leaf"));
} catch (Exception e) {
System.err.println("twoRailLeftRightDependenciesWithFailure exception on run " + i);
throw e;
}
}
}
@Test
public void valueInjection() throws Exception {
SkyKey key = nonHermeticKey("new_value");
Delta delta = Delta.justNew(new StringValue("val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEntry() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate(key).setConstantValue(new StringValue("old_val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingDirtyEntry() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate(key).setConstantValue(new StringValue("old_val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
tester.eval(/*keepGoing=*/ false, new SkyKey[0]); // Create the value.
tester.differencer.invalidate(ImmutableList.of(key));
tester.eval(/*keepGoing=*/ false, new SkyKey[0]); // Mark value as dirty.
tester.differencer.inject(ImmutableMap.of(key, delta));
tester.eval(/*keepGoing=*/ false, new SkyKey[0]); // Inject again.
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEntryMarkedForInvalidation() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate(key).setConstantValue(new StringValue("old_val"));
tester.differencer.invalidate(ImmutableList.of(key));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEntryMarkedForDeletion() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate(key).setConstantValue(new StringValue("old_val"));
tester.evaluator.delete(Predicates.alwaysTrue());
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEqualEntryMarkedForInvalidation() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
tester.differencer.invalidate(ImmutableList.of(key));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEqualEntryMarkedForDeletion() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
tester.evaluator.delete(Predicates.alwaysTrue());
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverValueWithDeps() throws Exception {
SkyKey key = nonHermeticKey("key");
SkyKey otherKey = nonHermeticKey("other");
Delta delta = Delta.justNew(new StringValue("val"));
StringValue prevVal = new StringValue("foo");
tester.getOrCreate(otherKey).setConstantValue(prevVal);
tester.getOrCreate(key).addDependency(otherKey).setComputedValue(COPY);
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(prevVal);
tester.differencer.inject(ImmutableMap.of(key, delta));
StringValue depVal = new StringValue("newfoo");
tester.getOrCreate(otherKey).setConstantValue(depVal);
tester.differencer.invalidate(ImmutableList.of(otherKey));
// Injected value is ignored for value with deps.
assertThat(tester.evalAndGet(/*keepGoing=*/ false, key)).isEqualTo(depVal);
}
@Test
public void valueInjectionOverEqualValueWithDeps() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate("other").setConstantValue(delta.newValue());
tester.getOrCreate(key).addDependency("other").setComputedValue(COPY);
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverValueWithErrors() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.getOrCreate(key).setHasError(true);
tester.evalAndGetError(/*keepGoing=*/ true, key);
tester.differencer.inject(ImmutableMap.of(key, delta));
assertThat(tester.evalAndGet(false, key)).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionInvalidatesReverseDeps() throws Exception {
SkyKey childKey = nonHermeticKey("child");
SkyKey parentKey = skyKey("parent");
StringValue oldVal = new StringValue("old_val");
tester.getOrCreate(childKey).setConstantValue(oldVal);
tester.getOrCreate(parentKey).addDependency(childKey).setComputedValue(COPY);
EvaluationResult<SkyValue> result = tester.eval(false, parentKey);
assertThat(result.hasError()).isFalse();
assertThat(result.get(parentKey)).isEqualTo(oldVal);
Delta delta = Delta.justNew(new StringValue("val"));
tester.differencer.inject(ImmutableMap.of(childKey, delta));
assertThat(tester.evalAndGet(/* keepGoing= */ false, childKey)).isEqualTo(delta.newValue());
// Injecting a new child should have invalidated the parent.
assertThat(tester.getExistingValue("parent")).isNull();
tester.eval(false, childKey);
assertThat(tester.getExistingValue(childKey)).isEqualTo(delta.newValue());
assertThat(tester.getExistingValue("parent")).isNull();
assertThat(tester.evalAndGet("parent")).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionOverExistingEqualEntryDoesNotInvalidate() throws Exception {
SkyKey childKey = nonHermeticKey("child");
SkyKey parentKey = skyKey("parent");
Delta delta = Delta.justNew(new StringValue("same_val"));
tester.getOrCreate(parentKey).addDependency(childKey).setComputedValue(COPY);
tester.getOrCreate(childKey).setConstantValue(new StringValue("same_val"));
assertThat(tester.evalAndGet("parent")).isEqualTo(delta.newValue());
tester.differencer.inject(ImmutableMap.of(childKey, delta));
assertThat(tester.getExistingValue(childKey)).isEqualTo(delta.newValue());
// Since we are injecting an equal value, the parent should not have been invalidated.
assertThat(tester.getExistingValue("parent")).isEqualTo(delta.newValue());
}
@Test
public void valueInjectionInterrupt() throws Exception {
SkyKey key = nonHermeticKey("key");
Delta delta = Delta.justNew(new StringValue("val"));
tester.differencer.inject(ImmutableMap.of(key, delta));
Thread.currentThread().interrupt();
assertThrows(InterruptedException.class, () -> tester.evalAndGet(/*keepGoing=*/ false, key));
SkyValue newVal = tester.evalAndGet(/*keepGoing=*/ false, key);
assertThat(newVal).isEqualTo(delta.newValue());
}
protected void runTestPersistentErrorsNotRerun(boolean includeTransientError) throws Exception {
SkyKey topKey = skyKey("top");
SkyKey transientErrorKey = skyKey("transientError");
SkyKey persistentErrorKey1 = skyKey("persistentError1");
SkyKey persistentErrorKey2 = skyKey("persistentError2");
TestFunction topFunction =
tester
.getOrCreate(topKey)
.addErrorDependency(persistentErrorKey1, new StringValue("doesn't matter"))
.setHasError(true);
tester.getOrCreate(persistentErrorKey1).setHasError(true);
if (includeTransientError) {
topFunction.addErrorDependency(transientErrorKey, new StringValue("doesn't matter"));
tester
.getOrCreate(transientErrorKey)
.addErrorDependency(persistentErrorKey2, new StringValue("doesn't matter"))
.setHasTransientError(true);
}
tester.getOrCreate(persistentErrorKey2).setHasError(true);
tester.evalAndGetError(/*keepGoing=*/ true, topKey);
if (includeTransientError) {
assertThat(tester.getEnqueuedValues())
.containsExactly(topKey, transientErrorKey, persistentErrorKey1, persistentErrorKey2);
} else {
assertThat(tester.getEnqueuedValues()).containsExactly(topKey, persistentErrorKey1);
}
tester.invalidate();
tester.invalidateTransientErrors();
tester.evalAndGetError(/*keepGoing=*/ true, topKey);
if (includeTransientError) {
// TODO(bazel-team): We can do better here once we implement change pruning for errors.
assertThat(tester.getEnqueuedValues()).containsExactly(topKey, transientErrorKey);
} else {
assertThat(tester.getEnqueuedValues()).isEmpty();
}
}
@Test
public void persistentErrorsNotRerun() throws Exception {
runTestPersistentErrorsNotRerun(/*includeTransientError=*/ true);
}
/**
* The following two tests check that the evaluator shuts down properly when encountering an error
* that is marked dirty but later verified to be unchanged from a prior build. In that case, the
* invariant that its parents are not enqueued for evaluation should be maintained.
*/
/**
* Test that a parent of a cached but invalidated error doesn't successfully build. First build
* the error. Then invalidate the error via a dependency (so it will not actually change) and
* build two new parents. Parent A will request error and abort since error isn't done yet. error
* is then revalidated, and A is restarted. If A does not throw upon encountering the error, and
* instead sets its value, then we throw in parent B, which waits for error to be done before
* requesting it. Then there will be the impossible situation of a node that was built during this
* evaluation depending on a node in error.
*/
@Test
public void shutDownBuildOnCachedError_Done() throws Exception {
// errorKey will be invalidated due to its dependence on invalidatedKey, but later revalidated
// since invalidatedKey re-evaluates to the same value on a subsequent build.
SkyKey errorKey = skyKey("error");
SkyKey invalidatedKey = nonHermeticKey("invalidated-leaf");
tester.set(invalidatedKey, new StringValue("invalidated-leaf-value"));
tester.getOrCreate(errorKey).addDependency(invalidatedKey).setHasError(true);
// Names are alphabetized in reverse deps of errorKey.
SkyKey fastToRequestSlowToSetValueKey = skyKey("A-slow-set-value-parent");
SkyKey failingKey = skyKey("B-fast-fail-parent");
tester
.getOrCreate(fastToRequestSlowToSetValueKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
tester.getOrCreate(failingKey).addDependency(errorKey).setComputedValue(CONCATENATE);
// We only want to force a particular order of operations at some points during evaluation.
AtomicBoolean synchronizeThreads = new AtomicBoolean(false);
// We don't expect slow-set-value to actually be built, but if it is, we wait for it.
CountDownLatch slowBuilt = new CountDownLatch(1);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (!synchronizeThreads.get()) {
return;
}
if (type == EventType.GET_LIFECYCLE_STATE && key.equals(failingKey)) {
// Wait for the build to abort or for the other node to incorrectly build.
try {
assertThat(slowBuilt.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.isTrue();
} catch (InterruptedException e) {
// This is ok, because it indicates the build is shutting down.
Thread.currentThread().interrupt();
}
} else if (type == EventType.SET_VALUE
&& key.equals(fastToRequestSlowToSetValueKey)
&& order == Order.AFTER) {
// This indicates a problem -- this parent shouldn't be built since it depends on
// an error.
slowBuilt.countDown();
// Before this node actually sets its value (and then throws an exception) we wait
// for the other node to throw an exception.
try {
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
throw new IllegalStateException("uninterrupted in " + key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
},
/* deterministic= */ true);
// Initialize graph.
tester.eval(/*keepGoing=*/ true, errorKey);
tester.getOrCreate(invalidatedKey, /*markAsModified=*/ true);
tester.invalidate();
synchronizeThreads.set(true);
tester.eval(/*keepGoing=*/ false, fastToRequestSlowToSetValueKey, failingKey);
}
/**
* Test that the invalidated parent of a cached but invalidated error doesn't get marked clean.
* First build the parent -- it will contain an error. Then invalidate the error via a dependency
* (so it will not actually change) and then build the parent and another node that depends on the
* error. The other node will wait to throw until the parent is signaled that all of its
* dependencies are done, or until it is interrupted. If it throws, the parent will be
* VERIFIED_CLEAN but not done, which is not a valid state once evaluation shuts down. The
* evaluator avoids this situation by throwing when the error is encountered, even though the
* error isn't evaluated or requested by an evaluating node.
*/
@Test
public void shutDownBuildOnCachedError_Verified() throws Exception {
// TrackingProgressReceiver does unnecessary examination of node values.
initializeTester(createTrackingProgressReceiver(/* checkEvaluationResults= */ false));
// errorKey will be invalidated due to its dependence on invalidatedKey, but later revalidated
// since invalidatedKey re-evaluates to the same value on a subsequent build.
SkyKey errorKey = skyKey("error");
SkyKey invalidatedKey = nonHermeticKey("invalidated-leaf");
SkyKey changedKey = nonHermeticKey("changed-leaf");
tester.set(invalidatedKey, new StringValue("invalidated-leaf-value"));
tester.set(changedKey, new StringValue("changed-leaf-value"));
// Names are alphabetized in reverse deps of errorKey.
SkyKey cachedParentKey = skyKey("A-cached-parent");
SkyKey uncachedParentKey = skyKey("B-uncached-parent");
tester.getOrCreate(errorKey).addDependency(invalidatedKey).setHasError(true);
tester.getOrCreate(cachedParentKey).addDependency(errorKey).setComputedValue(CONCATENATE);
tester
.getOrCreate(uncachedParentKey)
.addDependency(changedKey)
.addDependency(errorKey)
.setComputedValue(CONCATENATE);
// We only want to force a particular order of operations at some points during evaluation. In
// particular, we don't want to force anything during error bubbling.
AtomicBoolean synchronizeThreads = new AtomicBoolean(false);
CountDownLatch shutdownAwaiterStarted = new CountDownLatch(1);
injectGraphListenerForTesting(
new Listener() {
private final CountDownLatch cachedSignaled = new CountDownLatch(1);
@Override
public void accept(SkyKey key, EventType type, Order order, Object context) {
if (!synchronizeThreads.get() || order != Order.BEFORE || type != EventType.SIGNAL) {
return;
}
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
shutdownAwaiterStarted, "shutdown awaiter not started");
if (key.equals(uncachedParentKey)) {
// When the uncached parent is first signaled by its changed dep, make sure that
// we wait until the cached parent is signaled too.
try {
assertThat(cachedSignaled.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.isTrue();
} catch (InterruptedException e) {
// Before the relevant bug was fixed, this code was not interrupted, and the
// uncached parent got to build, yielding an inconsistent state at a later point
// during evaluation. With the bugfix, the cached parent is never signaled
// before the evaluator shuts down, and so the above code is interrupted.
Thread.currentThread().interrupt();
}
} else if (key.equals(cachedParentKey)) {
// This branch should never be reached by a well-behaved evaluator, since when the
// error node is reached, the evaluator should shut down. However, we don't test
// for that behavior here because that would be brittle and we expect that such an
// evaluator will crash hard later on in any case.
cachedSignaled.countDown();
try {
// Sleep until we're interrupted by the evaluator, so we know it's shutting
// down.
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
Thread currentThread = Thread.currentThread();
throw new IllegalStateException(
"no interruption in time in "
+ key
+ " for "
+ (currentThread.isInterrupted() ? "" : "un")
+ "interrupted "
+ currentThread
+ " with hash "
+ System.identityHashCode(currentThread)
+ " at "
+ System.currentTimeMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
},
/* deterministic= */ true);
// Initialize graph.
tester.eval(/*keepGoing=*/ true, cachedParentKey, uncachedParentKey);
tester.getOrCreate(invalidatedKey, /*markAsModified=*/ true);
tester.set(changedKey, new StringValue("new value"));
tester.invalidate();
synchronizeThreads.set(true);
SkyKey waitForShutdownKey = skyKey("wait-for-shutdown");
tester
.getOrCreate(waitForShutdownKey)
.setBuilder(
(skyKey, env) -> {
shutdownAwaiterStarted.countDown();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
((SkyFunctionEnvironment) env).getExceptionLatchForTesting(),
"exception not thrown");
// Threadpool is shutting down. Don't try to synchronize anything in the future
// during error bubbling.
synchronizeThreads.set(false);
throw new InterruptedException();
});
EvaluationResult<StringValue> result =
tester.eval(/*keepGoing=*/ false, cachedParentKey, uncachedParentKey, waitForShutdownKey);
assertWithMessage(result.toString()).that(result.hasError()).isTrue();
tester.getOrCreate(invalidatedKey, /*markAsModified=*/ true);
tester.invalidate();
result = tester.eval(/*keepGoing=*/ false, cachedParentKey, uncachedParentKey);
assertWithMessage(result.toString()).that(result.hasError()).isTrue();
}
/**
* Tests that a race between a node being marked clean and another node requesting it is benign.
* Here, we first evaluate errorKey, depending on invalidatedKey. Then we invalidate
* invalidatedKey (without actually changing it) and evaluate errorKey and topKey together.
* Through forced synchronization, we make sure that the following sequence of events happens:
*
* <ol>
* <li>topKey requests errorKey;
* <li>errorKey is marked clean;
* <li>topKey finishes its first evaluation and registers its deps;
* <li>topKey restarts, since it sees that its only dep, errorKey, is done;
* <li>topKey sees the error thrown by errorKey and throws the error, shutting down the
* threadpool;
* </ol>
*/
@Test
public void cachedErrorCausesRestart() throws Exception {
// TrackingProgressReceiver does unnecessary examination of node values.
initializeTester(createTrackingProgressReceiver(/* checkEvaluationResults= */ false));
SkyKey errorKey = skyKey("error");
SkyKey invalidatedKey = nonHermeticKey("invalidated");
SkyKey topKey = skyKey("top");
tester.getOrCreate(errorKey).addDependency(invalidatedKey).setHasError(true);
tester.getOrCreate(invalidatedKey).setConstantValue(new StringValue("constant"));
CountDownLatch topSecondEval = new CountDownLatch(2);
CountDownLatch topRequestedError = new CountDownLatch(1);
CountDownLatch errorMarkedClean = new CountDownLatch(1);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (errorKey.equals(key) && type == EventType.MARK_CLEAN) {
if (order == Order.BEFORE) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
topRequestedError, "top didn't request");
} else {
errorMarkedClean.countDown();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
topSecondEval, "top didn't restart");
// Make sure that the other thread notices the error and interrupts this thread.
try {
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
},
/* deterministic= */ false);
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, errorKey);
assertThatEvaluationResult(result).hasError();
assertThatEvaluationResult(result)
.hasErrorEntryForKeyThat(errorKey)
.hasExceptionThat()
.isNotNull();
tester
.getOrCreate(topKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
topSecondEval.countDown();
env.getValue(errorKey);
topRequestedError.countDown();
assertThat(env.valuesMissing()).isTrue();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
errorMarkedClean, "error not marked clean");
return null;
}
});
tester.getOrCreate(invalidatedKey, /*markAsModified=*/ true);
tester.invalidate();
EvaluationResult<StringValue> result2 = tester.eval(/*keepGoing=*/ false, errorKey, topKey);
assertThatEvaluationResult(result2).hasError();
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(errorKey)
.hasExceptionThat()
.isNotNull();
assertThatEvaluationResult(result2)
.hasErrorEntryForKeyThat(topKey)
.hasExceptionThat()
.isNotNull();
}
@Test
public void cachedChildErrorDepWithSiblingDepOnNoKeepGoingEval() throws Exception {
SkyKey parent1Key = skyKey("parent1");
SkyKey parent2Key = skyKey("parent2");
SkyKey errorKey = nonHermeticKey("error");
SkyKey otherKey = skyKey("other");
SkyFunction parentBuilder =
(skyKey, env) -> {
env.getValue(errorKey);
env.getValue(otherKey);
if (env.valuesMissing()) {
return null;
}
return new StringValue("parent");
};
tester.getOrCreate(parent1Key).setBuilder(parentBuilder);
tester.getOrCreate(parent2Key).setBuilder(parentBuilder);
tester.getOrCreate(errorKey).setConstantValue(new StringValue("no error yet"));
tester.getOrCreate(otherKey).setConstantValue(new StringValue("other"));
tester.eval(/*keepGoing=*/ true, parent1Key);
tester.eval(/*keepGoing=*/ false, parent2Key);
tester.getOrCreate(errorKey, /*markAsModified=*/ true).setHasError(true);
tester.invalidate();
tester.eval(/*keepGoing=*/ true, parent1Key);
tester.eval(/*keepGoing=*/ false, parent2Key);
}
private void injectGraphListenerForTesting(Listener listener, boolean deterministic) {
tester.evaluator.injectGraphTransformerForTesting(
DeterministicHelper.makeTransformer(listener, deterministic));
}
private void makeGraphDeterministic() {
tester.evaluator.injectGraphTransformerForTesting(DeterministicHelper.MAKE_DETERMINISTIC);
}
private static final class PassThroughSelected implements ValueComputer {
private final SkyKey key;
PassThroughSelected(SkyKey key) {
this.key = key;
}
@Override
public SkyValue compute(Map<SkyKey, SkyValue> deps, SkyFunction.Environment env) {
return Preconditions.checkNotNull(deps.get(key));
}
}
private void removedNodeComesBack() throws Exception {
SkyKey top = nonHermeticKey("top");
SkyKey mid = skyKey("mid");
SkyKey leaf = nonHermeticKey("leaf");
// When top depends on mid, which depends on leaf,
tester.getOrCreate(top).addDependency(mid).setComputedValue(CONCATENATE);
tester.getOrCreate(mid).addDependency(leaf).setComputedValue(CONCATENATE);
StringValue leafValue = new StringValue("leaf");
tester.set(leaf, leafValue);
// Then when top is evaluated, its value is as expected.
assertThat(tester.evalAndGet(/* keepGoing= */ true, top)).isEqualTo(leafValue);
// When top is changed to no longer depend on mid,
StringValue topValue = new StringValue("top");
tester
.getOrCreate(top, /* markAsModified= */ true)
.removeDependency(mid)
.setComputedValue(null)
.setConstantValue(topValue);
// And leaf is invalidated,
tester.getOrCreate(leaf, /*markAsModified=*/ true);
// Then when top is evaluated, its value is as expected,
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ true, top)).isEqualTo(topValue);
// And there is no value for mid in the graph,
assertThat(tester.evaluator.getExistingValue(mid)).isNull();
assertThat(tester.evaluator.getExistingErrorForTesting(mid)).isNull();
// Or for leaf.
assertThat(tester.evaluator.getExistingValue(leaf)).isNull();
assertThat(tester.evaluator.getExistingErrorForTesting(leaf)).isNull();
// When top is changed to depend directly on leaf,
tester
.getOrCreate(top, /*markAsModified=*/ true)
.addDependency(leaf)
.setConstantValue(null)
.setComputedValue(CONCATENATE);
// Then when top is evaluated, its value is as expected,
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ true, top)).isEqualTo(leafValue);
// and there is no value for mid in the graph,
assertThat(tester.evaluator.getExistingValue(mid)).isNull();
assertThat(tester.evaluator.getExistingErrorForTesting(mid)).isNull();
}
// Tests that a removed and then reinstated node doesn't try to invalidate its erstwhile parent
// when it is invalidated.
@Test
public void removedNodeComesBackAndInvalidates() throws Exception {
removedNodeComesBack();
// When leaf is invalidated again,
tester.getOrCreate(skyKey("leaf"), /* markAsModified= */ true);
// Then when top is evaluated, its value is as expected.
tester.invalidate();
assertThat(tester.evalAndGet(/* keepGoing= */ true, nonHermeticKey("top")))
.isEqualTo(new StringValue("leaf"));
}
// Tests that a removed and then reinstated node behaves properly when its parent disappears and
// then reappears.
@Test
public void removedNodeComesBackAndOtherInvalidates() throws Exception {
removedNodeComesBack();
SkyKey top = nonHermeticKey("top");
SkyKey mid = skyKey("mid");
SkyKey leaf = nonHermeticKey("leaf");
// When top is invalidated again,
tester.getOrCreate(top, /* markAsModified= */ true).removeDependency(leaf).addDependency(mid);
// Then when top is evaluated, its value is as expected.
tester.invalidate();
assertThat(tester.evalAndGet(/* keepGoing= */ true, top)).isEqualTo(new StringValue("leaf"));
}
// Tests that a removed and then reinstated node doesn't have a reverse dep on a former parent.
@Test
public void removedInvalidatedNodeComesBackAndOtherInvalidates() throws Exception {
SkyKey top = nonHermeticKey("top");
SkyKey leaf = nonHermeticKey("leaf");
// When top depends on leaf,
tester.getOrCreate(top).addDependency(leaf).setComputedValue(CONCATENATE);
StringValue leafValue = new StringValue("leaf");
tester.set(leaf, leafValue);
// Then when top is evaluated, its value is as expected.
assertThat(tester.evalAndGet(/* keepGoing= */ true, top)).isEqualTo(leafValue);
// When top is changed to no longer depend on leaf,
StringValue topValue = new StringValue("top");
tester
.getOrCreate(top, /* markAsModified= */ true)
.removeDependency(leaf)
.setComputedValue(null)
.setConstantValue(topValue);
// And leaf is invalidated,
tester.getOrCreate(leaf, /*markAsModified=*/ true);
// Then when top is evaluated, its value is as expected,
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ true, top)).isEqualTo(topValue);
// And there is no value for leaf in the graph.
assertThat(tester.evaluator.getExistingValue(leaf)).isNull();
assertThat(tester.evaluator.getExistingErrorForTesting(leaf)).isNull();
// When leaf is evaluated, so that it is present in the graph again,
assertThat(tester.evalAndGet(/*keepGoing=*/ true, leaf)).isEqualTo(leafValue);
// And top is changed to depend on leaf again,
tester
.getOrCreate(top, /*markAsModified=*/ true)
.addDependency(leaf)
.setConstantValue(null)
.setComputedValue(CONCATENATE);
// Then when top is evaluated, its value is as expected.
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ true, top)).isEqualTo(leafValue);
}
@Test
public void cleanReverseDepFromDirtyNodeNotInBuild() throws Exception {
SkyKey topKey = nonHermeticKey("top");
SkyKey inactiveKey = nonHermeticKey("inactive");
Thread mainThread = Thread.currentThread();
AtomicBoolean shouldInterrupt = new AtomicBoolean(false);
injectGraphListenerForTesting(
(key, type, order, context) -> {
if (shouldInterrupt.get()
&& key.equals(topKey)
&& type == EventType.IS_READY
&& order == Order.BEFORE) {
mainThread.interrupt();
shouldInterrupt.set(false);
try {
// Make sure threadpool propagates interrupt.
Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
},
/* deterministic= */ false);
// When top depends on inactive,
tester.getOrCreate(topKey).addDependency(inactiveKey).setComputedValue(COPY);
StringValue val = new StringValue("inactive");
// And inactive is constant,
tester.set(inactiveKey, val);
// Then top evaluates normally.
assertThat(tester.evalAndGet(/*keepGoing=*/ true, topKey)).isEqualTo(val);
// When evaluation will be interrupted as soon as top starts evaluating,
shouldInterrupt.set(true);
// And inactive is dirty,
tester.getOrCreate(inactiveKey, /*markAsModified=*/ true);
// And so is top,
tester.getOrCreate(topKey, /*markAsModified=*/ true);
tester.invalidate();
assertThrows(InterruptedException.class, () -> tester.eval(/*keepGoing=*/ false, topKey));
// But inactive is still present,
assertThat(tester.evaluator.getExistingEntryAtCurrentlyEvaluatingVersion(inactiveKey))
.isNotNull();
// And still dirty,
assertThat(tester.evaluator.getExistingEntryAtCurrentlyEvaluatingVersion(inactiveKey).isDirty())
.isTrue();
// And re-evaluates successfully,
assertThat(tester.evalAndGet(/*keepGoing=*/ true, inactiveKey)).isEqualTo(val);
// But top is gone from the graph,
assertThat(tester.evaluator.getExistingEntryAtCurrentlyEvaluatingVersion(topKey)).isNull();
// And we can successfully invalidate and re-evaluate inactive again.
tester.getOrCreate(inactiveKey, /*markAsModified=*/ true);
tester.invalidate();
assertThat(tester.evalAndGet(/*keepGoing=*/ true, inactiveKey)).isEqualTo(val);
}
@Test
public void errorChanged() throws Exception {
SkyKey error = nonHermeticKey("error");
tester.getOrCreate(error).setHasError(true);
assertThatErrorInfo(tester.evalAndGetError(/*keepGoing=*/ true, error))
.hasExceptionThat()
.isNotNull();
tester.getOrCreate(error, /*markAsModified=*/ true);
tester.invalidate();
assertThatErrorInfo(tester.evalAndGetError(/*keepGoing=*/ true, error))
.hasExceptionThat()
.isNotNull();
}
@Test
public void duplicateUnfinishedDeps_NoKeepGoing() throws Exception {
runTestDuplicateUnfinishedDeps(/*keepGoing=*/ false);
}
@Test
public void duplicateUnfinishedDeps_KeepGoing() throws Exception {
runTestDuplicateUnfinishedDeps(/*keepGoing=*/ true);
}
@Test
public void externalDep() throws Exception {
externalDep(1, 0);
externalDep(2, 0);
externalDep(1, 1);
externalDep(1, 2);
externalDep(2, 1);
externalDep(2, 2);
}
private void externalDep(int firstPassCount, int secondPassCount) throws Exception {
SkyKey parentKey = skyKey("parentKey");
CountDownLatch firstPassLatch = new CountDownLatch(1);
CountDownLatch secondPassLatch = new CountDownLatch(1);
tester
.getOrCreate(parentKey)
.setBuilder(
new SkyFunction() {
// Skyframe doesn't have native support for continuations, so we use fields here. A
// simple continuation API in Skyframe could be Environment providing a
// setContinuation(SkyContinuation) method, where SkyContinuation provides a compute
// method similar to SkyFunction. When restarting the node, Skyframe would then call
// the continuation rather than the original SkyFunction. If we do that, we should
// consider only allowing calls to dependOnFuture in combination with setContinuation.
private List<SettableFuture<SkyValue>> firstPass;
private List<SettableFuture<SkyValue>> secondPass;
@Override
public SkyValue compute(SkyKey skyKey, Environment env) {
if (firstPass == null) {
firstPass = new ArrayList<>();
for (int i = 0; i < firstPassCount; i++) {
SettableFuture<SkyValue> future = SettableFuture.create();
firstPass.add(future);
env.dependOnFuture(future);
}
assertThat(env.valuesMissing()).isTrue();
Thread helper =
new Thread(
() -> {
try {
firstPassLatch.await();
for (int i = 0; i < firstPassCount; i++) {
firstPass.get(i).set(new StringValue("value1"));
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
helper.start();
return null;
} else if (secondPass == null && secondPassCount > 0) {
for (int i = 0; i < firstPassCount; i++) {
assertThat(firstPass.get(i).isDone()).isTrue();
}
secondPass = new ArrayList<>();
for (int i = 0; i < secondPassCount; i++) {
SettableFuture<SkyValue> future = SettableFuture.create();
secondPass.add(future);
env.dependOnFuture(future);
}
assertThat(env.valuesMissing()).isTrue();
Thread helper =
new Thread(
() -> {
try {
secondPassLatch.await();
for (int i = 0; i < secondPassCount; i++) {
secondPass.get(i).set(new StringValue("value2"));
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
helper.start();
return null;
}
for (int i = 0; i < secondPassCount; i++) {
assertThat(secondPass.get(i).isDone()).isTrue();
}
return new StringValue("done!");
}
});
tester.evaluator.injectGraphTransformerForTesting(
NotifyingHelper.makeNotifyingTransformer(
new Listener() {
private boolean firstPassDone;
@Override
public void accept(SkyKey key, EventType type, Order order, Object context) {
// NodeEntry.addExternalDep is called as part of bookkeeping at the end of
// AbstractParallelEvaluator.Evaluate#run.
if (key == parentKey && type == EventType.ADD_EXTERNAL_DEP) {
if (!firstPassDone) {
firstPassLatch.countDown();
firstPassDone = true;
} else {
secondPassLatch.countDown();
}
}
}
}));
EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.hasError()).isFalse();
assertThat(result.get(parentKey)).isEqualTo(new StringValue("done!"));
}
private void runTestDuplicateUnfinishedDeps(boolean keepGoing) throws Exception {
SkyKey parentKey = skyKey("parent");
SkyKey childKey = skyKey("child");
SkyValue childValue = new StringValue("child");
tester
.getOrCreate(childKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env) {
if (keepGoing) {
return childValue;
} else {
throw new IllegalStateException("shouldn't get here");
}
}
});
SomeErrorException parentExn = new SomeErrorException("bad");
AtomicInteger numParentComputeCalls = new AtomicInteger(0);
tester
.getOrCreate(parentKey)
.setBuilder(
new SkyFunction() {
@Nullable
@Override
public SkyValue compute(SkyKey skyKey, Environment env)
throws SkyFunctionException, InterruptedException {
numParentComputeCalls.incrementAndGet();
if (!keepGoing || numParentComputeCalls.get() == 1) {
Preconditions.checkState(env.getValue(childKey) == null);
Preconditions.checkState(env.getValue(childKey) == null);
} else {
Preconditions.checkState(env.getValue(childKey).equals(childValue));
Preconditions.checkState(env.getValue(childKey).equals(childValue));
}
throw new GenericFunctionException(parentExn, Transience.PERSISTENT);
}
});
Exception exception = tester.evalAndGetError(keepGoing, parentKey).getException();
assertThat(exception).isInstanceOf(SomeErrorException.class);
assertThat(exception).hasMessageThat().isEqualTo("bad");
}
/** Data encapsulating a graph inconsistency found during evaluation. */
@AutoValue
abstract static class InconsistencyData {
abstract SkyKey key();
abstract ImmutableSet<SkyKey> otherKeys();
abstract Inconsistency inconsistency();
static InconsistencyData resetRequested(SkyKey key) {
return create(key, /* otherKeys= */ null, Inconsistency.RESET_REQUESTED);
}
static InconsistencyData rewind(SkyKey parent, ImmutableSet<SkyKey> children) {
return create(parent, children, Inconsistency.PARENT_FORCE_REBUILD_OF_CHILD);
}
static InconsistencyData create(
SkyKey key, @Nullable Collection<SkyKey> otherKeys, Inconsistency inconsistency) {
return new AutoValue_MemoizingEvaluatorTest_InconsistencyData(
key,
otherKeys == null ? ImmutableSet.of() : ImmutableSet.copyOf(otherKeys),
inconsistency);
}
}
/** A graph tester that is specific to the memoizing evaluator, with some convenience methods. */
protected final class MemoizingEvaluatorTester extends GraphTester {
private RecordingDifferencer differencer;
private MemoizingEvaluator evaluator;
private TrackingProgressReceiver progressReceiver =
createTrackingProgressReceiver(/*checkEvaluationResults=*/ true);
private GraphInconsistencyReceiver graphInconsistencyReceiver =
GraphInconsistencyReceiver.THROWING;
private EventFilter eventFilter = EventFilter.FULL_STORAGE;
/** Constructs a new {@link #evaluator}, so call before injecting a transformer into it! */
public void initialize() {
this.differencer = getRecordingDifferencer();
this.evaluator =
getMemoizingEvaluator(
getSkyFunctionMap(),
differencer,
progressReceiver,
graphInconsistencyReceiver,
eventFilter);
}
/**
* Sets the {@link #progressReceiver}. {@link #initialize} must be called after this to have any
* effect.
*/
public void setProgressReceiver(TrackingProgressReceiver progressReceiver) {
this.progressReceiver = progressReceiver;
}
/**
* Sets the {@link #eventFilter}. {@link #initialize} must be called after this to have any
* effect.
*/
public void setEventFilter(EventFilter eventFilter) {
this.eventFilter = eventFilter;
}
/**
* Sets the {@link #graphInconsistencyReceiver}. {@link #initialize} must be called after this
* to have any effect.
*/
public void setGraphInconsistencyReceiver(
GraphInconsistencyReceiver graphInconsistencyReceiver) {
this.graphInconsistencyReceiver = graphInconsistencyReceiver;
}
public MemoizingEvaluator getEvaluator() {
return evaluator;
}
public void invalidate() throws InterruptedException {
evaluator.noteEvaluationsAtSameVersionMayBeFinished(reporter);
differencer.invalidate(getModifiedValues());
clearModifiedValues();
progressReceiver.clear();
}
public void invalidateTransientErrors() {
differencer.invalidateTransientErrors();
}
public void delete(String key) {
evaluator.delete(Predicates.equalTo(skyKey(key)));
}
public void resetPlayedEvents() {
emittedEventState.clear();
}
public Set<SkyKey> getDirtyKeys() {
return progressReceiver.dirty;
}
public Set<SkyKey> getDeletedKeys() {
return progressReceiver.deleted;
}
public Set<SkyKey> getEnqueuedValues() {
return progressReceiver.enqueued;
}
public <T extends SkyValue> EvaluationResult<T> eval(
boolean keepGoing, int numThreads, SkyKey... keys) throws InterruptedException {
assertThat(getModifiedValues()).isEmpty();
EvaluationContext evaluationContext =
EvaluationContext.newBuilder()
.setKeepGoing(keepGoing)
.setParallelism(numThreads)
.setEventHandler(reporter)
.build();
BugReport.maybePropagateLastCrashIfInTest();
EvaluationResult<T> result = null;
beforeEvaluation();
try {
result = evaluator.evaluate(ImmutableList.copyOf(keys), evaluationContext);
return result;
} finally {
afterEvaluation(result, evaluationContext);
}
}
public <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, SkyKey... keys)
throws InterruptedException {
return eval(keepGoing, 100, keys);
}
public <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, String... keys)
throws InterruptedException {
return eval(keepGoing, toSkyKeys(keys).toArray(new SkyKey[0]));
}
public SkyValue evalAndGet(boolean keepGoing, String key) throws InterruptedException {
return evalAndGet(keepGoing, skyKey(key));
}
public SkyValue evalAndGet(String key) throws InterruptedException {
return evalAndGet(/*keepGoing=*/ false, key);
}
public SkyValue evalAndGet(boolean keepGoing, SkyKey key) throws InterruptedException {
EvaluationResult<StringValue> evaluationResult = eval(keepGoing, key);
SkyValue result = evaluationResult.get(key);
assertWithMessage(evaluationResult.toString()).that(result).isNotNull();
return result;
}
public ErrorInfo evalAndGetError(boolean keepGoing, SkyKey key) throws InterruptedException {
EvaluationResult<StringValue> evaluationResult = eval(keepGoing, key);
assertThatEvaluationResult(evaluationResult).hasErrorEntryForKeyThat(key);
return evaluationResult.getError(key);
}
public ErrorInfo evalAndGetError(boolean keepGoing, String key) throws InterruptedException {
return evalAndGetError(keepGoing, skyKey(key));
}
@Nullable
public SkyValue getExistingValue(SkyKey key) throws InterruptedException {
return evaluator.getExistingValue(key);
}
@Nullable
public SkyValue getExistingValue(String key) throws InterruptedException {
return getExistingValue(skyKey(key));
}
}
}