use of com.google.devtools.build.skyframe.NotifyingHelper.EventType in project bazel by bazelbuild.
the class MemoizingEvaluatorTest method cachedErrorCausesRestart.
/**
* 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(new TrackingProgressReceiver() {
@Override
public void evaluated(SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) {
evaluated.add(skyKey);
}
});
final SkyKey errorKey = GraphTester.toSkyKey("error");
SkyKey invalidatedKey = GraphTester.toSkyKey("invalidated");
final SkyKey topKey = GraphTester.toSkyKey("top");
tester.getOrCreate(errorKey).addDependency(invalidatedKey).setHasError(true);
tester.getOrCreate(invalidatedKey).setConstantValue(new StringValue("constant"));
final CountDownLatch topSecondEval = new CountDownLatch(2);
final CountDownLatch topRequestedError = new CountDownLatch(1);
final CountDownLatch errorMarkedClean = new CountDownLatch(1);
injectGraphListenerForTesting(new Listener() {
@Override
public void accept(SkyKey key, EventType type, Order order, Object 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 SkyFunctionException, InterruptedException {
topSecondEval.countDown();
env.getValue(errorKey);
topRequestedError.countDown();
assertThat(env.valuesMissing()).isTrue();
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(errorMarkedClean, "error not marked clean");
return null;
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
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();
assertThatEvaluationResult(result2).hasErrorEntryForKeyThat(topKey).rootCauseOfExceptionIs(errorKey);
}
use of com.google.devtools.build.skyframe.NotifyingHelper.EventType in project bazel by bazelbuild.
the class MemoizingEvaluatorTest method raceConditionWithNoKeepGoingErrors_InflightError.
@Test
public void raceConditionWithNoKeepGoingErrors_InflightError() throws Exception {
// Given a graph of two nodes, errorKey and otherErrorKey,
final SkyKey errorKey = GraphTester.toSkyKey("errorKey");
final SkyKey otherErrorKey = GraphTester.toSkyKey("otherErrorKey");
final CountDownLatch errorCommitted = new CountDownLatch(1);
final CountDownLatch otherStarted = new CountDownLatch(1);
final CountDownLatch otherDone = new CountDownLatch(1);
final AtomicInteger numOtherInvocations = new AtomicInteger(0);
final AtomicReference<String> bogusInvocationMessage = new AtomicReference<>(null);
final AtomicReference<String> nonNullValueMessage = new AtomicReference<>(null);
tester.getOrCreate(errorKey).setBuilder(new SkyFunction() {
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
// 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);
}
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
});
tester.getOrCreate(otherErrorKey).setBuilder(new SkyFunction() {
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, InterruptedException {
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;
}
}
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
});
injectGraphListenerForTesting(new Listener() {
@Override
public void accept(SkyKey key, EventType type, Order order, Object 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 because of errorKey,
assertTrue(result.hasError());
assertEquals(errorKey, result.getError().getRootCauseOfException());
// And no value is committed for otherErrorKey,
assertNull(tester.driver.getExistingErrorForTesting(otherErrorKey));
assertNull(tester.driver.getExistingValueForTesting(otherErrorKey));
// And no value was committed for errorKey,
assertNull(nonNullValueMessage.get(), nonNullValueMessage.get());
// And the SkyFunction for otherErrorKey was evaluated exactly once.
assertEquals(numOtherInvocations.get(), 1);
assertNull(bogusInvocationMessage.get(), bogusInvocationMessage.get());
// NB: The SkyFunction for otherErrorKey gets evaluated exactly once--it does not get
// re-evaluated during error bubbling. Why? When otherErrorKey throws, it is always the
// second error encountered, because it waited for errorKey's error to be committed before
// trying to get it. In fail-fast evaluations only the first failing SkyFunction's
// newly-discovered-dependencies are registered. Therefore, there won't be a reverse-dep from
// errorKey to otherErrorKey for the error to bubble through.
}
use of com.google.devtools.build.skyframe.NotifyingHelper.EventType in project bazel by bazelbuild.
the class MemoizingEvaluatorTest method dirtyAndChangedValueIsChanged.
/**
* 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 {
final SkyKey parent = GraphTester.toSkyKey("parent");
final AtomicBoolean blockingEnabled = new AtomicBoolean(false);
final 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.
final CountDownLatch threadsStarted = new CountDownLatch(3);
injectGraphListenerForTesting(new Listener() {
@Override
public void accept(SkyKey key, EventType type, Order order, Object 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");
boolean isChanged = (Boolean) context;
if (order == Order.BEFORE && !isChanged) {
TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(waitForChanged, "'changed' thread did not mark value changed in time");
return;
}
if (order == Order.AFTER && isChanged) {
waitForChanged.countDown();
}
}
}
}, /*deterministic=*/
false);
SkyKey leaf = GraphTester.toSkyKey("leaf");
tester.set(leaf, new StringValue("leaf"));
tester.getOrCreate(parent).addDependency(leaf).setComputedValue(CONCATENATE);
EvaluationResult<StringValue> result;
result = tester.eval(/*keepGoing=*/
false, parent);
assertEquals("leaf", result.get(parent).getValue());
// 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 = GraphTester.toSkyKey("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);
assertEquals("leafother2", result.get(parent).getValue());
assertEquals(0, waitForChanged.getCount());
assertEquals(0, threadsStarted.getCount());
}
use of com.google.devtools.build.skyframe.NotifyingHelper.EventType in project bazel by bazelbuild.
the class MemoizingEvaluatorTest method raceClearingIncompleteValues.
/**
* 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 = GraphTester.toSkyKey("aatop");
final SkyKey midKey = GraphTester.toSkyKey("zzmid");
SkyKey badKey = GraphTester.toSkyKey("bad");
final AtomicBoolean waitForSecondCall = new AtomicBoolean(false);
final CountDownLatch otherThreadWinning = new CountDownLatch(1);
final AtomicReference<Thread> firstThread = new AtomicReference<>();
injectGraphListenerForTesting(new Listener() {
@Override
public void accept(SkyKey key, EventType type, Order order, Object 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);
assertThat(result.getError(midKey).getRootCauses()).containsExactly(badKey);
waitForSecondCall.set(true);
result = tester.eval(/*keepGoing=*/
true, topKey, midKey);
assertNotNull(firstThread.get());
assertEquals(0, otherThreadWinning.getCount());
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey).isNotNull();
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey).isNotNull();
if (rootCausesStored()) {
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(midKey).rootCauseOfExceptionIs(badKey);
assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey).rootCauseOfExceptionIs(badKey);
}
}
use of com.google.devtools.build.skyframe.NotifyingHelper.EventType in project bazel by bazelbuild.
the class MemoizingEvaluatorTest method shutDownBuildOnCachedError_Verified.
/**
* 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(new TrackingProgressReceiver() {
@Override
public void evaluated(SkyKey skyKey, Supplier<SkyValue> skyValueSupplier, EvaluationState state) {
evaluated.add(skyKey);
}
});
// 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 = GraphTester.toSkyKey("error");
SkyKey invalidatedKey = GraphTester.toSkyKey("invalidated-leaf");
SkyKey changedKey = GraphTester.toSkyKey("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.
final SkyKey cachedParentKey = GraphTester.toSkyKey("A-cached-parent");
final SkyKey uncachedParentKey = GraphTester.toSkyKey("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.
final AtomicBoolean synchronizeThreads = new AtomicBoolean(false);
final 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)) {
// we wait until the cached parent is signaled too.
try {
assertTrue(cachedSignaled.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS));
} 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 = GraphTester.skyKey("wait-for-shutdown");
tester.getOrCreate(waitForShutdownKey).setBuilder(new SkyFunction() {
@Override
public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
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();
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
});
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();
}
Aggregations