| // 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.collect.Iterables.getOnlyElement; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.devtools.build.lib.testutil.EventIterableSubjectFactory.assertThatEvents; |
| import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult; |
| import static com.google.devtools.build.skyframe.GraphTester.CONCATENATE; |
| import static org.junit.Assert.assertThrows; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Interner; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| import com.google.common.eventbus.EventBus; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.ListeningExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.common.util.concurrent.SettableFuture; |
| import com.google.common.util.concurrent.Uninterruptibles; |
| import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor; |
| import com.google.devtools.build.lib.concurrent.BlazeInterners; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.events.EventKind; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; |
| import com.google.devtools.build.lib.events.Reporter; |
| import com.google.devtools.build.lib.events.StoredEventHandler; |
| import com.google.devtools.build.lib.testutil.TestThread; |
| import com.google.devtools.build.lib.testutil.TestUtils; |
| import com.google.devtools.build.skyframe.GraphTester.StringValue; |
| 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.SkyFunctionException.Transience; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.function.Supplier; |
| import javax.annotation.Nullable; |
| import org.junit.After; |
| 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}. |
| */ |
| @RunWith(JUnit4.class) |
| public class ParallelEvaluatorTest { |
| private static final SkyFunctionName CHILD_TYPE = SkyFunctionName.createHermetic("child"); |
| private static final SkyFunctionName PARENT_TYPE = SkyFunctionName.createHermetic("parent"); |
| |
| protected ProcessableGraph graph; |
| protected IntVersion graphVersion = IntVersion.of(0); |
| protected GraphTester tester = new GraphTester(); |
| |
| private StoredEventHandler storedEventHandler; |
| |
| private DirtyTrackingProgressReceiver revalidationReceiver = |
| new DirtyTrackingProgressReceiver(null); |
| |
| @Before |
| public void initializeReporter() { |
| storedEventHandler = new StoredEventHandler(); |
| } |
| |
| @After |
| public void assertNoTrackedErrors() { |
| TrackingAwaiter.INSTANCE.assertNoErrors(); |
| } |
| |
| private ParallelEvaluator makeEvaluator( |
| ProcessableGraph graph, |
| ImmutableMap<SkyFunctionName, ? extends SkyFunction> builders, |
| boolean keepGoing, |
| EventFilter storedEventFilter) { |
| Version oldGraphVersion = graphVersion; |
| graphVersion = graphVersion.next(); |
| return new ParallelEvaluator( |
| graph, |
| oldGraphVersion, |
| builders, |
| storedEventHandler, |
| new MemoizingEvaluator.EmittedEventState(), |
| storedEventFilter, |
| ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, |
| keepGoing, |
| revalidationReceiver, |
| GraphInconsistencyReceiver.THROWING, |
| () -> AbstractQueueVisitor.createExecutorService(200, "test-pool"), |
| new SimpleCycleDetector(), |
| EvaluationVersionBehavior.MAX_CHILD_VERSIONS); |
| } |
| |
| private ParallelEvaluator makeEvaluator(ProcessableGraph graph, |
| ImmutableMap<SkyFunctionName, ? extends SkyFunction> builders, boolean keepGoing) { |
| return makeEvaluator(graph, builders, keepGoing, |
| InMemoryMemoizingEvaluator.DEFAULT_STORED_EVENT_FILTER); |
| } |
| |
| /** Convenience method for eval-ing a single value. */ |
| protected SkyValue eval(boolean keepGoing, SkyKey key) throws InterruptedException { |
| return eval(keepGoing, ImmutableList.of(key)).get(key); |
| } |
| |
| protected <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, SkyKey... keys) |
| throws InterruptedException { |
| return eval(keepGoing, ImmutableList.copyOf(keys)); |
| } |
| |
| protected <T extends SkyValue> EvaluationResult<T> eval(boolean keepGoing, Iterable<SkyKey> keys) |
| throws InterruptedException { |
| ParallelEvaluator evaluator = makeEvaluator(graph, tester.getSkyFunctionMap(), keepGoing); |
| return evaluator.eval(keys); |
| } |
| |
| protected ErrorInfo evalValueInError(SkyKey key) throws InterruptedException { |
| return eval(true, ImmutableList.of(key)).getError(key); |
| } |
| |
| protected GraphTester.TestFunction set(String name, String value) { |
| return tester.set(name, new StringValue(value)); |
| } |
| |
| @Test |
| public void smoke() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| set("b", "b"); |
| tester.getOrCreate("ab").addDependency("a").addDependency("b").setComputedValue(CONCATENATE); |
| StringValue value = (StringValue) eval(false, GraphTester.toSkyKey("ab")); |
| assertThat(value.getValue()).isEqualTo("ab"); |
| assertThat(storedEventHandler.getEvents()).isEmpty(); |
| assertThat(storedEventHandler.getPosts()).isEmpty(); |
| } |
| |
| @Test |
| public void enqueueDoneFuture() throws Exception { |
| final SkyKey parentKey = GraphTester.toSkyKey("parentKey"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| SettableFuture<SkyValue> future = SettableFuture.create(); |
| future.set(new StringValue("good")); |
| env.dependOnFuture(future); |
| assertThat(env.valuesMissing()).isFalse(); |
| try { |
| return future.get(); |
| } catch (ExecutionException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| graph = new InMemoryGraphImpl(); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/ false, ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isFalse(); |
| assertThat(result.get(parentKey)).isEqualTo(new StringValue("good")); |
| } |
| |
| @Test |
| public void enqueueBadFuture() throws Exception { |
| final SkyKey parentKey = GraphTester.toSkyKey("parentKey"); |
| final CountDownLatch doneLatch = new CountDownLatch(1); |
| final ListeningExecutorService executor = |
| MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| private ListenableFuture<SkyValue> future; |
| |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| if (future == null) { |
| future = |
| executor.submit( |
| () -> { |
| doneLatch.await(); |
| throw new UnsupportedOperationException(); |
| }); |
| env.dependOnFuture(future); |
| assertThat(env.valuesMissing()).isTrue(); |
| return null; |
| } |
| assertThat(future.isDone()).isTrue(); |
| ExecutionException expected = |
| assertThrows(ExecutionException.class, () -> future.get()); |
| assertThat(expected.getCause()).isInstanceOf(UnsupportedOperationException.class); |
| return new StringValue("Caught!"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| graph = |
| NotifyingHelper.makeNotifyingTransformer( |
| new Listener() { |
| @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) { |
| doneLatch.countDown(); |
| } |
| } |
| }) |
| .transform(new InMemoryGraphImpl()); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/ false, ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isFalse(); |
| assertThat(result.get(parentKey)).isEqualTo(new StringValue("Caught!")); |
| } |
| |
| @Test |
| public void dependsOnKeyAndFuture() throws Exception { |
| final SkyKey parentKey = GraphTester.toSkyKey("parentKey"); |
| final SkyKey childKey = GraphTester.toSkyKey("childKey"); |
| final CountDownLatch doneLatch = new CountDownLatch(1); |
| tester.getOrCreate(childKey).setConstantValue(new StringValue("child")); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| private SettableFuture<SkyValue> future; |
| |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| SkyValue child = env.getValue(childKey); |
| if (future == null) { |
| assertThat(child).isNull(); |
| future = SettableFuture.create(); |
| env.dependOnFuture(future); |
| assertThat(env.valuesMissing()).isTrue(); |
| new Thread( |
| () -> { |
| try { |
| doneLatch.await(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| future.set(new StringValue("future")); |
| }) |
| .start(); |
| return null; |
| } |
| assertThat(child).isEqualTo(new StringValue("child")); |
| assertThat(future.isDone()).isTrue(); |
| try { |
| assertThat(future.get()).isEqualTo(new StringValue("future")); |
| } catch (ExecutionException e) { |
| throw new RuntimeException(e); |
| } |
| return new StringValue("All done!"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| graph = |
| NotifyingHelper.makeNotifyingTransformer( |
| new Listener() { |
| @Override |
| public void accept(SkyKey key, EventType type, Order order, Object context) { |
| if (key == childKey && type == EventType.SET_VALUE) { |
| doneLatch.countDown(); |
| } |
| } |
| }) |
| .transform(new InMemoryGraphImpl()); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/ false, ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isFalse(); |
| assertThat(result.get(parentKey)).isEqualTo(new StringValue("All done!")); |
| } |
| |
| /** |
| * Test interruption handling when a long-running SkyFunction gets interrupted. |
| */ |
| @Test |
| public void interruptedFunction() throws Exception { |
| runInterruptionTest(new SkyFunctionFactory() { |
| @Override |
| public SkyFunction create(final Semaphore threadStarted, final String[] errorMessage) { |
| return new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey key, Environment env) throws InterruptedException { |
| // Signal the waiting test thread that the evaluator thread has really started. |
| threadStarted.release(); |
| |
| // Simulate a SkyFunction that runs for 10 seconds (this number was chosen arbitrarily). |
| // The main thread should interrupt it shortly after it got started. |
| Thread.sleep(10 * 1000); |
| |
| // Set an error message to indicate that the expected interruption didn't happen. |
| // We can't use Assert.fail(String) on an async thread. |
| errorMessage[0] = "SkyFunction should have been interrupted"; |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }; |
| } |
| }); |
| } |
| |
| /** |
| * Test interruption handling when the Evaluator is in-between running SkyFunctions. |
| * |
| * <p>This is the point in time after a SkyFunction requested a dependency which is not yet built |
| * so the builder returned null to the Evaluator, and the latter is about to schedule evaluation |
| * of the missing dependency but gets interrupted before the dependency's SkyFunction could start. |
| */ |
| @Test |
| public void interruptedEvaluatorThread() throws Exception { |
| runInterruptionTest( |
| new SkyFunctionFactory() { |
| @Override |
| public SkyFunction create(final Semaphore threadStarted, final String[] errorMessage) { |
| return new SkyFunction() { |
| // No need to synchronize access to this field; we always request just one more |
| // dependency, so it's only one SkyFunction running at any time. |
| private int valueIdCounter = 0; |
| |
| @Override |
| public SkyValue compute(SkyKey key, Environment env) throws InterruptedException { |
| // Signal the waiting test thread that the Evaluator thread has really started. |
| threadStarted.release(); |
| |
| // Keep the evaluator busy until the test's thread gets scheduled and can |
| // interrupt the Evaluator's thread. |
| env.getValue(GraphTester.toSkyKey("a" + valueIdCounter++)); |
| |
| // This method never throws InterruptedException, therefore it's the responsibility |
| // of the Evaluator to detect the interrupt and avoid calling subsequent |
| // SkyFunctions. |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }; |
| } |
| }); |
| } |
| |
| @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 |
| // between 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( |
| () -> |
| assertThrows( |
| InterruptedException.class, () -> eval(/*keepGoing=*/ true, keyA, keyB))); |
| |
| // 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. |
| final CountDownLatch allValuesReady = new CountDownLatch(3); |
| final SkyKey waitKey = GraphTester.toSkyKey("wait"); |
| final SkyKey fastKey = GraphTester.toSkyKey("fast"); |
| SkyKey leafKey = GraphTester.toSkyKey("leaf"); |
| tester.getOrCreate(waitKey).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| allValuesReady.countDown(); |
| Thread.sleep(10000); |
| throw new AssertionError("Should have been interrupted"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester.getOrCreate(fastKey).setBuilder(new ChainedFunction(null, null, allValuesReady, false, |
| new StringValue("fast"), ImmutableList.of(leafKey))); |
| tester.set(leafKey, new StringValue("leaf")); |
| if (buildFastFirst) { |
| eval(/*keepGoing=*/false, fastKey); |
| } |
| final Set<SkyKey> receivedValues = Sets.newConcurrentHashSet(); |
| revalidationReceiver = |
| new DirtyTrackingProgressReceiver( |
| new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { |
| @Override |
| public void evaluated( |
| SkyKey skyKey, |
| @Nullable SkyValue value, |
| Supplier<EvaluationSuccessState> evaluationSuccessState, |
| EvaluationState state) { |
| receivedValues.add(skyKey); |
| } |
| }); |
| TestThread evalThread = |
| new TestThread( |
| () -> |
| assertThrows( |
| InterruptedException.class, () -> eval(/*keepGoing=*/ true, waitKey, fastKey))); |
| evalThread.start(); |
| assertThat(allValuesReady.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); |
| evalThread.interrupt(); |
| evalThread.join(TestUtils.WAIT_TIMEOUT_MILLISECONDS); |
| assertThat(evalThread.isAlive()).isFalse(); |
| if (buildFastFirst) { |
| // If leafKey was already built, it is not reported to the receiver. |
| assertThat(receivedValues).containsExactly(fastKey); |
| } else { |
| // On first time being built, leafKey is registered too. |
| assertThat(receivedValues).containsExactly(fastKey, leafKey); |
| } |
| } |
| |
| @Test |
| public void partialResultOnInterruption() throws Exception { |
| runPartialResultOnInterruption(/*buildFastFirst=*/false); |
| } |
| |
| @Test |
| public void partialCachedResultOnInterruption() throws Exception { |
| runPartialResultOnInterruption(/*buildFastFirst=*/true); |
| } |
| |
| /** |
| * Factory for SkyFunctions for interruption testing (see {@link #runInterruptionTest}). |
| */ |
| private interface SkyFunctionFactory { |
| /** |
| * Creates a SkyFunction suitable for a specific test scenario. |
| * |
| * @param threadStarted a latch which the returned SkyFunction must |
| * {@link Semaphore#release() release} once it started (otherwise the test won't work) |
| * @param errorMessage a single-element array; the SkyFunction can put a error message in it |
| * to indicate that an assertion failed (calling {@code fail} from async thread doesn't |
| * work) |
| */ |
| SkyFunction create(final Semaphore threadStarted, final String[] errorMessage); |
| } |
| |
| /** |
| * Test that we can handle the Evaluator getting interrupted at various points. |
| * |
| * <p>This method creates an Evaluator with the specified SkyFunction for GraphTested.NODE_TYPE, |
| * then starts a thread, requests evaluation and asserts that evaluation started. It then |
| * interrupts the Evaluator thread and asserts that it acknowledged the interruption. |
| * |
| * @param valueBuilderFactory creates a SkyFunction which may or may not handle interruptions |
| * (depending on the test) |
| */ |
| private void runInterruptionTest(SkyFunctionFactory valueBuilderFactory) throws Exception { |
| final Semaphore threadStarted = new Semaphore(0); |
| final Semaphore threadInterrupted = new Semaphore(0); |
| final String[] wasError = new String[] { null }; |
| final ParallelEvaluator evaluator = |
| makeEvaluator( |
| new InMemoryGraphImpl(), |
| ImmutableMap.of( |
| GraphTester.NODE_TYPE, valueBuilderFactory.create(threadStarted, wasError)), |
| false); |
| |
| Thread t = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| evaluator.eval(ImmutableList.of(GraphTester.toSkyKey("a"))); |
| |
| // There's no real need to set an error here. If the thread is not interrupted then |
| // threadInterrupted is not released and the test thread will fail to acquire it. |
| wasError[0] = "evaluation should have been interrupted"; |
| } catch (InterruptedException e) { |
| // This is the interrupt we are waiting for. It should come straight from the |
| // evaluator (more precisely, the AbstractQueueVisitor). |
| // Signal the waiting test thread that the interrupt was acknowledged. |
| threadInterrupted.release(); |
| } |
| } |
| }); |
| |
| // Start the thread and wait for a semaphore. This ensures that the thread was really started. |
| t.start(); |
| assertThat(threadStarted.tryAcquire(TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) |
| .isTrue(); |
| |
| // Interrupt the thread and wait for a semaphore. This ensures that the thread was really |
| // interrupted and this fact was acknowledged. |
| t.interrupt(); |
| assertThat( |
| threadInterrupted.tryAcquire( |
| TestUtils.WAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) |
| .isTrue(); |
| |
| // The SkyFunction may have reported an error. |
| if (wasError[0] != null) { |
| fail(wasError[0]); |
| } |
| |
| // Wait for the thread to finish. |
| t.join(TestUtils.WAIT_TIMEOUT_MILLISECONDS); |
| } |
| |
| @Test |
| public void unrecoverableError() throws Exception { |
| class CustomRuntimeException extends RuntimeException {} |
| final CustomRuntimeException expected = new CustomRuntimeException(); |
| |
| final SkyFunction builder = new SkyFunction() { |
| @Override |
| @Nullable |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| throw expected; |
| } |
| |
| @Override |
| @Nullable |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }; |
| |
| final ParallelEvaluator evaluator = |
| makeEvaluator( |
| new InMemoryGraphImpl(), ImmutableMap.of(GraphTester.NODE_TYPE, builder), false); |
| |
| SkyKey valueToEval = GraphTester.toSkyKey("a"); |
| RuntimeException re = |
| assertThrows(RuntimeException.class, () -> evaluator.eval(ImmutableList.of(valueToEval))); |
| assertThat(re) |
| .hasMessageThat() |
| .contains("Unrecoverable error while evaluating node '" + valueToEval.toString() + "'"); |
| assertThat(re).hasCauseThat().isInstanceOf(CustomRuntimeException.class); |
| } |
| |
| @Test |
| public void simpleWarning() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a").setWarning("warning on 'a'"); |
| StringValue value = (StringValue) eval(false, GraphTester.toSkyKey("a")); |
| assertThat(value.getValue()).isEqualTo("a"); |
| assertThatEvents(storedEventHandler.getEvents()).containsExactly("warning on 'a'"); |
| } |
| |
| /** Regression test: events from already-done value not replayed. */ |
| @Test |
| public void eventFromDoneChildRecorded() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a").setWarning("warning on 'a'"); |
| SkyKey a = GraphTester.toSkyKey("a"); |
| SkyKey top = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(top).addDependency(a).setComputedValue(CONCATENATE); |
| // Build a so that it is already in the graph. |
| eval(false, a); |
| assertThat(storedEventHandler.getEvents()).hasSize(1); |
| storedEventHandler.clear(); |
| // Build top. The warning from a should be printed. |
| eval(false, top); |
| assertThat(storedEventHandler.getEvents()).hasSize(1); |
| storedEventHandler.clear(); |
| // Build top again. The warning should have been stored in the value. |
| eval(false, top); |
| assertThat(storedEventHandler.getEvents()).hasSize(1); |
| } |
| |
| @Test |
| public void postableFromDoneChildRecorded() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| Postable post = new Postable() {}; |
| set("a", "a").setPostable(post); |
| SkyKey a = GraphTester.toSkyKey("a"); |
| SkyKey top = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(top).addDependency(a).setComputedValue(CONCATENATE); |
| // Build a so that it is already in the graph. |
| eval(false, a); |
| assertThat(storedEventHandler.getPosts()).containsExactly(post); |
| storedEventHandler.clear(); |
| // Build top. The post from a should be printed. |
| eval(false, top); |
| assertThat(storedEventHandler.getPosts()).containsExactly(post); |
| storedEventHandler.clear(); |
| // Build top again. The post should have been stored in the value. |
| eval(false, top); |
| assertThat(storedEventHandler.getPosts()).containsExactly(post); |
| } |
| |
| @Test |
| public void eventReportedTimely() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a").setWarning("warning on 'a'"); |
| SkyKey a = GraphTester.toSkyKey("a"); |
| SkyKey top = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(top).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey key, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| // The event from a should already have been posted. |
| assertThat(storedEventHandler.getEvents()).hasSize(1); |
| return new StringValue("foo"); |
| } |
| |
| @Override |
| @Nullable |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| // Build a so that it is already in the graph. |
| eval(false, a); |
| storedEventHandler.clear(); |
| // Build top. The warning from a should be printed before evaluating top. |
| eval(false, ImmutableList.of(a, top)); |
| assertThat(storedEventHandler.getEvents()).hasSize(1); |
| storedEventHandler.clear(); |
| } |
| |
| @Test |
| public void errorOfTopLevelTargetReported() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey a = GraphTester.toSkyKey("a"); |
| SkyKey b = GraphTester.toSkyKey("b"); |
| tester.getOrCreate(b).setHasError(true); |
| Event errorEvent = Event.error("foobar"); |
| tester.getOrCreate(a).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey key, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| try { |
| if (env.getValueOrThrow(b, SomeErrorException.class) == null) { |
| return null; |
| } |
| } catch (SomeErrorException ignored) { |
| // Continue silently. |
| } |
| env.getListener().handle(errorEvent); |
| throw new SkyFunctionException(new SomeErrorException("bazbar"), Transience.PERSISTENT) {}; |
| } |
| |
| @Override |
| @Nullable |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| eval(false, a); |
| assertThat(storedEventHandler.getEvents()).containsExactly(errorEvent); |
| } |
| |
| @Test |
| public void storedEventFilter() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey a = GraphTester.toSkyKey("a"); |
| final AtomicBoolean evaluated = new AtomicBoolean(false); |
| tester.getOrCreate(a).setBuilder(new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) { |
| evaluated.set(true); |
| env.getListener().handle(Event.error(null, "boop")); |
| env.getListener().handle(Event.warn(null, "beep")); |
| return new StringValue("a"); |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, |
| tester.getSkyFunctionMap(), |
| /*keepGoing=*/ false, |
| new EventFilter() { |
| @Override |
| public boolean apply(Event event) { |
| return event.getKind() == EventKind.ERROR; |
| } |
| |
| @Override |
| public boolean storeEventsAndPosts() { |
| return true; |
| } |
| }); |
| evaluator.eval(ImmutableList.of(a)); |
| assertThat(evaluated.get()).isTrue(); |
| assertThat(storedEventHandler.getEvents()).hasSize(2); |
| assertThatEvents(storedEventHandler.getEvents()).containsExactly("boop", "beep"); |
| storedEventHandler.clear(); |
| evaluator = makeEvaluator(graph, tester.getSkyFunctionMap(), /*keepGoing=*/ false); |
| evaluated.set(false); |
| evaluator.eval(ImmutableList.of(a)); |
| assertThat(evaluated.get()).isFalse(); |
| assertThatEvents(storedEventHandler.getEvents()).containsExactly("boop"); |
| } |
| |
| @Test |
| public void shouldCreateErrorValueWithRootCause() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(parentErrorKey).addDependency("a").addDependency(errorKey) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(errorKey).setHasError(true); |
| ErrorInfo error = evalValueInError(parentErrorKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void shouldBuildOneTarget() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| set("b", "b"); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorFreeKey = GraphTester.toSkyKey("ab"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(parentErrorKey).addDependency(errorKey).addDependency("a") |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate(errorFreeKey).addDependency("a").addDependency("b") |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(true, parentErrorKey, errorFreeKey); |
| ErrorInfo error = result.getError(parentErrorKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| StringValue abValue = result.get(errorFreeKey); |
| assertThat(abValue.getValue()).isEqualTo("ab"); |
| } |
| |
| @Test |
| public void catastropheHaltsBuild_KeepGoing_KeepEdges() throws Exception { |
| catastrophicBuild(true, true); |
| } |
| |
| @Test |
| public void catastropheHaltsBuild_KeepGoing_NoKeepEdges() throws Exception { |
| catastrophicBuild(true, false); |
| } |
| |
| @Test |
| public void catastropheInBuild_NoKeepGoing_KeepEdges() throws Exception { |
| catastrophicBuild(false, true); |
| } |
| |
| private void catastrophicBuild(boolean keepGoing, boolean keepEdges) throws Exception { |
| graph = new InMemoryGraphImpl(keepEdges); |
| |
| SkyKey catastropheKey = GraphTester.toSkyKey("catastrophe"); |
| SkyKey otherKey = GraphTester.toSkyKey("someKey"); |
| |
| final Exception catastrophe = new SomeErrorException("bad"); |
| tester |
| .getOrCreate(catastropheKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| throw new SkyFunctionException(catastrophe, Transience.PERSISTENT) { |
| @Override |
| public boolean isCatastrophic() { |
| return true; |
| } |
| }; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| |
| tester.getOrCreate(otherKey).setBuilder(new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| new CountDownLatch(1).await(); |
| throw new RuntimeException("can't get here"); |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(catastropheKey).setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(keepGoing, topKey, otherKey); |
| ErrorInfo error = result.getError(topKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(catastropheKey); |
| if (keepGoing) { |
| assertThat(result.getCatastrophe()).isSameInstanceAs(catastrophe); |
| } |
| } |
| |
| @Test |
| public void incrementalCycleWithCatastropheAndFailedBubbleUp() throws Exception { |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| // Comes alphabetically before "top". |
| SkyKey cycleKey = GraphTester.toSkyKey("cycle"); |
| SkyKey catastropheKey = GraphTester.toSkyKey("catastrophe"); |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| StringValue topValue = new StringValue("top"); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| env.getValues(ImmutableList.of(cycleKey)); |
| return env.valuesMissing() ? null : topValue; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(cycleKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| env.getValues(ImmutableList.of(cycleKey, catastropheKey)); |
| Preconditions.checkState(env.valuesMissing()); |
| return null; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(catastropheKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| throw new SkyFunctionException( |
| new SomeErrorException("catastrophe"), Transience.TRANSIENT) { |
| @Override |
| public boolean isCatastrophic() { |
| return true; |
| } |
| }; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/ true, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result).hasError(); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(topKey) |
| .hasCycleInfoThat() |
| .containsExactly(new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(cycleKey))); |
| } |
| |
| @Test |
| public void parentFailureDoesntAffectChild() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentKey).setHasError(true); |
| SkyKey childKey = GraphTester.toSkyKey("child"); |
| set("child", "onions"); |
| tester.getOrCreate(parentKey).addDependency(childKey).setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, parentKey, childKey); |
| // Child is guaranteed to complete successfully before parent can run (and fail), |
| // since parent depends on it. |
| StringValue childValue = result.get(childKey); |
| assertThat(childValue).isNotNull(); |
| assertThat(childValue.getValue()).isEqualTo("onions"); |
| ErrorInfo error = result.getError(parentKey); |
| assertThat(error).isNotNull(); |
| assertThat(error.getRootCauses().toList()).containsExactly(parentKey); |
| } |
| |
| @Test |
| public void newParentOfErrorShouldHaveError() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| ErrorInfo error = evalValueInError(errorKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency("error").setComputedValue(CONCATENATE); |
| error = evalValueInError(parentKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void errorTwoLevelsDeep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate("mid").addDependency(errorKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(parentKey).addDependency("mid").setComputedValue(CONCATENATE); |
| ErrorInfo error = evalValueInError(parentKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void valueNotUsedInFailFastErrorRecovery() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey recoveryKey = GraphTester.toSkyKey("midRecovery"); |
| SkyKey badKey = GraphTester.toSkyKey("bad"); |
| |
| tester.getOrCreate(topKey).addDependency(recoveryKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(recoveryKey).addErrorDependency(badKey, new StringValue("i recovered")) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(badKey).setHasError(true); |
| |
| EvaluationResult<SkyValue> result = eval(/*keepGoing=*/true, ImmutableList.of(recoveryKey)); |
| assertThat(result.errorMap()).isEmpty(); |
| assertThatEvaluationResult(result).hasNoError(); |
| assertThat(result.get(recoveryKey)).isEqualTo(new StringValue("i recovered")); |
| |
| result = eval(/*keepGoing=*/false, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result).hasError(); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap()).hasSize(1); |
| assertThat(result.getError(topKey).getException()).isNotNull(); |
| } |
| |
| @Test |
| public void multipleRootCauses() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey errorKey2 = GraphTester.toSkyKey("error2"); |
| SkyKey errorKey3 = GraphTester.toSkyKey("error3"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate(errorKey2).setHasError(true); |
| tester.getOrCreate(errorKey3).setHasError(true); |
| tester.getOrCreate("mid").addDependency(errorKey).addDependency(errorKey2) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(parentKey) |
| .addDependency("mid").addDependency(errorKey2).addDependency(errorKey3) |
| .setComputedValue(CONCATENATE); |
| ErrorInfo error = evalValueInError(parentKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey, errorKey2, errorKey3); |
| } |
| |
| @Test |
| public void rootCauseWithNoKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate("mid").addDependency(errorKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(parentKey).addDependency("mid").setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(false, ImmutableList.of(parentKey)); |
| Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); |
| assertThat(error.getKey()).isEqualTo(parentKey); |
| assertThat(error.getValue().getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void errorBubblesToParentsOfTopLevelValue() throws Exception { |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| CountDownLatch latch = new CountDownLatch(1); |
| graph = |
| new NotifyingHelper.NotifyingProcessableGraph( |
| new InMemoryGraphImpl(), |
| (key, type, order, context) -> { |
| if (key.equals(errorKey) |
| && parentKey.equals(context) |
| && type == EventType.ADD_REVERSE_DEP |
| && order == Order.AFTER) { |
| latch.countDown(); |
| } |
| }); |
| tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, /*waitToFinish=*/latch, null, |
| false, /*value=*/null, ImmutableList.<SkyKey>of())); |
| tester.getOrCreate(parentKey).addDependency(errorKey).setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval( /*keepGoing=*/false, |
| ImmutableList.of(parentKey, errorKey)); |
| assertWithMessage(result.toString()).that(result.errorMap().size()).isEqualTo(2); |
| } |
| |
| @Test |
| public void noKeepGoingAfterKeepGoingFails() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency(errorKey); |
| ErrorInfo error = evalValueInError(parentKey); |
| assertThat(error.getRootCauses().toList()).containsExactly(errorKey); |
| SkyKey[] list = { parentKey }; |
| EvaluationResult<StringValue> result = eval(false, list); |
| ErrorInfo errorInfo = result.getError(); |
| assertThat(errorInfo.getRootCauses().toList()).containsExactly(errorKey); |
| assertThat(errorInfo.getException()).hasMessageThat().isEqualTo(errorKey.toString()); |
| } |
| |
| @Test |
| public void twoErrors() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey firstError = GraphTester.toSkyKey("error1"); |
| SkyKey secondError = GraphTester.toSkyKey("error2"); |
| CountDownLatch firstStart = new CountDownLatch(1); |
| CountDownLatch secondStart = new CountDownLatch(1); |
| tester.getOrCreate(firstError).setBuilder(new ChainedFunction(firstStart, secondStart, |
| /*notifyFinish=*/null, /*waitForException=*/false, /*value=*/null, |
| ImmutableList.<SkyKey>of())); |
| tester.getOrCreate(secondError).setBuilder(new ChainedFunction(secondStart, firstStart, |
| /*notifyFinish=*/null, /*waitForException=*/false, /*value=*/null, |
| ImmutableList.<SkyKey>of())); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, firstError, secondError); |
| assertWithMessage(result.toString()).that(result.hasError()).isTrue(); |
| // With keepGoing=false, the eval call will terminate with exactly one error (the first one |
| // thrown). But the first one thrown here is non-deterministic since we synchronize the |
| // builders so that they run at roughly the same time. |
| assertThat(ImmutableSet.of(firstError, secondError)).contains( |
| Iterables.getOnlyElement(result.errorMap().keySet())); |
| } |
| |
| @Test |
| public void simpleCycle() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey); |
| ErrorInfo errorInfo = eval(false, ImmutableList.of(aKey)).getError(); |
| assertThat(errorInfo.getException()).isNull(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).isEmpty(); |
| } |
| |
| @Test |
| public void cycleWithHead() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| tester.getOrCreate(topKey).addDependency(midKey); |
| tester.getOrCreate(midKey).addDependency(aKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey); |
| ErrorInfo errorInfo = eval(false, ImmutableList.of(topKey)).getError(); |
| assertThat(errorInfo.getException()).isNull(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); |
| } |
| |
| @Test |
| public void selfEdgeWithHead() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| tester.getOrCreate(topKey).addDependency(midKey); |
| tester.getOrCreate(midKey).addDependency(aKey); |
| tester.getOrCreate(aKey).addDependency(aKey); |
| ErrorInfo errorInfo = eval(false, ImmutableList.of(topKey)).getError(); |
| assertThat(errorInfo.getException()).isNull(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(aKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); |
| } |
| |
| @Test |
| public void cycleWithKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey goodKey = GraphTester.toSkyKey("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 = eval(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()); |
| assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey, midKey).inOrder(); |
| } |
| |
| @Test |
| public void twoCycles() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey dKey = GraphTester.toSkyKey("d"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey); |
| tester.getOrCreate(cKey).addDependency(dKey); |
| tester.getOrCreate(dKey).addDependency(cKey); |
| EvaluationResult<StringValue> result = eval(false, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(topKey); |
| Iterable<CycleInfo> cycles = CycleInfo.prepareCycles(topKey, |
| ImmutableList.of(new CycleInfo(ImmutableList.of(aKey, bKey)), |
| new CycleInfo(ImmutableList.of(cKey, dKey)))); |
| assertThat(cycles).contains(getOnlyElement(errorInfo.getCycleInfo())); |
| } |
| |
| |
| @Test |
| public void twoCyclesKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey dKey = GraphTester.toSkyKey("d"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey); |
| tester.getOrCreate(cKey).addDependency(dKey); |
| tester.getOrCreate(dKey).addDependency(cKey); |
| EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(topKey); |
| CycleInfo aCycle = new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(aKey, bKey)); |
| CycleInfo cCycle = new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(cKey, dKey)); |
| assertThat(errorInfo.getCycleInfo()).containsExactly(aCycle, cCycle); |
| } |
| |
| @Test |
| public void triangleBelowHeadCycle() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(aKey); |
| tester.getOrCreate(aKey).addDependency(bKey).addDependency(cKey); |
| tester.getOrCreate(bKey).addDependency(cKey); |
| tester.getOrCreate(cKey).addDependency(topKey); |
| EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(topKey); |
| CycleInfo topCycle = new CycleInfo(ImmutableList.of(topKey, aKey, cKey)); |
| assertThat(errorInfo.getCycleInfo()).containsExactly(topCycle); |
| } |
| |
| @Test |
| public void longCycle() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(aKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(cKey); |
| tester.getOrCreate(cKey).addDependency(topKey); |
| EvaluationResult<StringValue> result = eval(true, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(topKey); |
| CycleInfo topCycle = new CycleInfo(ImmutableList.of(topKey, aKey, bKey, cKey)); |
| assertThat(errorInfo.getCycleInfo()).containsExactly(topCycle); |
| } |
| |
| @Test |
| public void cycleWithTail() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(aKey).addDependency(cKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey).addDependency(cKey); |
| tester.getOrCreate(cKey); |
| tester.set(cKey, new StringValue("cValue")); |
| EvaluationResult<StringValue> result = eval(false, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(topKey); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(aKey, bKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey).inOrder(); |
| } |
| |
| /** Regression test: "value cannot be ready in a cycle". */ |
| @Test |
| public void selfEdgeWithExtraChildrenUnderCycle() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey zKey = GraphTester.toSkyKey("z"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| tester.getOrCreate(aKey).addDependency(zKey); |
| tester.getOrCreate(zKey).addDependency(cKey).addDependency(zKey); |
| tester.getOrCreate(cKey).addDependency(aKey); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); |
| assertThat(result.get(aKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(aKey); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(zKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); |
| } |
| |
| /** Regression test: "value cannot be ready in a cycle". */ |
| @Test |
| public void cycleWithExtraChildrenUnderCycle() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| SkyKey dKey = GraphTester.toSkyKey("d"); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(cKey).addDependency(dKey); |
| tester.getOrCreate(cKey).addDependency(aKey); |
| tester.getOrCreate(dKey).addDependency(bKey); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); |
| assertThat(result.get(aKey)).isNull(); |
| ErrorInfo errorInfo = result.getError(aKey); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(bKey, dKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); |
| } |
| |
| /** Regression test: "value cannot be ready in a cycle". */ |
| @Test |
| public void cycleAboveIndependentCycle() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| SkyKey cKey = GraphTester.toSkyKey("c"); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(cKey); |
| tester.getOrCreate(cKey).addDependency(aKey).addDependency(bKey); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); |
| assertThat(result.get(aKey)).isNull(); |
| assertThat(result.getError(aKey).getCycleInfo()).containsExactly( |
| new CycleInfo(ImmutableList.of(aKey, bKey, cKey)), |
| new CycleInfo(ImmutableList.of(aKey), ImmutableList.of(bKey, cKey))); |
| } |
| |
| @Test |
| public void valueAboveCycleAndExceptionReportsException() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey aKey = GraphTester.toSkyKey("a"); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey bKey = GraphTester.toSkyKey("b"); |
| tester.getOrCreate(aKey).addDependency(bKey).addDependency(errorKey); |
| tester.getOrCreate(bKey).addDependency(bKey); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(aKey)); |
| assertThat(result.get(aKey)).isNull(); |
| assertThat(result.getError(aKey).getException()).isNotNull(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(result.getError(aKey).getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(bKey).inOrder(); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(aKey).inOrder(); |
| } |
| |
| @Test |
| public void errorValueStored() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(false, ImmutableList.of(errorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap().keySet()).containsExactly(errorKey); |
| ErrorInfo errorInfo = result.getError(); |
| assertThat(errorInfo.getRootCauses().toList()).containsExactly(errorKey); |
| // Update value. But builder won't rebuild it. |
| tester.getOrCreate(errorKey).setHasError(false); |
| tester.set(errorKey, new StringValue("no error?")); |
| result = eval(false, ImmutableList.of(errorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap().keySet()).containsExactly(errorKey); |
| errorInfo = result.getError(); |
| assertThat(errorInfo.getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| /** |
| * Regression test: "OOM in Skyframe cycle detection". |
| * We only store the first 20 cycles found below any given root value. |
| */ |
| @Test |
| public void manyCycles() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| for (int i = 0; i < 100; i++) { |
| SkyKey dep = GraphTester.toSkyKey(Integer.toString(i)); |
| tester.getOrCreate(topKey).addDependency(dep); |
| tester.getOrCreate(dep).addDependency(dep); |
| } |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| assertManyCycles(result.getError(topKey), topKey, /*selfEdge=*/false); |
| } |
| |
| /** |
| * Regression test: "OOM in Skyframe cycle detection". |
| * We filter out multiple paths to a cycle that go through the same child value. |
| */ |
| @Test |
| public void manyPathsToCycle() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey cycleKey = GraphTester.toSkyKey("cycle"); |
| tester.getOrCreate(topKey).addDependency(midKey); |
| tester.getOrCreate(cycleKey).addDependency(cycleKey); |
| for (int i = 0; i < 100; i++) { |
| SkyKey dep = GraphTester.toSkyKey(Integer.toString(i)); |
| tester.getOrCreate(midKey).addDependency(dep); |
| tester.getOrCreate(dep).addDependency(cycleKey); |
| } |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); |
| assertThat(result.get(topKey)).isNull(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(result.getError(topKey).getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).hasSize(1); |
| assertThat(cycleInfo.getPathToCycle()).hasSize(3); |
| assertThat(cycleInfo.getPathToCycle().subList(0, 2)).containsExactly(topKey, midKey).inOrder(); |
| } |
| |
| /** |
| * Checks that errorInfo has many self-edge cycles, and that one of them is a self-edge of |
| * topKey, if {@code selfEdge} is true. |
| */ |
| private static void assertManyCycles(ErrorInfo errorInfo, SkyKey topKey, boolean selfEdge) { |
| assertThat(Iterables.size(errorInfo.getCycleInfo())).isGreaterThan(1); |
| assertThat(Iterables.size(errorInfo.getCycleInfo())).isLessThan(50); |
| boolean foundSelfEdge = false; |
| for (CycleInfo cycle : errorInfo.getCycleInfo()) { |
| assertThat(cycle.getCycle()).hasSize(1); // Self-edge. |
| if (!Iterables.isEmpty(cycle.getPathToCycle())) { |
| assertThat(cycle.getPathToCycle()).containsExactly(topKey).inOrder(); |
| } else { |
| assertThat(cycle.getCycle()).containsExactly(topKey).inOrder(); |
| foundSelfEdge = true; |
| } |
| } |
| assertWithMessage(errorInfo + ", " + topKey).that(foundSelfEdge).isEqualTo(selfEdge); |
| } |
| |
| @Test |
| public void manyUnprocessedValuesInCycle() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey lastSelfKey = GraphTester.toSkyKey("zlastSelf"); |
| SkyKey firstSelfKey = GraphTester.toSkyKey("afirstSelf"); |
| SkyKey midSelfKey = GraphTester.toSkyKey("midSelf9"); |
| // We add firstSelf first so that it is processed last in cycle detection (LIFO), meaning that |
| // none of the dep values have to be cleared from firstSelf. |
| tester.getOrCreate(firstSelfKey).addDependency(firstSelfKey); |
| for (int i = 0; i < 100; i++) { |
| SkyKey firstDep = GraphTester.toSkyKey("first" + i); |
| SkyKey midDep = GraphTester.toSkyKey("midSelf" + i + "dep"); |
| SkyKey lastDep = GraphTester.toSkyKey("last" + i); |
| tester.getOrCreate(firstSelfKey).addDependency(firstDep); |
| tester.getOrCreate(midSelfKey).addDependency(midDep); |
| tester.getOrCreate(lastSelfKey).addDependency(lastDep); |
| if (i == 90) { |
| // Most of the deps will be cleared from midSelf. |
| tester.getOrCreate(midSelfKey).addDependency(midSelfKey); |
| } |
| tester.getOrCreate(firstDep).addDependency(firstDep); |
| tester.getOrCreate(midDep).addDependency(midDep); |
| tester.getOrCreate(lastDep).addDependency(lastDep); |
| } |
| // All the deps will be cleared from lastSelf. |
| tester.getOrCreate(lastSelfKey).addDependency(lastSelfKey); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, |
| ImmutableList.of(lastSelfKey, firstSelfKey, midSelfKey)); |
| assertWithMessage(result.toString()).that(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap().keySet()).containsExactly(lastSelfKey, firstSelfKey, midSelfKey); |
| |
| // Check lastSelfKey. |
| ErrorInfo errorInfo = result.getError(lastSelfKey); |
| assertWithMessage(errorInfo.toString()) |
| .that(Iterables.size(errorInfo.getCycleInfo())) |
| .isEqualTo(1); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo()); |
| assertThat(cycleInfo.getCycle()).containsExactly(lastSelfKey); |
| assertThat(cycleInfo.getPathToCycle()).isEmpty(); |
| |
| // Check firstSelfKey. It should not have discovered its own self-edge, because there were too |
| // many other values before it in the queue. |
| assertManyCycles(result.getError(firstSelfKey), firstSelfKey, /*selfEdge=*/false); |
| |
| // Check midSelfKey. It should have discovered its own self-edge. |
| assertManyCycles(result.getError(midSelfKey), midSelfKey, /*selfEdge=*/true); |
| } |
| |
| @Test |
| public void errorValueStoredWithKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(true, ImmutableList.of(errorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap().keySet()).containsExactly(errorKey); |
| ErrorInfo errorInfo = result.getError(); |
| assertThat(errorInfo.getRootCauses().toList()).containsExactly(errorKey); |
| // Update value. But builder won't rebuild it. |
| tester.getOrCreate(errorKey).setHasError(false); |
| tester.set(errorKey, new StringValue("no error?")); |
| result = eval(true, ImmutableList.of(errorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.errorMap().keySet()).containsExactly(errorKey); |
| errorInfo = result.getError(); |
| assertThat(errorInfo.getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void continueWithErrorDep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentKey).addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE).addDependency("after"); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(parentKey)); |
| assertThat(result.errorMap()).isEmpty(); |
| assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredafter"); |
| result = eval(/*keepGoing=*/false, ImmutableList.of(parentKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); |
| assertThat(error.getKey()).isEqualTo(parentKey); |
| assertThat(error.getValue().getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void transformErrorDep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")) |
| .setHasError(true); |
| EvaluationResult<StringValue> result = eval( |
| /*keepGoing=*/false, ImmutableList.of(parentErrorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); |
| assertThat(error.getKey()).isEqualTo(parentErrorKey); |
| assertThat(error.getValue().getRootCauses().toList()).containsExactly(parentErrorKey); |
| } |
| |
| @Test |
| public void transformErrorDepKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")) |
| .setHasError(true); |
| EvaluationResult<StringValue> result = eval( |
| /*keepGoing=*/true, ImmutableList.of(parentErrorKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); |
| assertThat(error.getKey()).isEqualTo(parentErrorKey); |
| assertThat(error.getValue().getRootCauses().toList()).containsExactly(parentErrorKey); |
| } |
| |
| @Test |
| public void transformErrorDepOneLevelDownKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); |
| tester.set(parentErrorKey, new StringValue("parent value")); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(parentErrorKey).addDependency("after") |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(topKey)); |
| assertThat(ImmutableList.<String>copyOf(result.<String>keyNames())).containsExactly("top"); |
| assertThat(result.get(topKey).getValue()).isEqualTo("parent valueafter"); |
| assertThat(result.errorMap()).isEmpty(); |
| } |
| |
| @Test |
| public void transformErrorDepOneLevelDownNoKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentErrorKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); |
| tester.set(parentErrorKey, new StringValue("parent value")); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(parentErrorKey).addDependency("after") |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, ImmutableList.of(topKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet()); |
| assertThat(error.getKey()).isEqualTo(topKey); |
| assertThat(error.getValue().getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| @Test |
| public void errorDepDoesntStopOtherDep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| final SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result1 = eval(/*keepGoing=*/ true, ImmutableList.of(errorKey)); |
| assertThatEvaluationResult(result1).hasError(); |
| assertThatEvaluationResult(result1) |
| .hasErrorEntryForKeyThat(errorKey) |
| .hasExceptionThat() |
| .isNotNull(); |
| final SkyKey otherKey = GraphTester.toSkyKey("other"); |
| tester.getOrCreate(otherKey).setConstantValue(new StringValue("other")); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| final Exception topException = new SomeErrorException("top exception"); |
| final AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| Map<SkyKey, ValueOrException<SomeErrorException>> values = |
| env.getValuesOrThrow( |
| ImmutableList.of(errorKey, otherKey), SomeErrorException.class); |
| if (numComputes.incrementAndGet() == 1) { |
| assertThat(env.valuesMissing()).isTrue(); |
| } else { |
| assertThat(numComputes.get()).isEqualTo(2); |
| assertThat(env.valuesMissing()).isFalse(); |
| } |
| try { |
| values.get(errorKey).get(); |
| throw new AssertionError("Should have thrown"); |
| } catch (SomeErrorException e) { |
| throw new SkyFunctionException(topException, Transience.PERSISTENT) {}; |
| } |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| EvaluationResult<StringValue> result2 = eval(/*keepGoing=*/ true, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result2).hasError(); |
| assertThatEvaluationResult(result2) |
| .hasErrorEntryForKeyThat(topKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(topException); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| /** |
| * Make sure that multiple unfinished children can be cleared from a cycle value. |
| */ |
| @Test |
| public void cycleWithMultipleUnfinishedChildren() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey cycleKey = GraphTester.toSkyKey("zcycle"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| SkyKey selfEdge1 = GraphTester.toSkyKey("selfEdge1"); |
| SkyKey selfEdge2 = GraphTester.toSkyKey("selfEdge2"); |
| tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); |
| // selfEdge* come before cycleKey, so cycleKey's path will be checked first (LIFO), and the |
| // cycle with mid will be detected before the selfEdge* cycles are. |
| tester.getOrCreate(midKey).addDependency(selfEdge1).addDependency(selfEdge2) |
| .addDependency(cycleKey) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(cycleKey).addDependency(midKey); |
| tester.getOrCreate(selfEdge1).addDependency(selfEdge1); |
| tester.getOrCreate(selfEdge2).addDependency(selfEdge2); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableSet.of(topKey)); |
| assertThat(result.errorMap().keySet()).containsExactly(topKey); |
| Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); |
| assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); |
| } |
| |
| /** |
| * Regression test: "value in cycle depends on error". |
| * The mid value will have two parents -- top and cycle. Error bubbles up from mid to cycle, and |
| * we should detect cycle. |
| */ |
| private void cycleAndErrorInBubbleUp(boolean keepGoing) throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey cycleKey = GraphTester.toSkyKey("cycle"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) |
| .setComputedValue(CONCATENATE); |
| |
| // We need to ensure that cycle value has finished its work, and we have recorded dependencies |
| CountDownLatch cycleFinish = new CountDownLatch(1); |
| tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, |
| null, cycleFinish, false, new StringValue(""), ImmutableSet.<SkyKey>of(midKey))); |
| tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, cycleFinish, |
| null, /*waitForException=*/false, null, ImmutableSet.<SkyKey>of())); |
| |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableSet.of(topKey)); |
| assertThat(result.errorMap().keySet()).containsExactly(topKey); |
| Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); |
| if (keepGoing) { |
| // The error thrown will only be recorded in keep_going mode. |
| assertThat(result.getError().getRootCauses().toList()).containsExactly(errorKey); |
| } |
| assertThat(cycleInfos).isNotEmpty(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); |
| assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); |
| } |
| |
| @Test |
| public void cycleAndErrorInBubbleUpNoKeepGoing() throws Exception { |
| cycleAndErrorInBubbleUp(false); |
| } |
| |
| @Test |
| public void cycleAndErrorInBubbleUpKeepGoing() throws Exception { |
| cycleAndErrorInBubbleUp(true); |
| } |
| |
| /** |
| * Regression test: "value in cycle depends on error". |
| * We add another value that won't finish building before the threadpool shuts down, to check that |
| * the cycle detection can handle unfinished values. |
| */ |
| @Test |
| public void cycleAndErrorAndOtherInBubbleUp() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey cycleKey = GraphTester.toSkyKey("cycle"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); |
| // We should add cycleKey first and errorKey afterwards. Otherwise there is a chance that |
| // during error propagation cycleKey will not be processed, and we will not detect the cycle. |
| tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) |
| .setComputedValue(CONCATENATE); |
| SkyKey otherTop = GraphTester.toSkyKey("otherTop"); |
| CountDownLatch topStartAndCycleFinish = new CountDownLatch(2); |
| // In nokeep_going mode, otherTop will wait until the threadpool has received an exception, |
| // then request its own dep. This guarantees that there is a value that is not finished when |
| // cycle detection happens. |
| tester.getOrCreate(otherTop).setBuilder(new ChainedFunction(topStartAndCycleFinish, |
| new CountDownLatch(0), null, /*waitForException=*/true, new StringValue("never returned"), |
| ImmutableSet.<SkyKey>of(GraphTester.toSkyKey("dep that never builds")))); |
| |
| tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, null, |
| topStartAndCycleFinish, /*waitForException=*/false, new StringValue(""), |
| ImmutableSet.<SkyKey>of(midKey))); |
| // error waits until otherTop starts and cycle finishes, to make sure otherTop will request |
| // its dep before the threadpool shuts down. |
| tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, topStartAndCycleFinish, |
| null, /*waitForException=*/false, null, |
| ImmutableSet.<SkyKey>of())); |
| EvaluationResult<StringValue> result = |
| eval(/*keepGoing=*/false, ImmutableSet.of(topKey, otherTop)); |
| assertThat(result.errorMap().keySet()).containsExactly(topKey); |
| Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); |
| assertThat(cycleInfos).isNotEmpty(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); |
| assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); |
| } |
| |
| /** |
| * Regression test: "value in cycle depends on error". |
| * Here, we add an additional top-level key in error, just to mix it up. |
| */ |
| private void cycleAndErrorAndError(boolean keepGoing) throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey cycleKey = GraphTester.toSkyKey("cycle"); |
| SkyKey midKey = GraphTester.toSkyKey("mid"); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(midKey).addDependency(errorKey).addDependency(cycleKey) |
| .setComputedValue(CONCATENATE); |
| SkyKey otherTop = GraphTester.toSkyKey("otherTop"); |
| CountDownLatch topStartAndCycleFinish = new CountDownLatch(2); |
| // In nokeep_going mode, otherTop will wait until the threadpool has received an exception, |
| // then throw its own exception. This guarantees that its exception will not be the one |
| // bubbling up, but that there is a top-level value with an exception by the time the bubbling |
| // up starts. |
| tester.getOrCreate(otherTop).setBuilder(new ChainedFunction(topStartAndCycleFinish, |
| new CountDownLatch(0), null, /*waitForException=*/!keepGoing, null, |
| ImmutableSet.<SkyKey>of())); |
| // error waits until otherTop starts and cycle finishes, to make sure otherTop will request |
| // its dep before the threadpool shuts down. |
| tester.getOrCreate(errorKey).setBuilder(new ChainedFunction(null, topStartAndCycleFinish, |
| null, /*waitForException=*/false, null, |
| ImmutableSet.<SkyKey>of())); |
| tester.getOrCreate(cycleKey).setBuilder(new ChainedFunction(null, null, |
| topStartAndCycleFinish, /*waitForException=*/false, new StringValue(""), |
| ImmutableSet.<SkyKey>of(midKey))); |
| EvaluationResult<StringValue> result = |
| eval(keepGoing, ImmutableSet.of(topKey, otherTop)); |
| if (keepGoing) { |
| assertThat(result.errorMap().keySet()).containsExactly(otherTop, topKey); |
| assertThat(result.getError(otherTop).getRootCauses().toList()).containsExactly(otherTop); |
| // The error thrown will only be recorded in keep_going mode. |
| assertThat(result.getError(topKey).getRootCauses().toList()).containsExactly(errorKey); |
| } |
| Iterable<CycleInfo> cycleInfos = result.getError(topKey).getCycleInfo(); |
| assertThat(cycleInfos).isNotEmpty(); |
| CycleInfo cycleInfo = Iterables.getOnlyElement(cycleInfos); |
| assertThat(cycleInfo.getPathToCycle()).containsExactly(topKey); |
| assertThat(cycleInfo.getCycle()).containsExactly(midKey, cycleKey); |
| } |
| |
| @Test |
| public void cycleAndErrorAndErrorNoKeepGoing() throws Exception { |
| cycleAndErrorAndError(false); |
| } |
| |
| @Test |
| public void cycleAndErrorAndErrorKeepGoing() throws Exception { |
| cycleAndErrorAndError(true); |
| } |
| |
| @Test |
| public void testFunctionCrashTrace() throws Exception { |
| |
| class ChildFunction implements SkyFunction { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) { |
| throw new IllegalStateException("I WANT A PONY!!!"); |
| } |
| |
| @Override public String extractTag(SkyKey skyKey) { return null; } |
| } |
| |
| class ParentFunction implements SkyFunction { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| SkyValue dep = env.getValue(ChildKey.create("billy the kid")); |
| if (dep == null) { |
| return null; |
| } |
| throw new IllegalStateException(); // Should never get here. |
| } |
| |
| @Override public String extractTag(SkyKey skyKey) { return null; } |
| } |
| |
| ImmutableMap<SkyFunctionName, SkyFunction> skyFunctions = |
| ImmutableMap.of( |
| CHILD_TYPE, new ChildFunction(), |
| PARENT_TYPE, new ParentFunction()); |
| ParallelEvaluator evaluator = makeEvaluator(new InMemoryGraphImpl(), skyFunctions, false); |
| |
| RuntimeException e = |
| assertThrows( |
| RuntimeException.class, |
| () -> evaluator.eval(ImmutableList.of(ParentKey.create("octodad")))); |
| assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("I WANT A PONY!!!"); |
| assertThat(e) |
| .hasMessageThat() |
| .isEqualTo( |
| "Unrecoverable error while evaluating node 'child:billy the kid' " |
| + "(requested by nodes 'parent:octodad')"); |
| } |
| |
| private static class SomeOtherErrorException extends Exception { |
| public SomeOtherErrorException(String msg) { |
| super(msg); |
| } |
| } |
| |
| private void unexpectedErrorDep(boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| final SomeOtherErrorException exception = new SomeOtherErrorException("error exception"); |
| tester.getOrCreate(errorKey).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| throw new SkyFunctionException(exception, Transience.PERSISTENT) {}; |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| throw new UnsupportedOperationException(); |
| } |
| }); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| tester.getOrCreate(topKey).addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.getError(topKey).getException()).isSameInstanceAs(exception); |
| assertThat(result.getError(topKey).getRootCauses().toList()).containsExactly(errorKey); |
| } |
| |
| /** |
| * This and the following three tests are in response a bug: "Skyframe error propagation model is |
| * problematic". They ensure that exceptions a child throws that a value does not specify it can |
| * handle in getValueOrThrow do not cause a crash. |
| */ |
| @Test |
| public void unexpectedErrorDepKeepGoing() throws Exception { |
| unexpectedErrorDep(true); |
| } |
| |
| @Test |
| public void unexpectedErrorDepNoKeepGoing() throws Exception { |
| unexpectedErrorDep(false); |
| } |
| |
| private void unexpectedErrorDepOneLevelDown(final boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = GraphTester.toSkyKey("my_error_value"); |
| final SomeErrorException exception = new SomeErrorException("error exception"); |
| final SomeErrorException topException = new SomeErrorException("top exception"); |
| final StringValue topValue = new StringValue("top"); |
| tester.getOrCreate(errorKey).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws GenericFunctionException { |
| throw new GenericFunctionException(exception, Transience.PERSISTENT); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| throw new UnsupportedOperationException(); |
| } |
| }); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| final SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency(errorKey).setComputedValue(CONCATENATE); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws GenericFunctionException, InterruptedException { |
| try { |
| if (env.getValueOrThrow(parentKey, SomeErrorException.class) == null) { |
| return null; |
| } |
| } catch (SomeErrorException e) { |
| assertWithMessage(e.toString()).that(e).isEqualTo(exception); |
| } |
| if (keepGoing) { |
| return topValue; |
| } else { |
| throw new GenericFunctionException(topException, Transience.PERSISTENT); |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| throw new UnsupportedOperationException(); |
| } |
| }); |
| tester.getOrCreate(topKey).addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); |
| if (!keepGoing) { |
| assertThat(result.keyNames()).isEmpty(); |
| assertThat(result.getError(topKey).getException()).isEqualTo(topException); |
| assertThat(result.getError(topKey).getRootCauses().toList()).containsExactly(topKey); |
| assertThatEvaluationResult(result).hasError(); |
| } else { |
| assertThatEvaluationResult(result).hasNoError(); |
| assertThat(result.get(topKey)).isSameInstanceAs(topValue); |
| } |
| } |
| |
| @Test |
| public void unexpectedErrorDepOneLevelDownKeepGoing() throws Exception { |
| unexpectedErrorDepOneLevelDown(true); |
| } |
| |
| @Test |
| public void unexpectedErrorDepOneLevelDownNoKeepGoing() throws Exception { |
| unexpectedErrorDepOneLevelDown(false); |
| } |
| |
| /** |
| * Exercises various situations involving groups of deps that overlap -- request one group, then |
| * request another group that has a dep in common with the first group. |
| * |
| * @param sameFirst whether the dep in common in the two groups should be the first dep. |
| * @param twoCalls whether the two groups should be requested in two different builder calls. |
| * @param valuesOrThrow whether the deps should be requested using getValuesOrThrow. |
| */ |
| private void sameDepInTwoGroups(final boolean sameFirst, final boolean twoCalls, |
| final boolean valuesOrThrow) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = GraphTester.toSkyKey("top"); |
| final List<SkyKey> leaves = new ArrayList<>(); |
| for (int i = 1; i <= 3; i++) { |
| SkyKey leaf = GraphTester.toSkyKey("leaf" + i); |
| leaves.add(leaf); |
| tester.set(leaf, new StringValue("leaf" + i)); |
| } |
| final SkyKey leaf4 = GraphTester.toSkyKey("leaf4"); |
| tester.set(leaf4, new StringValue("leaf" + 4)); |
| tester.getOrCreate(topKey).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException, |
| InterruptedException { |
| if (valuesOrThrow) { |
| env.getValuesOrThrow(leaves, SomeErrorException.class); |
| } else { |
| env.getValues(leaves); |
| } |
| if (twoCalls && env.valuesMissing()) { |
| return null; |
| } |
| SkyKey first = sameFirst ? leaves.get(0) : leaf4; |
| SkyKey second = sameFirst ? leaf4 : leaves.get(2); |
| List<SkyKey> secondRequest = ImmutableList.of(first, second); |
| if (valuesOrThrow) { |
| env.getValuesOrThrow(secondRequest, SomeErrorException.class); |
| } else { |
| env.getValues(secondRequest); |
| } |
| if (env.valuesMissing()) { |
| return null; |
| } |
| return new StringValue("top"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| eval(/*keepGoing=*/false, topKey); |
| assertThat(eval(/*keepGoing=*/false, topKey)).isEqualTo(new StringValue("top")); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Same_Two_Throw() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/true, /*valuesOrThrow=*/true); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Same_Two_Deps() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/true, /*valuesOrThrow=*/false); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Same_One_Throw() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/false, /*valuesOrThrow=*/true); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Same_One_Deps() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/true, /*twoCalls=*/false, /*valuesOrThrow=*/false); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Different_Two_Throw() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/true, /*valuesOrThrow=*/true); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Different_Two_Deps() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/true, /*valuesOrThrow=*/false); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Different_One_Throw() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/false, /*valuesOrThrow=*/true); |
| } |
| |
| @Test |
| public void sameDepInTwoGroups_Different_One_Deps() throws Exception { |
| sameDepInTwoGroups(/*sameFirst=*/false, /*twoCalls=*/false, /*valuesOrThrow=*/false); |
| } |
| |
| private void getValuesOrThrowWithErrors(boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| final SkyKey errorDep = GraphTester.toSkyKey("errorChild"); |
| final SomeErrorException childExn = new SomeErrorException("child error"); |
| tester.getOrCreate(errorDep).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| throw new GenericFunctionException(childExn, Transience.PERSISTENT); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| final List<SkyKey> deps = new ArrayList<>(); |
| for (int i = 1; i <= 3; i++) { |
| SkyKey dep = GraphTester.toSkyKey("child" + i); |
| deps.add(dep); |
| tester.set(dep, new StringValue("child" + i)); |
| } |
| final SomeErrorException parentExn = new SomeErrorException("parent error"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| try { |
| SkyValue value = env.getValueOrThrow(errorDep, SomeErrorException.class); |
| if (value == null) { |
| return null; |
| } |
| } catch (SomeErrorException e) { |
| // Recover from the child error. |
| } |
| env.getValues(deps); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| throw new GenericFunctionException(parentExn, Transience.PERSISTENT); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| EvaluationResult<StringValue> evaluationResult = eval(keepGoing, ImmutableList.of(parentKey)); |
| assertThat(evaluationResult.hasError()).isTrue(); |
| assertThat(evaluationResult.getError().getException()) |
| .isEqualTo(keepGoing ? parentExn : childExn); |
| } |
| |
| @Test |
| public void getValuesOrThrowWithErrors_NoKeepGoing() throws Exception { |
| getValuesOrThrowWithErrors(/*keepGoing=*/false); |
| } |
| |
| @Test |
| public void getValuesOrThrowWithErrors_KeepGoing() throws Exception { |
| getValuesOrThrowWithErrors(/*keepGoing=*/true); |
| } |
| |
| @Test |
| public void duplicateCycles() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey grandparentKey = GraphTester.toSkyKey("grandparent"); |
| SkyKey parentKey1 = GraphTester.toSkyKey("parent1"); |
| SkyKey parentKey2 = GraphTester.toSkyKey("parent2"); |
| SkyKey loopKey1 = GraphTester.toSkyKey("loop1"); |
| SkyKey loopKey2 = GraphTester.toSkyKey("loop2"); |
| tester.getOrCreate(loopKey1).addDependency(loopKey2); |
| tester.getOrCreate(loopKey2).addDependency(loopKey1); |
| tester.getOrCreate(parentKey1).addDependency(loopKey1); |
| tester.getOrCreate(parentKey2).addDependency(loopKey2); |
| tester.getOrCreate(grandparentKey).addDependency(parentKey1); |
| tester.getOrCreate(grandparentKey).addDependency(parentKey2); |
| |
| ErrorInfo errorInfo = evalValueInError(grandparentKey); |
| List<ImmutableList<SkyKey>> cycles = Lists.newArrayList(); |
| for (CycleInfo cycleInfo : errorInfo.getCycleInfo()) { |
| cycles.add(cycleInfo.getCycle()); |
| } |
| // Skyframe doesn't automatically dedupe cycles that are the same except for entry point. |
| assertThat(cycles).hasSize(2); |
| int numUniqueCycles = 0; |
| CycleDeduper<SkyKey> cycleDeduper = new CycleDeduper<SkyKey>(); |
| for (ImmutableList<SkyKey> cycle : cycles) { |
| if (cycleDeduper.seen(cycle)) { |
| numUniqueCycles++; |
| } |
| } |
| assertThat(numUniqueCycles).isEqualTo(1); |
| } |
| |
| @Test |
| public void signalValueEnqueuedAndEvaluated() throws Exception { |
| final Set<SkyKey> enqueuedValues = Sets.newConcurrentHashSet(); |
| final Set<SkyKey> evaluatedValues = Sets.newConcurrentHashSet(); |
| EvaluationProgressReceiver progressReceiver = |
| new EvaluationProgressReceiver.NullEvaluationProgressReceiver() { |
| @Override |
| public void enqueueing(SkyKey skyKey) { |
| enqueuedValues.add(skyKey); |
| } |
| |
| @Override |
| public void evaluated( |
| SkyKey skyKey, |
| @Nullable SkyValue value, |
| Supplier<EvaluationSuccessState> evaluationSuccessState, |
| EvaluationState state) { |
| evaluatedValues.add(skyKey); |
| } |
| }; |
| |
| ExtendedEventHandler reporter = |
| new Reporter( |
| new EventBus(), |
| new EventHandler() { |
| @Override |
| public void handle(Event e) { |
| throw new IllegalStateException(); |
| } |
| }); |
| |
| MemoizingEvaluator aug = |
| new InMemoryMemoizingEvaluator( |
| ImmutableMap.of(GraphTester.NODE_TYPE, tester.getFunction()), |
| new SequencedRecordingDifferencer(), |
| progressReceiver); |
| SequentialBuildDriver driver = new SequentialBuildDriver(aug); |
| |
| tester.getOrCreate("top1").setComputedValue(CONCATENATE) |
| .addDependency("d1").addDependency("d2"); |
| tester.getOrCreate("top2").setComputedValue(CONCATENATE).addDependency("d3"); |
| tester.getOrCreate("top3"); |
| assertThat(enqueuedValues).isEmpty(); |
| assertThat(evaluatedValues).isEmpty(); |
| |
| tester.set("d1", new StringValue("1")); |
| tester.set("d2", new StringValue("2")); |
| tester.set("d3", new StringValue("3")); |
| |
| EvaluationContext evaluationContext = |
| EvaluationContext.newBuilder() |
| .setKeepGoing(false) |
| .setNumThreads(200) |
| .setEventHander(reporter) |
| .build(); |
| driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top1")), evaluationContext); |
| assertThat(enqueuedValues).containsExactlyElementsIn( |
| GraphTester.toSkyKeys("top1", "d1", "d2")); |
| assertThat(evaluatedValues).containsExactlyElementsIn( |
| GraphTester.toSkyKeys("top1", "d1", "d2")); |
| enqueuedValues.clear(); |
| evaluatedValues.clear(); |
| |
| driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top2")), evaluationContext); |
| assertThat(enqueuedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top2", "d3")); |
| assertThat(evaluatedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top2", "d3")); |
| enqueuedValues.clear(); |
| evaluatedValues.clear(); |
| |
| driver.evaluate(ImmutableList.of(GraphTester.toSkyKey("top1")), evaluationContext); |
| assertThat(enqueuedValues).isEmpty(); |
| assertThat(evaluatedValues).containsExactlyElementsIn(GraphTester.toSkyKeys("top1")); |
| } |
| |
| public void runDepOnErrorHaltsNoKeepGoingBuildEagerly(boolean childErrorCached, |
| final boolean handleChildError) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| final SkyKey childKey = GraphTester.toSkyKey("child"); |
| tester.getOrCreate(childKey).setHasError(/*hasError=*/true); |
| // The parent should be built exactly twice: once during normal evaluation and once |
| // during error bubbling. |
| final AtomicInteger numParentInvocations = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| int invocations = numParentInvocations.incrementAndGet(); |
| if (handleChildError) { |
| try { |
| SkyValue value = env.getValueOrThrow(childKey, SomeErrorException.class); |
| // On the first invocation, either the child error should already be cached and |
| // not propagated, or it should be computed freshly and not propagated. On the |
| // second build (error bubbling), the child error should be propagated. |
| assertWithMessage("bogus non-null value " + value).that(value == null).isTrue(); |
| assertWithMessage("parent incorrectly re-computed during normal evaluation") |
| .that(invocations) |
| .isEqualTo(1); |
| assertWithMessage("child error not propagated during error bubbling") |
| .that(env.inErrorBubblingForTesting()) |
| .isFalse(); |
| return value; |
| } catch (SomeErrorException e) { |
| assertWithMessage("child error propagated during normal evaluation") |
| .that(env.inErrorBubblingForTesting()) |
| .isTrue(); |
| assertThat(invocations).isEqualTo(2); |
| return null; |
| } |
| } else { |
| if (invocations == 1) { |
| assertWithMessage( |
| "parent's first computation should be during normal evaluation") |
| .that(env.inErrorBubblingForTesting()) |
| .isFalse(); |
| return env.getValue(childKey); |
| } else { |
| assertThat(invocations).isEqualTo(2); |
| assertWithMessage("parent incorrectly re-computed during normal evaluation") |
| .that(env.inErrorBubblingForTesting()) |
| .isTrue(); |
| return env.getValue(childKey); |
| } |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| if (childErrorCached) { |
| // Ensure that the child is already in the graph. |
| evalValueInError(childKey); |
| } |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, ImmutableList.of(parentKey)); |
| assertThat(numParentInvocations.get()).isEqualTo(2); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getRootCauseOfException()).isEqualTo(childKey); |
| } |
| |
| @Test |
| public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorCachedAndHandled() |
| throws Exception { |
| runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/true, |
| /*handleChildError=*/true); |
| } |
| |
| @Test |
| public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorCachedAndNotHandled() |
| throws Exception { |
| runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/true, |
| /*handleChildError=*/false); |
| } |
| |
| @Test |
| public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorFreshAndHandled() throws Exception { |
| runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/false, |
| /*handleChildError=*/true); |
| } |
| |
| @Test |
| public void depOnErrorHaltsNoKeepGoingBuildEagerly_ChildErrorFreshAndNotHandled() |
| throws Exception { |
| runDepOnErrorHaltsNoKeepGoingBuildEagerly(/*childErrorCached=*/false, |
| /*handleChildError=*/false); |
| } |
| |
| @Test |
| public void raceConditionWithNoKeepGoingErrors_FutureError() throws Exception { |
| final CountDownLatch errorCommitted = new CountDownLatch(1); |
| final CountDownLatch otherStarted = new CountDownLatch(1); |
| final CountDownLatch otherParentSignaled = new CountDownLatch(1); |
| final SkyKey errorParentKey = GraphTester.toSkyKey("errorParentKey"); |
| final SkyKey errorKey = GraphTester.toSkyKey("errorKey"); |
| final SkyKey otherParentKey = GraphTester.toSkyKey("otherParentKey"); |
| final SkyKey otherKey = GraphTester.toSkyKey("otherKey"); |
| final AtomicInteger numOtherParentInvocations = new AtomicInteger(0); |
| final AtomicInteger numErrorParentInvocations = new AtomicInteger(0); |
| tester |
| .getOrCreate(otherParentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| int invocations = numOtherParentInvocations.incrementAndGet(); |
| assertWithMessage("otherParentKey should not be restarted") |
| .that(invocations) |
| .isEqualTo(1); |
| return env.getValue(otherKey); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(otherKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| otherStarted.countDown(); |
| TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( |
| errorCommitted, "error didn't get committed to the graph in time"); |
| return new StringValue("other"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(errorKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException { |
| TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( |
| otherStarted, "other didn't start in time"); |
| throw new GenericFunctionException( |
| new SomeErrorException("error"), Transience.PERSISTENT); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(errorParentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| int invocations = numErrorParentInvocations.incrementAndGet(); |
| try { |
| SkyValue value = env.getValueOrThrow(errorKey, SomeErrorException.class); |
| assertWithMessage("bogus non-null value " + value).that(value == null).isTrue(); |
| if (invocations == 1) { |
| return null; |
| } else { |
| assertThat(env.inErrorBubblingForTesting()).isFalse(); |
| fail("RACE CONDITION: errorParentKey was restarted!"); |
| return null; |
| } |
| } catch (SomeErrorException e) { |
| assertWithMessage("child error propagated during normal evaluation") |
| .that(env.inErrorBubblingForTesting()) |
| .isTrue(); |
| assertThat(invocations).isEqualTo(2); |
| return null; |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| graph = |
| new NotifyingHelper.NotifyingProcessableGraph( |
| new InMemoryGraphImpl(), |
| 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( |
| otherParentSignaled, "otherParent didn't get signaled in time"); |
| // We try to give some time for ParallelEvaluator to incorrectly re-evaluate |
| // 'otherParentKey'. This test case is testing for a real race condition and the |
| // 10ms time was chosen experimentally to give a true positive rate of 99.8% |
| // (without a sleep it has a 1% true positive rate). There's no good way to do |
| // this without sleeping. We *could* introspect ParallelEvaulator's |
| // AbstractQueueVisitor to see if the re-evaluation has been enqueued, but that's |
| // relying on pretty low-level implementation details. |
| Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); |
| } |
| if (key.equals(otherParentKey) |
| && type == EventType.SIGNAL |
| && order == Order.AFTER) { |
| otherParentSignaled.countDown(); |
| } |
| } |
| }); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/false, |
| ImmutableList.of(otherParentKey, errorParentKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); |
| } |
| |
| @Test |
| public void cachedErrorsFromKeepGoingUsedOnNoKeepGoing() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| SkyKey parent1Key = GraphTester.toSkyKey("parent1"); |
| SkyKey parent2Key = GraphTester.toSkyKey("parent2"); |
| tester.getOrCreate(parent1Key).addDependency(errorKey).setConstantValue( |
| new StringValue("parent1")); |
| tester.getOrCreate(parent2Key).addDependency(errorKey).setConstantValue( |
| new StringValue("parent2")); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(parent1Key)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); |
| result = eval(/*keepGoing=*/false, ImmutableList.of(parent2Key)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError(parent2Key).getRootCauseOfException()).isEqualTo(errorKey); |
| } |
| |
| @Test |
| public void cachedTopLevelErrorsShouldHaltNoKeepGoingBuildEarly() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = GraphTester.toSkyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/true, ImmutableList.of(errorKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getRootCauseOfException()).isEqualTo(errorKey); |
| SkyKey rogueKey = GraphTester.toSkyKey("rogue"); |
| tester.getOrCreate(rogueKey).setBuilder(new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) { |
| // This SkyFunction could do an arbitrarily bad computation, e.g. loop-forever. So we want |
| // to make sure that it is never run when we want to fail-fast anyway. |
| fail("eval call should have already terminated"); |
| return null; |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| result = eval(/*keepGoing=*/false, ImmutableList.of(errorKey, rogueKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError(errorKey).getRootCauseOfException()).isEqualTo(errorKey); |
| assertThat(result.errorMap()).doesNotContainKey(rogueKey); |
| } |
| |
| // Explicit test that we tolerate a SkyFunction that declares different [sequences of] deps each |
| // restart. Such behavior from a SkyFunction isn't desired, but Bazel-on-Skyframe does indeed do |
| // this. |
| @Test |
| public void declaresDifferentDepsAfterRestart() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey grandChild1Key = GraphTester.toSkyKey("grandChild1"); |
| tester.getOrCreate(grandChild1Key).setConstantValue(new StringValue("grandChild1")); |
| SkyKey child1Key = GraphTester.toSkyKey("child1"); |
| tester |
| .getOrCreate(child1Key) |
| .addDependency(grandChild1Key) |
| .setConstantValue(new StringValue("child1")); |
| SkyKey grandChild2Key = GraphTester.toSkyKey("grandChild2"); |
| tester.getOrCreate(grandChild2Key).setConstantValue(new StringValue("grandChild2")); |
| SkyKey child2Key = GraphTester.toSkyKey("child2"); |
| tester.getOrCreate(child2Key).setConstantValue(new StringValue("child2")); |
| SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| switch (numComputes.incrementAndGet()) { |
| case 1: |
| env.getValue(child1Key); |
| Preconditions.checkState(env.valuesMissing()); |
| return null; |
| case 2: |
| env.getValue(child2Key); |
| Preconditions.checkState(env.valuesMissing()); |
| return null; |
| case 3: |
| return new StringValue("the third time's the charm!"); |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| EvaluationResult<StringValue> result = eval(/*keepGoing=*/ false, ImmutableList.of(parentKey)); |
| assertThatEvaluationResult(result).hasNoError(); |
| assertThatEvaluationResult(result) |
| .hasEntryThat(parentKey) |
| .isEqualTo(new StringValue("the third time's the charm!")); |
| } |
| |
| private void runUnhandledTransitiveErrors(boolean keepGoing, |
| final boolean explicitlyPropagateError) throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey grandparentKey = GraphTester.toSkyKey("grandparent"); |
| final SkyKey parentKey = GraphTester.toSkyKey("parent"); |
| final SkyKey childKey = GraphTester.toSkyKey("child"); |
| final AtomicBoolean errorPropagated = new AtomicBoolean(false); |
| tester |
| .getOrCreate(grandparentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| try { |
| return env.getValueOrThrow(parentKey, SomeErrorException.class); |
| } catch (SomeErrorException e) { |
| errorPropagated.set(true); |
| throw new GenericFunctionException(e, Transience.PERSISTENT); |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| if (explicitlyPropagateError) { |
| try { |
| return env.getValueOrThrow(childKey, SomeErrorException.class); |
| } catch (SomeErrorException e) { |
| throw new GenericFunctionException(e, childKey); |
| } |
| } else { |
| return env.getValue(childKey); |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| }); |
| tester.getOrCreate(childKey).setHasError(/*hasError=*/true); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(grandparentKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(errorPropagated.get()).isTrue(); |
| assertThat(result.getError().getRootCauseOfException()).isEqualTo(grandparentKey); |
| } |
| |
| @Test |
| public void unhandledTransitiveErrorsDuringErrorBubbling_ImplicitPropagation() throws Exception { |
| runUnhandledTransitiveErrors(/*keepGoing=*/false, /*explicitlyPropagateError=*/false); |
| } |
| |
| @Test |
| public void unhandledTransitiveErrorsDuringErrorBubbling_ExplicitPropagation() throws Exception { |
| runUnhandledTransitiveErrors(/*keepGoing=*/false, /*explicitlyPropagateError=*/true); |
| } |
| |
| @Test |
| public void unhandledTransitiveErrorsDuringNormalEvaluation_ImplicitPropagation() |
| throws Exception { |
| runUnhandledTransitiveErrors(/*keepGoing=*/true, /*explicitlyPropagateError=*/false); |
| } |
| |
| @Test |
| public void unhandledTransitiveErrorsDuringNormalEvaluation_ExplicitPropagation() |
| throws Exception { |
| runUnhandledTransitiveErrors(/*keepGoing=*/true, /*explicitlyPropagateError=*/true); |
| } |
| |
| private static class ChildKey extends AbstractSkyKey<String> { |
| private static final Interner<ChildKey> interner = BlazeInterners.newWeakInterner(); |
| |
| private ChildKey(String arg) { |
| super(arg); |
| } |
| |
| static ChildKey create(String arg) { |
| return interner.intern(new ChildKey(arg)); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return CHILD_TYPE; |
| } |
| } |
| |
| private static class ParentKey extends AbstractSkyKey<String> { |
| private static final Interner<ParentKey> interner = BlazeInterners.newWeakInterner(); |
| |
| private ParentKey(String arg) { |
| super(arg); |
| } |
| |
| private static ParentKey create(String arg) { |
| return interner.intern(new ParentKey(arg)); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return PARENT_TYPE; |
| } |
| } |
| } |