| // 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; | |
| } | |
| } | |
| } |