aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar nharmata <nharmata@google.com>2018-07-23 13:54:19 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-07-23 13:55:21 -0700
commit6571091fcab5b877c0348ab692a7ee606c38ff22 (patch)
tree9838cb86b5a2cbe067b6f0182bc1085e7e2ad23b
parent3da60d2d4f996b88980058f8d039da790ed7675f (diff)
Fix crash bug in AbstractExceptionalParallelEvaluator#doMutatingEvaluation in a very specific window of time inbetween enqueueing one top-level node for evaluation and checking if another top-level node is done. See the added unit test for details.
RELNOTES: None PiperOrigin-RevId: 205718683
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/AbstractExceptionalParallelEvaluator.java13
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/InMemoryGraphImpl.java43
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/BUILD1
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/ParallelEvaluatorTest.java93
4 files changed, 129 insertions, 21 deletions
diff --git a/src/main/java/com/google/devtools/build/skyframe/AbstractExceptionalParallelEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/AbstractExceptionalParallelEvaluator.java
index af658f32d5..315aa8458c 100644
--- a/src/main/java/com/google/devtools/build/skyframe/AbstractExceptionalParallelEvaluator.java
+++ b/src/main/java/com/google/devtools/build/skyframe/AbstractExceptionalParallelEvaluator.java
@@ -260,17 +260,24 @@ public abstract class AbstractExceptionalParallelEvaluator<E extends Exception>
throw new IllegalStateException(entry + " for " + skyKey + " in unknown state");
}
}
- } catch (InterruptedException e) {
+ } catch (InterruptedException ie) {
// When multiple keys are being evaluated, it's possible that a key may get queued before
// an InterruptedException is thrown from either #addReverseDepAndCheckIfDone or
// #informProgressReceiverThatValueIsDone on a different key. Therefore we have to make sure
// all evaluation threads are properly interrupted and shut down, if main thread (current
// thread) is interrupted.
Thread.currentThread().interrupt();
- evaluatorContext.getVisitor().waitForCompletion();
+ try {
+ evaluatorContext.getVisitor().waitForCompletion();
+ } catch (SchedulerException se) {
+ // A SchedulerException due to a SkyFunction observing the interrupt is completely expected.
+ if (!(se.getCause() instanceof InterruptedException)) {
+ throw se;
+ }
+ }
// Rethrow the InterruptedException to avoid proceeding to construct the result.
- throw e;
+ throw ie;
}
return waitForCompletionAndConstructResult(skyKeys);
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryGraphImpl.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraphImpl.java
index 079ddba175..12a2ba3fd4 100644
--- a/src/main/java/com/google/devtools/build/skyframe/InMemoryGraphImpl.java
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryGraphImpl.java
@@ -14,7 +14,6 @@
package com.google.devtools.build.skyframe;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -35,7 +34,7 @@ import javax.annotation.Nullable;
*/
public class InMemoryGraphImpl implements InMemoryGraph {
- protected final ConcurrentMap<SkyKey, InMemoryNodeEntry> nodeMap = new ConcurrentHashMap<>(1024);
+ protected final ConcurrentMap<SkyKey, NodeEntry> nodeMap = new ConcurrentHashMap<>(1024);
private final boolean keepEdges;
InMemoryGraphImpl() {
@@ -52,7 +51,7 @@ public class InMemoryGraphImpl implements InMemoryGraph {
}
@Override
- public InMemoryNodeEntry get(@Nullable SkyKey requestor, Reason reason, SkyKey skyKey) {
+ public NodeEntry get(@Nullable SkyKey requestor, Reason reason, SkyKey skyKey) {
return nodeMap.get(skyKey);
}
@@ -63,7 +62,7 @@ public class InMemoryGraphImpl implements InMemoryGraph {
// and ImmutableMap.Builder does not tolerate duplicates. The map will be thrown away shortly.
HashMap<SkyKey, NodeEntry> result = new HashMap<>();
for (SkyKey key : keys) {
- InMemoryNodeEntry entry = get(null, Reason.OTHER, key);
+ NodeEntry entry = get(null, Reason.OTHER, key);
if (entry != null) {
result.put(key, entry);
}
@@ -71,17 +70,20 @@ public class InMemoryGraphImpl implements InMemoryGraph {
return result;
}
- protected InMemoryNodeEntry createIfAbsent(SkyKey key) {
- InMemoryNodeEntry newval =
- keepEdges ? new InMemoryNodeEntry() : new EdgelessInMemoryNodeEntry();
- InMemoryNodeEntry oldval = nodeMap.putIfAbsent(key, newval);
+ protected NodeEntry newNodeEntry(SkyKey key) {
+ return keepEdges ? new InMemoryNodeEntry() : new EdgelessInMemoryNodeEntry();
+ }
+
+ protected NodeEntry createIfAbsent(SkyKey key) {
+ NodeEntry newval = newNodeEntry(key);
+ NodeEntry oldval = nodeMap.putIfAbsent(key, newval);
return oldval == null ? newval : oldval;
}
@Override
- public Map<SkyKey, InMemoryNodeEntry> createIfAbsentBatch(
+ public Map<SkyKey, NodeEntry> createIfAbsentBatch(
@Nullable SkyKey requestor, Reason reason, Iterable<SkyKey> keys) {
- ImmutableMap.Builder<SkyKey, InMemoryNodeEntry> builder = ImmutableMap.builder();
+ ImmutableMap.Builder<SkyKey, NodeEntry> builder = ImmutableMap.builder();
for (SkyKey key : keys) {
builder.put(key, createIfAbsent(key));
}
@@ -98,10 +100,11 @@ public class InMemoryGraphImpl implements InMemoryGraph {
return Collections.unmodifiableMap(
Maps.transformValues(
nodeMap,
- new Function<InMemoryNodeEntry, SkyValue>() {
- @Override
- public SkyValue apply(InMemoryNodeEntry entry) {
+ entry -> {
+ try {
return entry.toValue();
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
}
}));
}
@@ -112,17 +115,21 @@ public class InMemoryGraphImpl implements InMemoryGraph {
Maps.filterValues(
Maps.transformValues(
nodeMap,
- new Function<InMemoryNodeEntry, SkyValue>() {
- @Override
- public SkyValue apply(InMemoryNodeEntry entry) {
- return entry.isDone() ? entry.getValue() : null;
+ entry -> {
+ if (!entry.isDone()) {
+ return null;
+ }
+ try {
+ return entry.getValue();
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
}
}),
Predicates.notNull()));
}
@Override
- public Map<SkyKey, InMemoryNodeEntry> getAllValues() {
+ public Map<SkyKey, NodeEntry> getAllValues() {
return Collections.unmodifiableMap(nodeMap);
}
diff --git a/src/test/java/com/google/devtools/build/skyframe/BUILD b/src/test/java/com/google/devtools/build/skyframe/BUILD
index b3808d29c5..d43082d6db 100644
--- a/src/test/java/com/google/devtools/build/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/skyframe/BUILD
@@ -63,6 +63,7 @@ java_test(
"//third_party:guava-testlib",
"//third_party:jsr305",
"//third_party:junit4",
+ "//third_party:mockito",
"//third_party:truth",
],
)
diff --git a/src/test/java/com/google/devtools/build/skyframe/ParallelEvaluatorTest.java b/src/test/java/com/google/devtools/build/skyframe/ParallelEvaluatorTest.java
index 51687de2ea..fad52068cd 100644
--- a/src/test/java/com/google/devtools/build/skyframe/ParallelEvaluatorTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/ParallelEvaluatorTest.java
@@ -61,6 +61,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
/**
* Tests for {@link ParallelEvaluator}.
@@ -229,6 +230,98 @@ public class ParallelEvaluatorTest {
});
}
+ @Test
+ public void interruptedEvaluatorThreadAfterEnqueueBeforeWaitForCompletionAndConstructResult()
+ throws InterruptedException {
+ // This is a regression test for a crash bug in
+ // AbstractExceptionalParallelEvaluator#doMutatingEvaluation in a very specific window of time
+ // inbetween enqueueing one top-level node for evaluation and checking if another top-level node
+ // is done.
+
+ // When we have two top-level nodes, A and B,
+ SkyKey keyA = GraphTester.toSkyKey("a");
+ SkyKey keyB = GraphTester.toSkyKey("b");
+
+ // And rig the graph and node entries, such that B's addReverseDepAndCheckIfDone waits for A to
+ // start computing and then tries to observe an interrupt (which will happen on the calling
+ // thread, aka the main Skyframe evaluation thread),
+ CountDownLatch keyAStartedComputingLatch = new CountDownLatch(1);
+ CountDownLatch keyBAddReverseDepAndCheckIfDoneLatch = new CountDownLatch(1);
+ NodeEntry nodeEntryB = Mockito.mock(NodeEntry.class);
+ AtomicBoolean keyBAddReverseDepAndCheckIfDoneInterrupted = new AtomicBoolean(false);
+ Mockito.doAnswer(
+ invocation -> {
+ keyAStartedComputingLatch.await();
+ keyBAddReverseDepAndCheckIfDoneLatch.countDown();
+ try {
+ Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
+ throw new IllegalStateException("shouldn't get here");
+ } catch (InterruptedException e) {
+ keyBAddReverseDepAndCheckIfDoneInterrupted.set(true);
+ throw e;
+ }
+ })
+ .when(nodeEntryB)
+ .addReverseDepAndCheckIfDone(Mockito.eq(null));
+ graph = new InMemoryGraphImpl() {
+ @Override
+ protected NodeEntry newNodeEntry(SkyKey key) {
+ return key.equals(keyB) ? nodeEntryB : super.newNodeEntry(key);
+ }
+ };
+ // And A's SkyFunction tries to observe an interrupt after it starts computing,
+ AtomicBoolean keyAComputeInterrupted = new AtomicBoolean(false);
+ tester.getOrCreate(keyA).setBuilder(new SkyFunction() {
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
+ keyAStartedComputingLatch.countDown();
+ try {
+ Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
+ throw new IllegalStateException("shouldn't get here");
+ } catch (InterruptedException e) {
+ keyAComputeInterrupted.set(true);
+ throw e;
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+ });
+
+ // And we have a dedicated thread that kicks off the evaluation of A and B together (in that
+ // order).
+ TestThread evalThread = new TestThread() {
+ @Override
+ public void runTest() throws Exception {
+ try {
+ eval(/*keepGoing=*/true, keyA, keyB);
+ fail();
+ } catch (InterruptedException e) {
+ // Expected.
+ }
+ }
+ };
+
+ // Then when we start that thread,
+ evalThread.start();
+ // We (the thread running the test) are able to observe that B's addReverseDepAndCheckIfDone has
+ // just been called (implying that A has started to be computed).
+ assertThat(
+ keyBAddReverseDepAndCheckIfDoneLatch.await(
+ TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+ .isTrue();
+ // Then when we interrupt the evaluation thread,
+ evalThread.interrupt();
+ // The evaluation thread eventually terminates.
+ evalThread.joinAndAssertState(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
+ // And we are able to verify both that A's SkyFunction had observed an interrupt,
+ assertThat(keyAComputeInterrupted.get()).isTrue();
+ // And also that B's addReverseDepAndCheckIfDoneInterrupted had observed an interrupt.
+ assertThat(keyBAddReverseDepAndCheckIfDoneInterrupted.get()).isTrue();
+ }
+
private void runPartialResultOnInterruption(boolean buildFastFirst) throws Exception {
graph = new InMemoryGraphImpl();
// Two runs for fastKey's builder and one for the start of waitKey's builder.