| // 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.base.Preconditions.checkNotNull; |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.collect.Iterables.getOnlyElement; |
| import static com.google.common.collect.Streams.stream; |
| 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 static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Splitter; |
| 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.testing.GcFinalization; |
| import com.google.common.util.concurrent.AtomicLongMap; |
| 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.bugreport.BugReporter; |
| import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor; |
| import com.google.devtools.build.lib.concurrent.BlazeInterners; |
| import com.google.devtools.build.lib.concurrent.QuiescingExecutor; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; |
| import com.google.devtools.build.lib.events.Reportable; |
| 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.EvaluationContext.UnnecessaryTemporaryStateDropper; |
| import com.google.devtools.build.skyframe.EvaluationContext.UnnecessaryTemporaryStateDropperReceiver; |
| import com.google.devtools.build.skyframe.GraphTester.SkipBatchPrefetchKey; |
| import com.google.devtools.build.skyframe.GraphTester.StringValue; |
| import com.google.devtools.build.skyframe.NotifyingHelper.EventType; |
| import com.google.devtools.build.skyframe.NotifyingHelper.Order; |
| import com.google.devtools.build.skyframe.PartialReevaluationMailbox.Kind; |
| import com.google.devtools.build.skyframe.PartialReevaluationMailbox.Mail; |
| import com.google.devtools.build.skyframe.SkyFunction.Environment.ClassToInstanceMapSkyKeyComputeState; |
| import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState; |
| import com.google.devtools.build.skyframe.SkyFunctionException.Transience; |
| import com.google.testing.junit.testparameterinjector.TestParameter; |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; |
| import java.io.IOException; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.LinkedBlockingQueue; |
| 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.concurrent.atomic.AtomicReference; |
| import javax.annotation.Nullable; |
| import org.junit.After; |
| import org.junit.Assume; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| /** Tests for {@link ParallelEvaluator}. */ |
| @RunWith(TestParameterInjector.class) |
| public class ParallelEvaluatorTest { |
| private static final SkyFunctionName CHILD_TYPE = SkyFunctionName.createHermetic("child"); |
| private static final SkyFunctionName PARENT_TYPE = SkyFunctionName.createHermetic("parent"); |
| |
| @TestParameter private boolean useQueryDep; |
| |
| /** |
| * If true, {@link #skyKey} creates the {@link SkipBatchPrefetchKey} so that {@link |
| * SkyFunctionEnvironment} is created and previously requested deps values are not batch |
| * prefetched. |
| */ |
| // TODO: b/324948927 - Remove this test parameter along with `SkyKey#skipBatchPrefetch()` method. |
| // Design another approach to cover scenarios when batch prefetch does and does not happen in |
| // `ParallelEvaluatorTest`. |
| @TestParameter private boolean useSkipBatchPrefetchKey; |
| |
| private SkyKey skyKey(String key) { |
| return useSkipBatchPrefetchKey |
| ? GraphTester.skipBatchPrefetchKey(key) |
| : GraphTester.skyKey(key); |
| } |
| |
| protected ProcessableGraph graph; |
| protected IntVersion graphVersion = IntVersion.of(0); |
| protected GraphTester tester = new GraphTester(); |
| |
| private final StoredEventHandler reportedEvents = new StoredEventHandler(); |
| |
| private DirtyAndInflightTrackingProgressReceiver revalidationReceiver = |
| new DirtyAndInflightTrackingProgressReceiver(EvaluationProgressReceiver.NULL); |
| |
| @Before |
| public void configureTesterUseLookup() { |
| tester.setUseQueryDep(useQueryDep); |
| } |
| |
| @After |
| public void assertNoTrackedErrors() { |
| TrackingAwaiter.INSTANCE.assertNoErrors(); |
| } |
| |
| private ParallelEvaluator makeEvaluator( |
| ProcessableGraph graph, |
| ImmutableMap<SkyFunctionName, SkyFunction> builders, |
| boolean keepGoing, |
| EventFilter storedEventFilter, |
| Version evaluationVersion) { |
| return new ParallelEvaluator( |
| graph, |
| evaluationVersion, |
| Version.minimal(), |
| builders, |
| reportedEvents, |
| new EmittedEventState(), |
| storedEventFilter, |
| ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, |
| keepGoing, |
| revalidationReceiver, |
| GraphInconsistencyReceiver.THROWING, |
| AbstractQueueVisitor.create("test-pool", 200, ParallelEvaluatorErrorClassifier.instance()), |
| new SimpleCycleDetector(), |
| UnnecessaryTemporaryStateDropperReceiver.NULL); |
| } |
| |
| private ParallelEvaluator makeEvaluator( |
| ProcessableGraph graph, |
| ImmutableMap<SkyFunctionName, SkyFunction> builders, |
| boolean keepGoing, |
| EventFilter storedEventFilter) { |
| Version oldGraphVersion = graphVersion; |
| graphVersion = graphVersion.next(); |
| return makeEvaluator(graph, builders, keepGoing, storedEventFilter, oldGraphVersion); |
| } |
| |
| private ParallelEvaluator makeEvaluator( |
| ProcessableGraph graph, |
| ImmutableMap<SkyFunctionName, SkyFunction> builders, |
| boolean keepGoing) { |
| return makeEvaluator(graph, builders, keepGoing, EventFilter.FULL_STORAGE); |
| } |
| |
| /** 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); |
| } |
| |
| private 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(skyKey(name), new StringValue(value)); |
| } |
| |
| @Test |
| public void smoke() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| set("b", "b"); |
| |
| SkyKey abKey = skyKey("ab"); |
| tester |
| .getOrCreate(abKey) |
| .addDependency(skyKey("a")) |
| .addDependency(skyKey("b")) |
| .setComputedValue(CONCATENATE); |
| StringValue value = (StringValue) eval(false, abKey); |
| assertThat(value.getValue()).isEqualTo("ab"); |
| assertThat(reportedEvents.getEvents()).isEmpty(); |
| assertThat(reportedEvents.getPosts()).isEmpty(); |
| } |
| |
| @Test |
| public void enqueueDoneFuture() throws Exception { |
| SkyKey parentKey = skyKey("parentKey"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| 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); |
| } |
| }); |
| 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 { |
| SkyKey parentKey = skyKey("parentKey"); |
| CountDownLatch doneLatch = new CountDownLatch(1); |
| 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) { |
| 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!"); |
| } |
| }); |
| graph = |
| NotifyingHelper.makeNotifyingTransformer( |
| (key, type, order, 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 { |
| SkyKey parentKey = skyKey("parentKey"); |
| SkyKey childKey = skyKey("childKey"); |
| 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!"); |
| } |
| }); |
| graph = |
| NotifyingHelper.makeNotifyingTransformer( |
| (key, type, order, 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( |
| (threadStarted, errorMessage) -> |
| (key, env) -> { |
| // 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; |
| }); |
| } |
| |
| /** |
| * 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( |
| (threadStarted, errorMessage) -> |
| 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(skyKey("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; |
| } |
| }); |
| } |
| |
| @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 = skyKey("a"); |
| SkyKey keyB = skyKey("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); |
| InMemoryNodeEntry nodeEntryB = spy(new IncrementalInMemoryNodeEntry(keyB)); |
| AtomicBoolean keyBAddReverseDepAndCheckIfDoneInterrupted = new AtomicBoolean(false); |
| 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(eq(null)); |
| graph = |
| new InMemoryGraphImpl() { |
| @Override |
| protected InMemoryNodeEntry 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( |
| (skyKey, env) -> { |
| keyAStartedComputingLatch.countDown(); |
| try { |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); |
| throw new IllegalStateException("shouldn't get here"); |
| } catch (InterruptedException e) { |
| keyAComputeInterrupted.set(true); |
| throw e; |
| } |
| }); |
| |
| // 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(); |
| } |
| |
| @Test |
| public void runPartialResultOnInterruption(@TestParameter boolean buildFastFirst) |
| throws Exception { |
| graph = new InMemoryGraphImpl(); |
| // Two runs for fastKey's builder and one for the start of waitKey's builder. |
| CountDownLatch allValuesReady = new CountDownLatch(3); |
| SkyKey waitKey = skyKey("wait"); |
| SkyKey fastKey = skyKey("fast"); |
| SkyKey leafKey = skyKey("leaf"); |
| tester |
| .getOrCreate(waitKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| allValuesReady.countDown(); |
| Thread.sleep(10000); |
| throw new AssertionError("Should have been interrupted"); |
| }); |
| 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); |
| } |
| Set<SkyKey> receivedValues = Sets.newConcurrentHashSet(); |
| revalidationReceiver = |
| new DirtyAndInflightTrackingProgressReceiver( |
| new EvaluationProgressReceiver() { |
| @Override |
| public void evaluated( |
| SkyKey skyKey, |
| EvaluationState state, |
| @Nullable SkyValue newValue, |
| @Nullable ErrorInfo newError, |
| @Nullable GroupedDeps directDeps) { |
| 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); |
| } |
| } |
| |
| /** 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(Semaphore threadStarted, 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 { |
| Semaphore threadStarted = new Semaphore(0); |
| Semaphore threadInterrupted = new Semaphore(0); |
| String[] wasError = new String[] {null}; |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| new InMemoryGraphImpl(), |
| ImmutableMap.of( |
| GraphTester.NODE_TYPE, valueBuilderFactory.create(threadStarted, wasError)), |
| false); |
| |
| Thread t = |
| new Thread( |
| () -> { |
| try { |
| evaluator.eval(ImmutableList.of(skyKey("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() { |
| class CustomRuntimeException extends RuntimeException {} |
| CustomRuntimeException expected = new CustomRuntimeException(); |
| |
| SkyFunction builder = |
| new SkyFunction() { |
| @Override |
| @Nullable |
| public SkyValue compute(SkyKey skyKey, Environment env) { |
| throw expected; |
| } |
| }; |
| |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| new InMemoryGraphImpl(), ImmutableMap.of(GraphTester.NODE_TYPE, builder), false); |
| |
| SkyKey valueToEval = skyKey("a"); |
| RuntimeException re = |
| assertThrows(RuntimeException.class, () -> evaluator.eval(ImmutableList.of(valueToEval))); |
| assertThat(re) |
| .hasMessageThat() |
| .contains("Unrecoverable error while evaluating node '" + valueToEval + "'"); |
| 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, skyKey("a")); |
| assertThat(value.getValue()).isEqualTo("a"); |
| assertThatEvents(reportedEvents.getEvents()).containsExactly("warning on 'a'"); |
| } |
| |
| @Test |
| public void errorOfTopLevelTargetReported() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey a = skyKey("a"); |
| SkyKey b = skyKey("b"); |
| tester.getOrCreate(b).setHasError(true); |
| Event errorEvent = Event.error("foobar"); |
| tester |
| .getOrCreate(a) |
| .setBuilder( |
| (key, env) -> { |
| 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) {}; |
| }); |
| eval(false, a); |
| assertThat(reportedEvents.getEvents()).containsExactly(errorEvent); |
| } |
| |
| private static final class ExamplePost implements Postable { |
| private final boolean storeForReplay; |
| |
| ExamplePost(boolean storeForReplay) { |
| this.storeForReplay = storeForReplay; |
| } |
| |
| @Override |
| public boolean storeForReplay() { |
| return storeForReplay; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this).add("storeForReplay", storeForReplay).toString(); |
| } |
| } |
| |
| private enum SkyframeEventType { |
| EVENT { |
| @Override |
| Event createUnstored() { |
| return Event.progress("analyzing"); |
| } |
| |
| @Override |
| Reportable createStored() { |
| return Event.error("broken"); |
| } |
| |
| @Override |
| ImmutableList<Event> getResults(StoredEventHandler reportedEvents) { |
| return reportedEvents.getEvents(); |
| } |
| }, |
| POST { |
| @Override |
| Postable createUnstored() { |
| return new ExamplePost(false); |
| } |
| |
| @Override |
| Postable createStored() { |
| return new ExamplePost(true); |
| } |
| |
| @Override |
| ImmutableList<Postable> getResults(StoredEventHandler reportedEvents) { |
| return reportedEvents.getPosts(); |
| } |
| }; |
| |
| abstract Reportable createUnstored(); |
| |
| abstract Reportable createStored(); |
| |
| abstract ImmutableList<? extends Reportable> getResults(StoredEventHandler reportedEvents); |
| } |
| |
| @Test |
| public void fullEventStorage_unstoredEvent_reportedImmediately_notReplayed( |
| @TestParameter SkyframeEventType eventType) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey key = skyKey("key"); |
| AtomicBoolean evaluated = new AtomicBoolean(false); |
| Reportable unstoredEvent = eventType.createUnstored(); |
| assertThat(unstoredEvent.storeForReplay()).isFalse(); |
| tester |
| .getOrCreate(key) |
| .setBuilder( |
| (skyKey, env) -> { |
| evaluated.set(true); |
| unstoredEvent.reportTo(env.getListener()); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(unstoredEvent); |
| return new StringValue("value"); |
| }); |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.FULL_STORAGE); |
| evaluator.eval(ImmutableList.of(key)); |
| assertThat(evaluated.get()).isTrue(); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(unstoredEvent); |
| |
| reportedEvents.clear(); |
| evaluated.set(false); |
| |
| evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.FULL_STORAGE); |
| evaluator.eval(ImmutableList.of(key)); |
| assertThat(evaluated.get()).isFalse(); |
| assertThat(eventType.getResults(reportedEvents)).isEmpty(); |
| } |
| |
| @Test |
| public void fullEventStorage_storedEvent_reportedAfterSkyFunctionCompletes_replayed( |
| @TestParameter SkyframeEventType eventType) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey top = skyKey("top"); |
| SkyKey mid = skyKey("mid"); |
| SkyKey bottom = skyKey("bottom"); |
| String tag = "this is the tag"; |
| AtomicBoolean evaluatedMid = new AtomicBoolean(false); |
| Reportable storedEvent = eventType.createStored(); |
| assertThat(storedEvent.storeForReplay()).isTrue(); |
| Reportable taggedEvent = storedEvent.withTag(tag); |
| tester |
| .getOrCreate(top) |
| .setBuilder( |
| (skyKey, env) -> { |
| SkyValue midValue = env.getValue(mid); |
| if (midValue == null) { |
| return null; |
| } |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(taggedEvent); |
| return new StringValue("topValue"); |
| }); |
| tester |
| .getOrCreate(mid) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| evaluatedMid.set(true); |
| storedEvent.reportTo(env.getListener()); |
| assertThat(eventType.getResults(reportedEvents)).isEmpty(); |
| SkyValue bottomValue = env.getValue(bottom); |
| if (bottomValue == null) { |
| return null; |
| } |
| return new StringValue("midValue"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| assertThat(skyKey).isEqualTo(mid); |
| return tag; |
| } |
| }); |
| tester.getOrCreate(bottom).setConstantValue(new StringValue("depValue")); |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.FULL_STORAGE); |
| evaluator.eval(ImmutableList.of(top)); |
| assertThat(evaluatedMid.get()).isTrue(); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(taggedEvent); |
| |
| reportedEvents.clear(); |
| evaluatedMid.set(false); |
| |
| evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.FULL_STORAGE); |
| evaluator.eval(ImmutableList.of(top)); |
| assertThat(evaluatedMid.get()).isFalse(); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(taggedEvent); |
| } |
| |
| @Test |
| public void noEventStorage_unstoredEvent_reportedImmediately_notReplayed( |
| @TestParameter SkyframeEventType eventType) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey key = skyKey("key"); |
| AtomicBoolean evaluated = new AtomicBoolean(false); |
| Reportable unstoredEvent = eventType.createUnstored(); |
| assertThat(unstoredEvent.storeForReplay()).isFalse(); |
| tester |
| .getOrCreate(key) |
| .setBuilder( |
| (skyKey, env) -> { |
| evaluated.set(true); |
| unstoredEvent.reportTo(env.getListener()); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(unstoredEvent); |
| return new StringValue("value"); |
| }); |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.NO_STORAGE); |
| evaluator.eval(ImmutableList.of(key)); |
| assertThat(evaluated.get()).isTrue(); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(unstoredEvent); |
| |
| reportedEvents.clear(); |
| evaluated.set(false); |
| |
| evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.NO_STORAGE); |
| evaluator.eval(ImmutableList.of(key)); |
| assertThat(evaluated.get()).isFalse(); |
| assertThat(eventType.getResults(reportedEvents)).isEmpty(); |
| } |
| |
| @Test |
| public void noEventStorage_storedEvent_reportedAfterSkyFunctionCompletes_notReplayed( |
| @TestParameter SkyframeEventType eventType) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey top = skyKey("top"); |
| SkyKey mid = skyKey("mid"); |
| SkyKey bottom = skyKey("bottom"); |
| String tag = "this is the tag"; |
| AtomicBoolean evaluatedMid = new AtomicBoolean(false); |
| Reportable storedEvent = eventType.createStored(); |
| assertThat(storedEvent.storeForReplay()).isTrue(); |
| Reportable taggedEvent = storedEvent.withTag(tag); |
| tester |
| .getOrCreate(top) |
| .setBuilder( |
| (skyKey, env) -> { |
| SkyValue midValue = env.getValue(mid); |
| if (midValue == null) { |
| return null; |
| } |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(taggedEvent); |
| return new StringValue("topValue"); |
| }); |
| tester |
| .getOrCreate(mid) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| evaluatedMid.set(true); |
| storedEvent.reportTo(env.getListener()); |
| assertThat(eventType.getResults(reportedEvents)).isEmpty(); |
| SkyValue bottomValue = env.getValue(bottom); |
| if (bottomValue == null) { |
| return null; |
| } |
| return new StringValue("midValue"); |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| assertThat(skyKey).isEqualTo(mid); |
| return tag; |
| } |
| }); |
| tester.getOrCreate(bottom).setConstantValue(new StringValue("depValue")); |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.NO_STORAGE); |
| evaluator.eval(ImmutableList.of(top)); |
| assertThat(evaluatedMid.get()).isTrue(); |
| assertThat(eventType.getResults(reportedEvents)).containsExactly(taggedEvent); |
| |
| reportedEvents.clear(); |
| evaluatedMid.set(false); |
| |
| evaluator = |
| makeEvaluator( |
| graph, tester.getSkyFunctionMap(), /* keepGoing= */ false, EventFilter.NO_STORAGE); |
| evaluator.eval(ImmutableList.of(top)); |
| assertThat(evaluatedMid.get()).isFalse(); |
| assertThat(eventType.getResults(reportedEvents)).isEmpty(); |
| } |
| |
| @Test |
| public void shouldCreateErrorValueWithRootCause() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| SkyKey parentErrorKey = skyKey("parent"); |
| SkyKey errorKey = skyKey("error"); |
| tester |
| .getOrCreate(parentErrorKey) |
| .addDependency(skyKey("a")) |
| .addDependency(errorKey) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(errorKey).setHasError(true); |
| evalValueInError(parentErrorKey); |
| } |
| |
| @Test |
| public void shouldBuildOneTarget() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| set("a", "a"); |
| set("b", "b"); |
| SkyKey parentErrorKey = skyKey("parent"); |
| SkyKey errorFreeKey = skyKey("ab"); |
| SkyKey errorKey = skyKey("error"); |
| tester |
| .getOrCreate(parentErrorKey) |
| .addDependency(errorKey) |
| .addDependency(skyKey("a")) |
| .setComputedValue(CONCATENATE); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester |
| .getOrCreate(errorFreeKey) |
| .addDependency(skyKey("a")) |
| .addDependency(skyKey("b")) |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(true, parentErrorKey, errorFreeKey); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(parentErrorKey); |
| assertThatEvaluationResult(result).hasEntryThat(errorFreeKey).isEqualTo(new StringValue("ab")); |
| } |
| |
| @Test |
| public void catastrophicBuild(@TestParameter boolean keepGoing, @TestParameter boolean keepEdges) |
| throws Exception { |
| Assume.assumeTrue(keepGoing || keepEdges); |
| |
| graph = |
| keepEdges |
| ? InMemoryGraph.create(/* usePooledInterning= */ true) |
| : InMemoryGraph.createEdgeless(/* usePooledInterning= */ true); |
| |
| SkyKey catastropheKey = skyKey("catastrophe"); |
| SkyKey otherKey = skyKey("someKey"); |
| |
| 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; |
| } |
| }; |
| } |
| }); |
| |
| 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"); |
| } |
| }); |
| |
| SkyKey topKey = skyKey("top"); |
| tester.getOrCreate(topKey).addDependency(catastropheKey).setComputedValue(CONCATENATE); |
| |
| ParallelEvaluator evaluator = |
| makeEvaluator( |
| graph, |
| tester.getSkyFunctionMap(), |
| keepGoing, |
| EventFilter.FULL_STORAGE, |
| keepEdges ? graphVersion : Version.constant()); |
| |
| EvaluationResult<StringValue> result = evaluator.eval(ImmutableList.of(topKey, otherKey)); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(topKey); |
| if (keepGoing) { |
| assertThat(result.getCatastrophe()).isSameInstanceAs(catastrophe); |
| } |
| } |
| |
| @Test |
| public void topCatastrophe() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey catastropheKey = skyKey("catastrophe"); |
| 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; |
| } |
| }; |
| } |
| }); |
| |
| EvaluationResult<StringValue> result = |
| eval(/* keepGoing= */ true, ImmutableList.of(catastropheKey)); |
| assertThat(result.getCatastrophe()).isEqualTo(catastrophe); |
| } |
| |
| @Test |
| public void catastropheBubblesIntoNonCatastrophe() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey catastropheKey = skyKey("catastrophe"); |
| 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; |
| } |
| }; |
| } |
| }); |
| SkyKey topKey = skyKey("top"); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| try { |
| env.getValueOrThrow(catastropheKey, SomeErrorException.class); |
| } catch (SomeErrorException e) { |
| throw new SkyFunctionException( |
| new SomeErrorException("We got: " + e.getMessage()), |
| Transience.PERSISTENT) {}; |
| } |
| return null; |
| } |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(topKey)); |
| |
| assertThat(result.getError(topKey).getException()).isInstanceOf(SomeErrorException.class); |
| assertThat(result.getError(topKey).getException()).hasMessageThat().isEqualTo("We got: bad"); |
| assertThat(result.getCatastrophe()).isNotNull(); |
| } |
| |
| @Test |
| public void incrementalCycleWithCatastropheAndFailedBubbleUp() throws Exception { |
| SkyKey topKey = skyKey("top"); |
| // Comes alphabetically before "top". |
| SkyKey cycleKey = skyKey("cycle"); |
| SkyKey catastropheKey = skyKey("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.getValue(cycleKey); |
| return env.valuesMissing() ? null : topValue; |
| } |
| }); |
| tester |
| .getOrCreate(cycleKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException { |
| env.getValuesAndExceptions(ImmutableList.of(cycleKey, catastropheKey)); |
| Preconditions.checkState(env.valuesMissing()); |
| 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; |
| } |
| }; |
| } |
| }); |
| 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 = skyKey("parent"); |
| tester.getOrCreate(parentKey).setHasError(true); |
| SkyKey childKey = skyKey("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. |
| assertThatEvaluationResult(result).hasEntryThat(childKey).isEqualTo(new StringValue("onions")); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(parentKey); |
| } |
| |
| @Test |
| public void newParentOfErrorShouldHaveError() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| evalValueInError(errorKey); |
| SkyKey parentKey = skyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency(errorKey).setComputedValue(CONCATENATE); |
| evalValueInError(parentKey); |
| } |
| |
| @Test |
| public void errorTwoLevelsDeep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey errorKey = skyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate("mid").addDependency(errorKey).setComputedValue(CONCATENATE); |
| tester.getOrCreate(parentKey).addDependency("mid").setComputedValue(CONCATENATE); |
| evalValueInError(parentKey); |
| } |
| |
| @Test |
| public void valueNotUsedInFailFastErrorRecovery() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = skyKey("top"); |
| SkyKey recoveryKey = skyKey("midRecovery"); |
| SkyKey badKey = skyKey("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 = skyKey("parent"); |
| SkyKey errorKey = skyKey("error"); |
| SkyKey errorKey2 = skyKey("error2"); |
| SkyKey errorKey3 = skyKey("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); |
| evalValueInError(parentKey); |
| } |
| |
| @Test |
| public void rootCauseWithNoKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey errorKey = skyKey("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)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(parentKey); |
| } |
| |
| @Test |
| public void errorBubblesToParentsOfTopLevelValue() throws Exception { |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey errorKey = skyKey("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.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 = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| SkyKey parentKey = skyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency(errorKey); |
| evalValueInError(parentKey); |
| SkyKey[] list = {parentKey}; |
| EvaluationResult<StringValue> result = eval(false, list); |
| assertThatEvaluationResult(result) |
| .hasSingletonErrorThat(parentKey) |
| .hasExceptionThat() |
| .hasMessageThat() |
| .isEqualTo(errorKey.toString()); |
| } |
| |
| @Test |
| public void twoErrors() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey firstError = skyKey("error1"); |
| SkyKey secondError = skyKey("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.of())); |
| tester |
| .getOrCreate(secondError) |
| .setBuilder( |
| new ChainedFunction( |
| secondStart, |
| firstStart, |
| /* notifyFinish= */ null, |
| /* waitForException= */ false, |
| /* value= */ null, |
| ImmutableList.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 = skyKey("a"); |
| SkyKey bKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey topKey = skyKey("top"); |
| SkyKey midKey = skyKey("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 = skyKey("a"); |
| SkyKey topKey = skyKey("top"); |
| SkyKey midKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey topKey = skyKey("top"); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey goodKey = skyKey("good"); |
| StringValue goodValue = new StringValue("good"); |
| tester.set(goodKey, goodValue); |
| tester.getOrCreate(topKey).addDependency(midKey); |
| tester.getOrCreate(midKey).addDependency(aKey); |
| tester.getOrCreate(aKey).addDependency(bKey); |
| tester.getOrCreate(bKey).addDependency(aKey); |
| EvaluationResult<StringValue> result = 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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey dKey = skyKey("d"); |
| SkyKey topKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey dKey = skyKey("d"); |
| SkyKey topKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey topKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey topKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey topKey = skyKey("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 = skyKey("a"); |
| SkyKey zKey = skyKey("z"); |
| SkyKey cKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("c"); |
| SkyKey dKey = skyKey("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 = skyKey("a"); |
| SkyKey bKey = skyKey("b"); |
| SkyKey cKey = skyKey("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 = skyKey("a"); |
| SkyKey errorKey = skyKey("error"); |
| SkyKey bKey = skyKey("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 = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(false, ImmutableList.of(errorKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(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)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(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 = skyKey("top"); |
| for (int i = 0; i < 100; i++) { |
| SkyKey dep = skyKey(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 = skyKey("top"); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey cycleKey = skyKey("cycle"); |
| tester.getOrCreate(topKey).addDependency(midKey); |
| tester.getOrCreate(cycleKey).addDependency(cycleKey); |
| for (int i = 0; i < 100; i++) { |
| SkyKey dep = skyKey(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 = skyKey("zlastSelf"); |
| SkyKey firstSelfKey = skyKey("afirstSelf"); |
| SkyKey midSelfKey = skyKey("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 = skyKey("first" + i); |
| SkyKey midDep = skyKey("midSelf" + i + "dep"); |
| SkyKey lastDep = skyKey("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 = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(true, ImmutableList.of(errorKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(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)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(errorKey); |
| } |
| |
| @Test |
| public void continueWithErrorDep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentKey = skyKey("parent"); |
| tester |
| .getOrCreate(parentKey) |
| .addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE) |
| .addDependency("after"); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThat(result.errorMap()).isEmpty(); |
| assertThat(result.get(parentKey).getValue()).isEqualTo("recoveredafter"); |
| result = eval(/* keepGoing= */ false, ImmutableList.of(parentKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(parentKey); |
| } |
| |
| @Test |
| public void transformErrorDep(@TestParameter boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| SkyKey parentErrorKey = skyKey("parent"); |
| tester |
| .getOrCreate(parentErrorKey) |
| .addErrorDependency(errorKey, new StringValue("recovered")) |
| .setHasError(true); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(parentErrorKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(parentErrorKey); |
| } |
| |
| @Test |
| public void transformErrorDepOneLevelDownKeepGoing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentErrorKey = skyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); |
| tester.set(parentErrorKey, new StringValue("parent value")); |
| SkyKey topKey = skyKey("top"); |
| tester |
| .getOrCreate(topKey) |
| .addDependency(parentErrorKey) |
| .addDependency("after") |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(topKey)); |
| assertThat(ImmutableList.<String>copyOf(result.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 = skyKey("my_error_value"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.set("after", new StringValue("after")); |
| SkyKey parentErrorKey = skyKey("parent"); |
| tester.getOrCreate(parentErrorKey).addErrorDependency(errorKey, new StringValue("recovered")); |
| tester.set(parentErrorKey, new StringValue("parent value")); |
| SkyKey topKey = skyKey("top"); |
| tester |
| .getOrCreate(topKey) |
| .addDependency(parentErrorKey) |
| .addDependency("after") |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ false, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(topKey); |
| } |
| |
| @Test |
| public void errorDepDoesntStopOtherDep() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result1 = eval(/* keepGoing= */ true, ImmutableList.of(errorKey)); |
| assertThatEvaluationResult(result1).hasError(); |
| assertThatEvaluationResult(result1) |
| .hasErrorEntryForKeyThat(errorKey) |
| .hasExceptionThat() |
| .isNotNull(); |
| SkyKey otherKey = skyKey("other"); |
| tester.getOrCreate(otherKey).setConstantValue(new StringValue("other")); |
| SkyKey topKey = skyKey("top"); |
| Exception topException = new SomeErrorException("top exception"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| SkyframeLookupResult values = |
| env.getValuesAndExceptions(ImmutableList.of(errorKey, otherKey)); |
| if (numComputes.incrementAndGet() == 1) { |
| assertThat(env.valuesMissing()).isTrue(); |
| } else { |
| assertThat(numComputes.get()).isEqualTo(2); |
| assertThat(env.valuesMissing()).isFalse(); |
| } |
| try { |
| values.getOrThrow(errorKey, SomeErrorException.class); |
| throw new AssertionError("Should have thrown"); |
| } catch (SomeErrorException e) { |
| throw new SkyFunctionException(topException, Transience.PERSISTENT) {}; |
| } |
| } |
| }); |
| EvaluationResult<StringValue> result2 = eval(/* keepGoing= */ true, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result2).hasError(); |
| assertThatEvaluationResult(result2) |
| .hasErrorEntryForKeyThat(topKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(topException); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| private SkyKey createCycleKey(String keyName, boolean isCycleNodePartialReevaluation) { |
| if (isCycleNodePartialReevaluation) { |
| tester.putDelegateFunction(PartialReevaluationKey.FUNCTION_NAME); |
| return new PartialReevaluationKey(keyName); |
| } |
| return skyKey(keyName); |
| } |
| |
| /** Make sure that multiple unfinished children can be cleared from a cycle value. */ |
| @Test |
| public void cycleWithMultipleUnfinishedChildren( |
| @TestParameter boolean isCycleNodePartialReevaluation) throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| SkyKey cycleKey = createCycleKey("cycle", isCycleNodePartialReevaluation); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey topKey = skyKey("top"); |
| SkyKey selfEdge1 = skyKey("selfEdge1"); |
| SkyKey selfEdge2 = skyKey("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. |
| */ |
| @Test |
| public void cycleAndErrorInBubbleUp( |
| @TestParameter boolean keepGoing, @TestParameter boolean isCycleNodePartialReevaluation) |
| throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = skyKey("error"); |
| SkyKey cycleKey = createCycleKey("cycle", isCycleNodePartialReevaluation); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey topKey = skyKey("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.of(midKey))); |
| tester |
| .getOrCreate(errorKey) |
| .setBuilder( |
| new ChainedFunction( |
| null, cycleFinish, null, /* waitForException= */ false, null, ImmutableSet.of())); |
| |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableSet.of(topKey)); |
| assertThatEvaluationResult(result) |
| .hasSingletonErrorThat(topKey) |
| .hasCycleInfoThat() |
| .containsExactly( |
| new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(midKey, cycleKey))); |
| } |
| |
| /** |
| * 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 = skyKey("error"); |
| SkyKey cycleKey = skyKey("cycle"); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey topKey = skyKey("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 = skyKey("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.of(skyKey("dep that never builds")))); |
| |
| tester |
| .getOrCreate(cycleKey) |
| .setBuilder( |
| new ChainedFunction( |
| null, |
| null, |
| topStartAndCycleFinish, |
| /* waitForException= */ false, |
| new StringValue(""), |
| ImmutableSet.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.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. |
| */ |
| @Test |
| public void cycleAndErrorAndError( |
| @TestParameter boolean keepGoing, @TestParameter boolean isCycleNodePartialReevaluation) |
| throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = skyKey("error"); |
| SkyKey cycleKey = createCycleKey("cycle", isCycleNodePartialReevaluation); |
| SkyKey midKey = skyKey("mid"); |
| SkyKey topKey = skyKey("top"); |
| tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE); |
| tester |
| .getOrCreate(midKey) |
| .addDependency(errorKey) |
| .addDependency(cycleKey) |
| .setComputedValue(CONCATENATE); |
| SkyKey otherTop = skyKey("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.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.of())); |
| tester |
| .getOrCreate(cycleKey) |
| .setBuilder( |
| new ChainedFunction( |
| null, |
| null, |
| topStartAndCycleFinish, |
| /* waitForException= */ false, |
| new StringValue(""), |
| ImmutableSet.of(midKey))); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableSet.of(topKey, otherTop)); |
| if (keepGoing) { |
| assertThatEvaluationResult(result).hasErrorMapThat().hasSize(2); |
| } |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(topKey) |
| .hasCycleInfoThat() |
| .containsExactly( |
| new CycleInfo(ImmutableList.of(topKey), ImmutableList.of(midKey, cycleKey))); |
| } |
| |
| @Test |
| public void testFunctionCrashTrace() { |
| |
| class ChildFunction implements SkyFunction { |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) { |
| throw new IllegalStateException("I WANT A PONY!!!"); |
| } |
| } |
| |
| 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. |
| } |
| } |
| |
| 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 final class SomeOtherErrorException extends Exception { |
| SomeOtherErrorException(String msg) { |
| super(msg); |
| } |
| } |
| |
| /** |
| * This and the following tests are in response to 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 unexpectedErrorDep(@TestParameter boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("my_error_value"); |
| SomeOtherErrorException exception = new SomeOtherErrorException("error exception"); |
| tester |
| .getOrCreate(errorKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new SkyFunctionException(exception, Transience.PERSISTENT) {}; |
| }); |
| SkyKey topKey = skyKey("top"); |
| tester |
| .getOrCreate(topKey) |
| .addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(topKey); |
| } |
| |
| @Test |
| public void unexpectedErrorDepOneLevelDown(@TestParameter boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey errorKey = skyKey("my_error_value"); |
| SomeErrorException exception = new SomeErrorException("error exception"); |
| SomeErrorException topException = new SomeErrorException("top exception"); |
| StringValue topValue = new StringValue("top"); |
| tester |
| .getOrCreate(errorKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new GenericFunctionException(exception, Transience.PERSISTENT); |
| }); |
| SkyKey topKey = skyKey("top"); |
| SkyKey parentKey = skyKey("parent"); |
| tester.getOrCreate(parentKey).addDependency(errorKey).setComputedValue(CONCATENATE); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| 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); |
| } |
| }); |
| tester |
| .getOrCreate(topKey) |
| .addErrorDependency(errorKey, new StringValue("recovered")) |
| .setComputedValue(CONCATENATE); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(topKey)); |
| if (!keepGoing) { |
| assertThatEvaluationResult(result).hasSingletonErrorThat(topKey); |
| } else { |
| assertThatEvaluationResult(result).hasNoError(); |
| assertThatEvaluationResult(result).hasEntryThat(topKey).isSameInstanceAs(topValue); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @Test |
| public void sameDepInTwoGroups(@TestParameter boolean sameFirst, @TestParameter boolean twoCalls) |
| throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey topKey = skyKey("top"); |
| List<SkyKey> leaves = new ArrayList<>(); |
| for (int i = 1; i <= 3; i++) { |
| SkyKey leaf = skyKey("leaf" + i); |
| leaves.add(leaf); |
| tester.set(leaf, new StringValue("leaf" + i)); |
| } |
| SkyKey leaf4 = skyKey("leaf4"); |
| tester.set(leaf4, new StringValue("leaf" + 4)); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| env.getValuesAndExceptions(leaves); |
| if (twoCalls && env.valuesMissing()) { |
| return null; |
| } |
| SkyKey first = sameFirst ? leaves.get(0) : leaf4; |
| SkyKey second = sameFirst ? leaf4 : leaves.get(2); |
| ImmutableList<SkyKey> secondRequest = ImmutableList.of(first, second); |
| env.getValuesAndExceptions(secondRequest); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| return new StringValue("top"); |
| }); |
| eval(/* keepGoing= */ false, topKey); |
| assertThat(eval(/* keepGoing= */ false, topKey)).isEqualTo(new StringValue("top")); |
| } |
| |
| @Test |
| public void getValueOrThrowWithErrors(@TestParameter boolean keepGoing) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey errorDep = skyKey("errorChild"); |
| SomeErrorException childExn = new SomeErrorException("child error"); |
| tester |
| .getOrCreate(errorDep) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new GenericFunctionException(childExn, Transience.PERSISTENT); |
| }); |
| List<SkyKey> deps = new ArrayList<>(); |
| for (int i = 1; i <= 3; i++) { |
| SkyKey dep = skyKey("child" + i); |
| deps.add(dep); |
| tester.set(dep, new StringValue("child" + i)); |
| } |
| SomeErrorException parentExn = new SomeErrorException("parent error"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| try { |
| SkyValue value = env.getValueOrThrow(errorDep, SomeErrorException.class); |
| if (value == null) { |
| return null; |
| } |
| } catch (SomeErrorException e) { |
| // Recover from the child error. |
| } |
| env.getValuesAndExceptions(deps); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| throw new GenericFunctionException(parentExn, Transience.PERSISTENT); |
| }); |
| EvaluationResult<StringValue> evaluationResult = eval(keepGoing, ImmutableList.of(parentKey)); |
| assertThat(evaluationResult.hasError()).isTrue(); |
| assertThat(evaluationResult.getError().getException()) |
| .isEqualTo(keepGoing ? parentExn : childExn); |
| } |
| |
| @Test |
| public void getValuesAndExceptions() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey otherKey = skyKey("other"); |
| SkyKey anotherKey = skyKey("another"); |
| SkyKey errorExpectedKey = skyKey("errorExpected"); |
| SkyKey topKey = skyKey("top"); |
| Exception topException = new SomeErrorException("top exception"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| |
| tester.set(otherKey, new StringValue("other")); |
| tester.set(anotherKey, new StringValue("another")); |
| tester.getOrCreate(errorExpectedKey).setHasError(true); |
| tester |
| .getOrCreate(topKey) |
| .setBuilder( |
| new SkyFunction() { |
| @Nullable |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws SkyFunctionException, InterruptedException { |
| ImmutableList<SkyKey> depKeys = |
| ImmutableList.of(otherKey, anotherKey, errorExpectedKey); |
| SkyframeLookupResult skyframeLookupResult = env.getValuesAndExceptions(depKeys); |
| if (numComputes.incrementAndGet() == 1) { |
| assertThat(env.valuesMissing()).isTrue(); |
| for (SkyKey depKey : depKeys.reverse()) { |
| try { |
| assertThat(skyframeLookupResult.getOrThrow(depKey, SomeErrorException.class)) |
| .isNull(); |
| } catch (SomeErrorException e) { |
| throw new AssertionError("should not have thrown", e); |
| } |
| } |
| return null; |
| } else { |
| assertThat(numComputes.get()).isEqualTo(2); |
| SkyValue value1 = skyframeLookupResult.get(otherKey); |
| assertThat(value1).isNotNull(); |
| assertThat(env.valuesMissing()).isFalse(); |
| try { |
| SkyValue value2 = |
| skyframeLookupResult.getOrThrow(anotherKey, SomeErrorException.class); |
| assertThat(value2).isNotNull(); |
| assertThat(env.valuesMissing()).isFalse(); |
| } catch (SomeErrorException e) { |
| throw new AssertionError("Should not have thrown", e); |
| } |
| try { |
| skyframeLookupResult.getOrThrow(errorExpectedKey, SomeErrorException.class); |
| throw new AssertionError("Should throw"); |
| } catch (SomeErrorException e) { |
| assertThat(env.valuesMissing()).isFalse(); |
| } |
| throw new SkyFunctionException(topException, Transience.PERSISTENT) {}; |
| } |
| } |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(topKey)); |
| |
| assertThatEvaluationResult(result).hasError(); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(topKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(topException); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| @Test |
| public void getValuesAndExceptionsWithErrors() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey childKey = skyKey("error"); |
| SomeErrorException childExn = new SomeErrorException("child error"); |
| tester |
| .getOrCreate(childKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new GenericFunctionException(childExn, Transience.PERSISTENT); |
| }); |
| SkyKey parentKey = skyKey("parent"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| try { |
| SkyValue value = |
| env.getValuesAndExceptions(ImmutableList.of(childKey)) |
| .getOrThrow(childKey, SomeOtherErrorException.class); |
| assertThat(value).isNull(); |
| } catch (SomeOtherErrorException e) { |
| throw new AssertionError("Should not have thrown", e); |
| } |
| numComputes.incrementAndGet(); |
| assertThat(env.valuesMissing()).isTrue(); |
| return null; |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThatEvaluationResult(result).hasError(); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(parentKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(childExn); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| @Test |
| public void declareDependenciesAndCheckIfValuesMissing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey childKey = skyKey("error"); |
| SomeErrorException childExn = new SomeErrorException("child error"); |
| tester |
| .getOrCreate(childKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new GenericFunctionException(childExn, Transience.PERSISTENT); |
| }); |
| SkyKey parentKey = skyKey("parent"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| BugReporter mockReporter = mock(BugReporter.class); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| boolean valuesMissing = |
| GraphTraversingHelper.declareDependenciesAndCheckIfValuesMissing( |
| env, |
| ImmutableList.of(childKey), |
| SomeOtherErrorException.class, |
| /* exceptionClass2= */ null, |
| mockReporter); |
| numComputes.incrementAndGet(); |
| assertThat(valuesMissing).isTrue(); |
| return null; |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| verify(mockReporter) |
| .logUnexpected("Value for: '%s' was missing, this should never happen", childKey); |
| verifyNoMoreInteractions(mockReporter); |
| assertThatEvaluationResult(result).hasError(); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(parentKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(childExn); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| @Test |
| public void declareDependenciesAndCheckIfNotValuesMissing() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey otherKey = skyKey("other"); |
| SkyKey childKey = skyKey("error"); |
| SomeErrorException childExn = new SomeErrorException("child error"); |
| tester.set(otherKey, new StringValue("other")); |
| tester |
| .getOrCreate(childKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| throw new GenericFunctionException(childExn, Transience.PERSISTENT); |
| }); |
| SkyKey parentKey = skyKey("parent"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| if (numComputes.incrementAndGet() == 1) { |
| boolean valuesMissing = |
| GraphTraversingHelper.declareDependenciesAndCheckIfValuesMissing( |
| env, ImmutableList.of(otherKey, childKey), SomeErrorException.class); |
| assertThat(valuesMissing).isTrue(); |
| } else { |
| boolean valuesMissing = |
| GraphTraversingHelper.declareDependenciesAndCheckIfValuesMissing( |
| env, ImmutableList.of(otherKey, childKey), SomeErrorException.class); |
| assertThat(valuesMissing).isFalse(); |
| } |
| return null; |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThatEvaluationResult(result).hasError(); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(parentKey) |
| .hasExceptionThat() |
| .isSameInstanceAs(childExn); |
| assertThat(numComputes.get()).isEqualTo(2); |
| } |
| |
| @Test |
| public void validateExceptionTypeInDifferentPosition( |
| @TestParameter({"0", "1", "2", "3"}) int exceptionIndex) throws Exception { |
| ImmutableList<Class<? extends Exception>> exceptions = |
| ImmutableList.of( |
| Exception.class, |
| SomeOtherErrorException.class, |
| IOException.class, |
| SomeErrorException.class); |
| graph = new InMemoryGraphImpl(); |
| SkyKey otherKey = skyKey("other"); |
| tester.set(otherKey, new StringValue("other")); |
| SkyKey parentKey = skyKey("parent"); |
| SomeErrorException parentExn = new SomeErrorException("parent error"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| IllegalStateException illegalStateException = |
| assertThrows( |
| IllegalStateException.class, |
| () -> |
| env.getValueOrThrow( |
| otherKey, |
| exceptions.get(exceptionIndex % 4), |
| exceptions.get((exceptionIndex + 1) % 4), |
| exceptions.get((exceptionIndex + 2) % 4), |
| exceptions.get((exceptionIndex + 3) % 4))); |
| assertThat(illegalStateException) |
| .hasMessageThat() |
| .contains("is a supertype of RuntimeException"); |
| assertThat(env.valuesMissing()).isFalse(); |
| throw new GenericFunctionException(parentExn, Transience.PERSISTENT); |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getException()).isEqualTo(parentExn); |
| } |
| |
| @Test |
| public void validateExceptionTypeWithDifferentException( |
| @TestParameter ExceptionOption exceptionOption) throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey otherKey = skyKey("other"); |
| tester.set(otherKey, new StringValue("other")); |
| SkyKey parentKey = skyKey("parent"); |
| SomeErrorException parentExn = new SomeErrorException("parent error"); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| IllegalStateException illegalStateException = |
| assertThrows( |
| IllegalStateException.class, |
| () -> env.getValueOrThrow(otherKey, exceptionOption.exceptionClass)); |
| assertThat(illegalStateException) |
| .hasMessageThat() |
| .contains(exceptionOption.errorMessage); |
| assertThat(env.valuesMissing()).isFalse(); |
| throw new GenericFunctionException(parentExn, Transience.PERSISTENT); |
| }); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isTrue(); |
| assertThat(result.getError().getException()).isEqualTo(parentExn); |
| } |
| |
| private enum ExceptionOption { |
| EXCEPTION(Exception.class, "is a supertype of RuntimeException"), |
| NULL_POINTER_EXCEPTION(NullPointerException.class, "is a subtype of RuntimeException"), |
| INTERRUPTED_EXCEPTION(InterruptedException.class, "is a subtype of InterruptedException"); |
| |
| final Class<? extends Exception> exceptionClass; |
| final String errorMessage; |
| |
| ExceptionOption(Class<? extends Exception> exceptionClass, String errorMessage) { |
| this.exceptionClass = exceptionClass; |
| this.errorMessage = errorMessage; |
| } |
| } |
| |
| @Test |
| public void duplicateCycles() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey grandparentKey = skyKey("grandparent"); |
| SkyKey parentKey1 = skyKey("parent1"); |
| SkyKey parentKey2 = skyKey("parent2"); |
| SkyKey loopKey1 = skyKey("loop1"); |
| SkyKey loopKey2 = skyKey("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<>(); |
| for (ImmutableList<SkyKey> cycle : cycles) { |
| if (!cycleDeduper.alreadySeen(cycle)) { |
| numUniqueCycles++; |
| } |
| } |
| assertThat(numUniqueCycles).isEqualTo(1); |
| } |
| |
| @Test |
| public void signalValueEnqueuedAndEvaluated() throws Exception { |
| Set<SkyKey> enqueuedValues = Sets.newConcurrentHashSet(); |
| Set<SkyKey> evaluatedValues = Sets.newConcurrentHashSet(); |
| EvaluationProgressReceiver progressReceiver = |
| new EvaluationProgressReceiver() { |
| @Override |
| public void enqueueing(SkyKey skyKey) { |
| enqueuedValues.add(skyKey); |
| } |
| |
| @Override |
| public void evaluated( |
| SkyKey skyKey, |
| EvaluationState state, |
| @Nullable SkyValue newValue, |
| @Nullable ErrorInfo newError, |
| @Nullable GroupedDeps directDeps) { |
| evaluatedValues.add(skyKey); |
| } |
| }; |
| |
| ExtendedEventHandler reporter = |
| new Reporter( |
| new EventBus(), |
| e -> { |
| throw new IllegalStateException(); |
| }); |
| |
| MemoizingEvaluator evaluator = |
| new InMemoryMemoizingEvaluator( |
| ImmutableMap.of(GraphTester.NODE_TYPE, tester.getFunction()), |
| new SequencedRecordingDifferencer(), |
| progressReceiver); |
| |
| tester |
| .getOrCreate(skyKey("top1")) |
| .setComputedValue(CONCATENATE) |
| .addDependency(skyKey("d1")) |
| .addDependency(skyKey("d2")); |
| tester.getOrCreate(skyKey("top2")).setComputedValue(CONCATENATE).addDependency(skyKey("d3")); |
| tester.getOrCreate(skyKey("top3")); |
| assertThat(enqueuedValues).isEmpty(); |
| assertThat(evaluatedValues).isEmpty(); |
| |
| tester.set(skyKey("d1"), new StringValue("1")); |
| tester.set(skyKey("d2"), new StringValue("2")); |
| tester.set(skyKey("d3"), new StringValue("3")); |
| |
| EvaluationContext evaluationContext = |
| EvaluationContext.newBuilder() |
| .setKeepGoing(false) |
| .setParallelism(200) |
| .setEventHandler(reporter) |
| .build(); |
| evaluator.evaluate(ImmutableList.of(skyKey("top1")), evaluationContext); |
| assertThat(enqueuedValues) |
| .containsExactlyElementsIn( |
| GraphTester.toSkyKeys(useSkipBatchPrefetchKey, "top1", "d1", "d2")); |
| assertThat(evaluatedValues) |
| .containsExactlyElementsIn( |
| GraphTester.toSkyKeys(useSkipBatchPrefetchKey, "top1", "d1", "d2")); |
| enqueuedValues.clear(); |
| evaluatedValues.clear(); |
| |
| evaluator.evaluate(ImmutableList.of(skyKey("top2")), evaluationContext); |
| assertThat(enqueuedValues) |
| .containsExactlyElementsIn(GraphTester.toSkyKeys(useSkipBatchPrefetchKey, "top2", "d3")); |
| assertThat(evaluatedValues) |
| .containsExactlyElementsIn(GraphTester.toSkyKeys(useSkipBatchPrefetchKey, "top2", "d3")); |
| enqueuedValues.clear(); |
| evaluatedValues.clear(); |
| |
| evaluator.evaluate(ImmutableList.of(skyKey("top1")), evaluationContext); |
| assertThat(enqueuedValues).isEmpty(); |
| assertThat(evaluatedValues) |
| .containsExactlyElementsIn(GraphTester.toSkyKeys(useSkipBatchPrefetchKey, "top1")); |
| } |
| |
| @Test |
| public void runDepOnErrorHaltsNoKeepGoingBuildEagerly( |
| @TestParameter boolean childErrorCached, @TestParameter boolean handleChildError) |
| throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey childKey = skyKey("child"); |
| tester.getOrCreate(childKey).setHasError(/* hasError= */ true); |
| // The parent should be built exactly twice: once during normal evaluation and once |
| // during error bubbling. |
| AtomicInteger numParentInvocations = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| 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.inErrorBubbling()) |
| .isFalse(); |
| return value; |
| } catch (SomeErrorException e) { |
| assertWithMessage("child error propagated during normal evaluation") |
| .that(env.inErrorBubbling()) |
| .isTrue(); |
| assertThat(invocations).isEqualTo(2); |
| return null; |
| } |
| } else { |
| if (invocations == 1) { |
| assertWithMessage("parent's first computation should be during normal evaluation") |
| .that(env.inErrorBubbling()) |
| .isFalse(); |
| return env.getValue(childKey); |
| } else { |
| assertThat(invocations).isEqualTo(2); |
| assertWithMessage("parent incorrectly re-computed during normal evaluation") |
| .that(env.inErrorBubbling()) |
| .isTrue(); |
| return env.getValue(childKey); |
| } |
| } |
| }); |
| 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); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(parentKey); |
| } |
| |
| @Test |
| public void raceConditionWithNoKeepGoingErrors_FutureError() throws Exception { |
| CountDownLatch errorCommitted = new CountDownLatch(1); |
| CountDownLatch otherStarted = new CountDownLatch(1); |
| CountDownLatch otherParentSignaled = new CountDownLatch(1); |
| SkyKey errorParentKey = skyKey("errorParentKey"); |
| SkyKey errorKey = skyKey("errorKey"); |
| SkyKey otherParentKey = skyKey("otherParentKey"); |
| SkyKey otherKey = skyKey("otherKey"); |
| AtomicInteger numOtherParentInvocations = new AtomicInteger(0); |
| AtomicInteger numErrorParentInvocations = new AtomicInteger(0); |
| tester |
| .getOrCreate(otherParentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| int invocations = numOtherParentInvocations.incrementAndGet(); |
| assertWithMessage("otherParentKey should not be restarted") |
| .that(invocations) |
| .isEqualTo(1); |
| return env.getValue(otherKey); |
| }); |
| tester |
| .getOrCreate(otherKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| otherStarted.countDown(); |
| TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( |
| errorCommitted, "error didn't get committed to the graph in time"); |
| return new StringValue("other"); |
| }); |
| tester |
| .getOrCreate(errorKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( |
| otherStarted, "other didn't start in time"); |
| throw new GenericFunctionException( |
| new SomeErrorException("error"), Transience.PERSISTENT); |
| }); |
| tester |
| .getOrCreate(errorParentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| 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.inErrorBubbling()).isFalse(); |
| fail("RACE CONDITION: errorParentKey was restarted!"); |
| return null; |
| } |
| } catch (SomeErrorException e) { |
| assertWithMessage("child error propagated during normal evaluation") |
| .that(env.inErrorBubbling()) |
| .isTrue(); |
| assertThat(invocations).isEqualTo(2); |
| return null; |
| } |
| }); |
| graph = |
| new NotifyingHelper.NotifyingProcessableGraph( |
| new InMemoryGraphImpl(), |
| (key, type, order, 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 ParallelEvaluator'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(); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(errorParentKey); |
| } |
| |
| @Test |
| public void cachedErrorsFromKeepGoingUsedOnNoKeepGoing() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = skyKey("error"); |
| SkyKey parent1Key = skyKey("parent1"); |
| SkyKey parent2Key = skyKey("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)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(parent1Key); |
| result = eval(/* keepGoing= */ false, ImmutableList.of(parent2Key)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(parent2Key); |
| } |
| |
| @Test |
| public void cachedTopLevelErrorsShouldHaltNoKeepGoingBuildEarly() throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey errorKey = skyKey("error"); |
| tester.getOrCreate(errorKey).setHasError(true); |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(errorKey)); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(errorKey); |
| SkyKey rogueKey = skyKey("rogue"); |
| tester |
| .getOrCreate(rogueKey) |
| .setBuilder( |
| (skyKey, 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; |
| }); |
| result = eval(/* keepGoing= */ false, ImmutableList.of(errorKey, rogueKey)); |
| assertThatEvaluationResult(result).hasErrorMapThat().hasSize(1); |
| assertThatEvaluationResult(result).hasErrorEntryForKeyThat(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 = skyKey("grandChild1"); |
| tester.getOrCreate(grandChild1Key).setConstantValue(new StringValue("grandChild1")); |
| SkyKey child1Key = skyKey("child1"); |
| tester |
| .getOrCreate(child1Key) |
| .addDependency(grandChild1Key) |
| .setConstantValue(new StringValue("child1")); |
| SkyKey grandChild2Key = skyKey("grandChild2"); |
| tester.getOrCreate(grandChild2Key).setConstantValue(new StringValue("grandChild2")); |
| SkyKey child2Key = skyKey("child2"); |
| tester.getOrCreate(child2Key).setConstantValue(new StringValue("child2")); |
| SkyKey parentKey = skyKey("parent"); |
| AtomicInteger numComputes = new AtomicInteger(0); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| 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(); |
| } |
| }); |
| 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!")); |
| } |
| |
| @Test |
| public void runUnhandledTransitiveErrors( |
| @TestParameter boolean keepGoing, @TestParameter boolean explicitlyPropagateError) |
| throws Exception { |
| graph = new DeterministicHelper.DeterministicProcessableGraph(new InMemoryGraphImpl()); |
| tester = new GraphTester(); |
| SkyKey grandparentKey = skyKey("grandparent"); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey childKey = skyKey("child"); |
| AtomicBoolean errorPropagated = new AtomicBoolean(false); |
| tester |
| .getOrCreate(grandparentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| try { |
| return env.getValueOrThrow(parentKey, SomeErrorException.class); |
| } catch (SomeErrorException e) { |
| errorPropagated.set(true); |
| throw new GenericFunctionException(e, Transience.PERSISTENT); |
| } |
| }); |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (skyKey, env) -> { |
| if (explicitlyPropagateError) { |
| try { |
| return env.getValueOrThrow(childKey, SomeErrorException.class); |
| } catch (SomeErrorException e) { |
| throw new GenericFunctionException(e); |
| } |
| } else { |
| return env.getValue(childKey); |
| } |
| }); |
| tester.getOrCreate(childKey).setHasError(/* hasError= */ true); |
| EvaluationResult<StringValue> result = eval(keepGoing, ImmutableList.of(grandparentKey)); |
| assertThat(errorPropagated.get()).isTrue(); |
| assertThatEvaluationResult(result).hasSingletonErrorThat(grandparentKey); |
| } |
| |
| 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; |
| } |
| } |
| |
| private static class SkyKeyForSkyKeyComputeStateTests extends AbstractSkyKey<String> { |
| private static final SkyFunctionName FUNCTION_NAME = |
| SkyFunctionName.createHermetic("SKY_KEY_COMPUTE_STATE_TESTS"); |
| |
| private SkyKeyForSkyKeyComputeStateTests(String arg) { |
| super(arg); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return FUNCTION_NAME; |
| } |
| } |
| |
| // Test for the basic functionality of SkyKeyComputeState. |
| @Test |
| public void skyKeyComputeState() throws InterruptedException { |
| // When we have 3 nodes: key1, key2, key3. |
| // (with dependency graph key1 -> key2; key2 -> key3, to be declared later in this test) |
| // (and we'll be evaluating key1 later in this test) |
| SkyKey key1 = new SkyKeyForSkyKeyComputeStateTests("key1"); |
| SkyKey key2 = new SkyKeyForSkyKeyComputeStateTests("key2"); |
| SkyKey key3 = new SkyKeyForSkyKeyComputeStateTests("key3"); |
| |
| // And an SkyKeyComputeState implementation that tracks global instance counts and per-instance |
| // usage counts, |
| AtomicInteger globalStateInstanceCounter = new AtomicInteger(); |
| class State implements SkyKeyComputeState { |
| final int instanceCount = globalStateInstanceCounter.incrementAndGet(); |
| int usageCount = 0; |
| } |
| |
| // And a SkyFunction for these nodes, |
| AtomicLongMap<SkyKey> numCalls = AtomicLongMap.create(); |
| AtomicReference<WeakReference<State>> stateForKey2Ref = new AtomicReference<>(); |
| AtomicReference<WeakReference<State>> stateForKey3Ref = new AtomicReference<>(); |
| SkyFunction skyFunctionForTest = |
| // Whose #compute is such that |
| (skyKey, env) -> { |
| State state = env.getState(State::new); |
| state.usageCount++; |
| int numCallsForKey = (int) numCalls.incrementAndGet(skyKey); |
| // The number of calls to #compute is expected to be equal to the number of usages of |
| // the state for that key, |
| assertThat(state.usageCount).isEqualTo(numCallsForKey); |
| if (skyKey.equals(key1)) { |
| // And the semantics for key1 are: |
| |
| // The state for key1 is expected to be the first one created (since key1 is expected |
| // to be the first node we attempt to compute). |
| assertThat(state.instanceCount).isEqualTo(1); |
| // And key1 declares a dep on key2, |
| if (env.getValue(key2) == null) { |
| // (And that dep is expected to be missing on the initial #compute call for key1) |
| assertThat(numCallsForKey).isEqualTo(1); |
| return null; |
| } |
| // And if that dep is not missing, then we expect: |
| // - We're on the second #compute call for key1 |
| assertThat(numCallsForKey).isEqualTo(2); |
| // - The state for key2 should have been eligible for GC. This is because the node |
| // for key2 must have been fully computed, meaning its compute state is no longer |
| // needed, and so ParallelEvaluator ought to have made it eligible for GC. |
| GcFinalization.awaitClear(stateForKey2Ref.get()); |
| return new StringValue("value1"); |
| } else if (skyKey.equals(key2)) { |
| // And the semantics for key2 are: |
| |
| // The state for key2 is expected to be the second one created. |
| assertThat(state.instanceCount).isEqualTo(2); |
| stateForKey2Ref.set(new WeakReference<>(state)); |
| // And key2 declares a dep on key3, |
| if (env.getValue(key3) == null) { |
| // (And that dep is expected to be missing on the initial #compute call for key2) |
| assertThat(numCallsForKey).isEqualTo(1); |
| return null; |
| } |
| // And if that dep is not missing, then we expect the same sort of things we expected |
| // for key1 in this situation. |
| assertThat(numCallsForKey).isEqualTo(2); |
| GcFinalization.awaitClear(stateForKey3Ref.get()); |
| return new StringValue("value2"); |
| } else if (skyKey.equals(key3)) { |
| // And the semantics for key3 are: |
| |
| // The state for key3 is expected to be the third one created. |
| assertThat(state.instanceCount).isEqualTo(3); |
| stateForKey3Ref.set(new WeakReference<>(state)); |
| // And key3 declares no deps. |
| return new StringValue("value3"); |
| } |
| throw new IllegalStateException(); |
| }; |
| |
| tester.putSkyFunction(SkyKeyForSkyKeyComputeStateTests.FUNCTION_NAME, skyFunctionForTest); |
| graph = new InMemoryGraphImpl(); |
| // Then, when we evaluate key1, |
| SkyValue resultValue = eval(/* keepGoing= */ true, key1); |
| // It successfully produces the value we expect, confirming all our other expectations about |
| // the compute states were correct. |
| assertThat(resultValue).isEqualTo(new StringValue("value1")); |
| } |
| |
| private static class SkyFunctionExceptionForTest extends SkyFunctionException { |
| SkyFunctionExceptionForTest(String message) { |
| super(new SomeErrorException(message), Transience.PERSISTENT); |
| } |
| } |
| |
| // Test for SkyKeyComputeState in the situation of an error for one node causing normal evaluation |
| // to fail-fast, but when there are SkyKeyComputeState instances for other inflight nodes. |
| @Test |
| public void skyKeyComputeState_noKeepGoingWithAnError() throws InterruptedException { |
| // When we have 3 nodes: key1, key2, key3. |
| // (with dependency graph key1 -> key2; key3, to be declared later in this test) |
| // (and we'll be evaluating key1 & key3 in parallel later in this test) |
| SkyKey key1 = new SkyKeyForSkyKeyComputeStateTests("key1"); |
| SkyKey key2 = new SkyKeyForSkyKeyComputeStateTests("key2"); |
| SkyKey key3 = new SkyKeyForSkyKeyComputeStateTests("key3"); |
| |
| class State implements SkyKeyComputeState {} |
| |
| // And a SkyFunction for these nodes, |
| AtomicReference<WeakReference<State>> stateForKey1Ref = new AtomicReference<>(); |
| AtomicReference<WeakReference<State>> stateForKey3Ref = new AtomicReference<>(); |
| CountDownLatch key3SleepingLatch = new CountDownLatch(1); |
| AtomicBoolean onNormalEvaluation = new AtomicBoolean(true); |
| SkyFunction skyFunctionForTest = |
| // Whose #compute is such that |
| (skyKey, env) -> { |
| if (onNormalEvaluation.get()) { |
| // When we're on the normal evaluation: |
| |
| State state = env.getState(State::new); |
| if (skyKey.equals(key1)) { |
| // For key1: |
| |
| stateForKey1Ref.set(new WeakReference<>(state)); |
| // We declare a dep on key. |
| return env.getValue(key2); |
| } else if (skyKey.equals(key2)) { |
| // For key2: |
| |
| // We wait for the thread computing key3 to be sleeping |
| key3SleepingLatch.await(); |
| // And then we throw an error, which will fail the normal evaluation and trigger |
| // error bubbling. |
| onNormalEvaluation.set(false); |
| throw new SkyFunctionExceptionForTest("normal evaluation"); |
| } else if (skyKey.equals(key3)) { |
| // For key3: |
| |
| stateForKey3Ref.set(new WeakReference<>(state)); |
| key3SleepingLatch.countDown(); |
| // We sleep forever. (To be interrupted by ParallelEvaluator when the normal |
| // evaluation fails). |
| Thread.sleep(Long.MAX_VALUE); |
| } |
| throw new IllegalStateException(); |
| } else { |
| // When we're in error bubbling: |
| |
| // The states for the nodes from normal evaluation should have been eligible for GC. |
| // This is because ParallelEvaluator ought to have them eligible for GC before |
| // starting error bubbling. |
| GcFinalization.awaitClear(stateForKey1Ref.get()); |
| GcFinalization.awaitClear(stateForKey3Ref.get()); |
| |
| // We bubble up a unique error message. |
| throw new SkyFunctionExceptionForTest("error bubbling for " + skyKey.argument()); |
| } |
| }; |
| |
| tester.putSkyFunction(SkyKeyForSkyKeyComputeStateTests.FUNCTION_NAME, skyFunctionForTest); |
| graph = new InMemoryGraphImpl(); |
| // Then, when we do a nokeep_going evaluation of key1 and key3 in parallel, |
| assertThatEvaluationResult(eval(/* keepGoing= */ false, key1, key3)) |
| // The evaluation fails (as expected), |
| .hasErrorEntryForKeyThat(key1) |
| .hasExceptionThat() |
| .hasMessageThat() |
| // And the error message for key1 is from error bubbling, |
| .isEqualTo("error bubbling for key1"); |
| // Confirming that all our other expectations about the compute states were correct. |
| } |
| |
| // Demonstrates we're able to drop SkyKeyCompute state intra-evaluation and post-evaluation. |
| @Test |
| public void skyKeyComputeState_unnecessaryTemporaryStateDropperReceiver() |
| throws InterruptedException { |
| // When we have 2 nodes: key1, key2 |
| // (with dependency graph key1 -> key2, to be declared later in this test) |
| // (and we'll be evaluating key1 later in this test) |
| SkyKey key1 = new SkyKeyForSkyKeyComputeStateTests("key1"); |
| SkyKey key2 = new SkyKeyForSkyKeyComputeStateTests("key2"); |
| |
| // And an SkyKeyComputeState implementation that tracks global instance counts and cleanup call |
| // counts, |
| AtomicInteger globalStateInstanceCounter = new AtomicInteger(); |
| AtomicInteger globalStateCleanupCounter = new AtomicInteger(); |
| class State implements SkyKeyComputeState { |
| final int instanceCount = globalStateInstanceCounter.incrementAndGet(); |
| |
| @Override |
| public void close() { |
| globalStateCleanupCounter.incrementAndGet(); |
| } |
| } |
| |
| // And an UnnecessaryTemporaryStateDropperReceiver that, |
| AtomicReference<UnnecessaryTemporaryStateDropper> dropperRef = new AtomicReference<>(); |
| UnnecessaryTemporaryStateDropperReceiver dropperReceiver = |
| new UnnecessaryTemporaryStateDropperReceiver() { |
| @Override |
| public void onEvaluationStarted(UnnecessaryTemporaryStateDropper dropper) { |
| // Captures the UnnecessaryTemporaryStateDropper (for our use intra-evaluation) |
| dropperRef.set(dropper); |
| } |
| |
| @Override |
| public void onEvaluationFinished() { |
| // And then drops everything when the evaluation is done. |
| dropperRef.get().drop(); |
| } |
| }; |
| |
| AtomicReference<WeakReference<State>> stateForKey1Ref = new AtomicReference<>(); |
| |
| // And a SkyFunction for these nodes, |
| SkyFunction skyFunctionForTest = |
| // Whose #compute is such that |
| (skyKey, env) -> { |
| State state = env.getState(State::new); |
| if (skyKey.equals(key1)) { |
| // The semantics for key1 are: |
| |
| // We declare a dep on key2. |
| if (env.getValue(key2) == null) { |
| // If key2 is missing, that means we're on the initial #compute call for key1, |
| // And so we expect the compute state to be the first instance ever. |
| assertThat(state.instanceCount).isEqualTo(1); |
| stateForKey1Ref.set(new WeakReference<>(state)); |
| |
| return null; |
| } else { |
| // But if key2 is not missing, that means we're on the subsequent #compute call for |
| // key1. That means we expect the compute state to be the third instance ever, |
| // because... |
| assertThat(state.instanceCount).isEqualTo(3); |
| |
| return new StringValue("value1"); |
| } |
| } else if (skyKey.equals(key2)) { |
| // ... The semantics for key2 are: |
| |
| // Drop all compute states. |
| dropperRef.get().drop(); |
| // Confirm the old compute state for key1 was GC'd. |
| GcFinalization.awaitClear(stateForKey1Ref.get()); |
| // At this point, both state objects have been cleaned up. |
| assertThat(globalStateCleanupCounter.get()).isEqualTo(2); |
| // Also confirm key2's compute state is the second instance ever. |
| assertThat(state.instanceCount).isEqualTo(2); |
| |
| return new StringValue("value2"); |
| } |
| throw new IllegalStateException(); |
| }; |
| |
| tester.putSkyFunction(SkyKeyForSkyKeyComputeStateTests.FUNCTION_NAME, skyFunctionForTest); |
| graph = new InMemoryGraphImpl(); |
| |
| ParallelEvaluator parallelEvaluator = |
| new ParallelEvaluator( |
| graph, |
| graphVersion, |
| Version.minimal(), |
| tester.getSkyFunctionMap(), |
| reportedEvents, |
| new EmittedEventState(), |
| EventFilter.FULL_STORAGE, |
| ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, |
| // Doesn't matter for this test case. |
| /* keepGoing= */ false, |
| revalidationReceiver, |
| GraphInconsistencyReceiver.THROWING, |
| // We ought not need more than 1 thread for this test case. |
| AbstractQueueVisitor.create( |
| "test-pool", 1, ParallelEvaluatorErrorClassifier.instance()), |
| new SimpleCycleDetector(), |
| dropperReceiver); |
| // Then, when we evaluate key1, |
| SkyValue resultValue = parallelEvaluator.eval(ImmutableList.of(key1)).get(key1); |
| // It successfully produces the value we expect, confirming all our other expectations about |
| // the compute states were correct. |
| assertThat(resultValue).isEqualTo(new StringValue("value1")); |
| // And all state objects have been dropped, confirming the #onEvaluationFinished method was |
| // called. |
| assertThat(globalStateCleanupCounter.get()).isEqualTo(3); |
| } |
| |
| // Test for the basic functionality of ClassToInstanceMapSkyKeyComputeState, demonstrating |
| // that it can hold state associated with different classes and those states are independent. |
| @Test |
| public void classToInstanceMapSkyKeyComputeState( |
| @TestParameter boolean touchStateA, @TestParameter boolean touchStateB) |
| throws InterruptedException { |
| class StateA implements SkyKeyComputeState { |
| boolean touched; |
| } |
| class StateB implements SkyKeyComputeState { |
| boolean touched; |
| } |
| SkyKey key1 = new SkyKeyForSkyKeyComputeStateTests("key1"); |
| SkyKey key2 = new SkyKeyForSkyKeyComputeStateTests("key2"); |
| AtomicLongMap<SkyKey> numCalls = AtomicLongMap.create(); |
| SkyFunction skyFunctionForTest = |
| (skyKey, env) -> { |
| int numCallsForKey = (int) numCalls.incrementAndGet(skyKey); |
| if (skyKey.equals(key1)) { |
| if (numCallsForKey == 1) { |
| if (touchStateA) { |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new) |
| .getInstance(StateA.class, StateA::new) |
| .touched = |
| true; |
| } |
| if (touchStateB) { |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new) |
| .getInstance(StateB.class, StateB::new) |
| .touched = |
| true; |
| } |
| assertThat(env.getValue(key2)).isNull(); |
| return null; |
| } |
| assertThat( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new) |
| .getInstance(StateA.class, StateA::new) |
| .touched) |
| .isEqualTo(touchStateA); |
| assertThat( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new) |
| .getInstance(StateB.class, StateB::new) |
| .touched) |
| .isEqualTo(touchStateB); |
| SkyValue value = env.getValue(key2); |
| assertThat(value).isEqualTo(new StringValue("value")); |
| return value; |
| } |
| if (skyKey.equals(key2)) { |
| return new StringValue("value"); |
| } |
| throw new IllegalStateException(); |
| }; |
| tester.putSkyFunction(SkyKeyForSkyKeyComputeStateTests.FUNCTION_NAME, skyFunctionForTest); |
| graph = new InMemoryGraphImpl(); |
| SkyValue resultValue = eval(/* keepGoing= */ true, key1); |
| assertThat(resultValue).isEqualTo(new StringValue("value")); |
| } |
| |
| private static class PartialReevaluationKey extends AbstractSkyKey<String> { |
| private static final SkyFunctionName FUNCTION_NAME = |
| SkyFunctionName.createHermetic("PARTIAL_REEVALUATION"); |
| |
| private PartialReevaluationKey(String arg) { |
| super(arg); |
| } |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return FUNCTION_NAME; |
| } |
| |
| @Override |
| public boolean supportsPartialReevaluation() { |
| return true; |
| } |
| } |
| |
| /** |
| * A bundle of {@link SkyframeLookupResult}s that can be consumed from as if it was a single one. |
| */ |
| static class DelegatingSkyframeLookupResult implements SkyframeLookupResult { |
| private static final Splitter BATCH_SPLITTER = Splitter.on(';'); |
| private static final Splitter KEY_SPLITTER = Splitter.on(','); |
| private final ImmutableMap<SkyKey, SkyframeLookupResult> resultsByKey; |
| |
| /** |
| * Makes {@link SkyFunction.Environment#getValuesAndExceptions} calls in batches as described by |
| * {@code requestBatches} and returns them bundled up. |
| * |
| * <p>The {@code requestBatches} string is split first by ';' to define an order-sensitive |
| * sequence of batches, then by ',' to define an order-insensitive collection of keys. |
| */ |
| static DelegatingSkyframeLookupResult fromRequestBatches( |
| String requestBatches, |
| SkyFunction.Environment env, |
| ImmutableList<? extends AbstractSkyKey<String>> keys) |
| throws InterruptedException { |
| ImmutableMap<String, ? extends AbstractSkyKey<String>> keysByArg = |
| keys.stream().collect(ImmutableMap.toImmutableMap(AbstractSkyKey::argument, k -> k)); |
| |
| ImmutableMap.Builder<SkyKey, SkyframeLookupResult> builder = new ImmutableMap.Builder<>(); |
| Iterable<String> batches = BATCH_SPLITTER.split(requestBatches); |
| for (String batch : batches) { |
| Iterable<String> batchKeys = KEY_SPLITTER.split(batch); |
| SkyframeLookupResult result = |
| env.getValuesAndExceptions( |
| stream(batchKeys).map(keysByArg::get).collect(toImmutableList())); |
| for (String batchKey : batchKeys) { |
| builder.put(checkNotNull(keysByArg.get(batchKey)), result); |
| } |
| } |
| ImmutableMap<SkyKey, SkyframeLookupResult> resultsByKey = builder.buildOrThrow(); |
| |
| assertThat(resultsByKey).hasSize(keys.size()); |
| return new DelegatingSkyframeLookupResult(resultsByKey); |
| } |
| |
| private DelegatingSkyframeLookupResult( |
| ImmutableMap<SkyKey, SkyframeLookupResult> resultsByKey) { |
| this.resultsByKey = resultsByKey; |
| } |
| |
| @Nullable |
| @Override |
| public <E1 extends Exception, E2 extends Exception, E3 extends Exception> SkyValue getOrThrow( |
| SkyKey skyKey, |
| @Nullable Class<E1> exceptionClass1, |
| @Nullable Class<E2> exceptionClass2, |
| @Nullable Class<E3> exceptionClass3) |
| throws E1, E2, E3 { |
| return checkNotNull(resultsByKey.get(skyKey)) |
| .getOrThrow(skyKey, exceptionClass1, exceptionClass2, exceptionClass3); |
| } |
| |
| @Override |
| public boolean queryDep(SkyKey key, QueryDepCallback resultCallback) { |
| return checkNotNull(resultsByKey.get(key)).queryDep(key, resultCallback); |
| } |
| } |
| |
| @Test |
| public void partialReevaluationOneButNotAllDeps( |
| @TestParameter({"key2,key3", "key2;key3", "key3;key2"}) String requestBatches) |
| throws InterruptedException { |
| // The parameterization of this test ensures that the partial reevaluation implementation is |
| // insensitive to dep grouping. |
| |
| // This test illustrates the basic functionality of partial reevaluation: that a |
| // function opting into partial reevaluation can be resumed when one, but not all, of its |
| // previously requested-and-not-already-evaluated dependencies finishes evaluation. |
| |
| // Graph structure: |
| // * key1 depends on key2 and key3 |
| |
| // Evaluation behavior: |
| // * key3 will not finish evaluating until key1 has been restarted with the result of key2 |
| |
| PartialReevaluationKey key1 = new PartialReevaluationKey("key1"); |
| PartialReevaluationKey key2 = new PartialReevaluationKey("key2"); |
| PartialReevaluationKey key3 = new PartialReevaluationKey("key3"); |
| AtomicInteger key1EvaluationCount = new AtomicInteger(); |
| CountDownLatch key1ObservesTheValueOfKey2 = new CountDownLatch(1); |
| SkyFunction f = |
| (skyKey, env) -> { |
| PartialReevaluationMailbox mailbox = |
| PartialReevaluationMailbox.from( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new)); |
| Mail mail = mailbox.getMail(); |
| assertThat(mailbox.getMail().kind()).isEqualTo(Kind.EMPTY); |
| |
| if (skyKey.equals(key1)) { |
| int c = key1EvaluationCount.incrementAndGet(); |
| SkyframeLookupResult result = |
| DelegatingSkyframeLookupResult.fromRequestBatches( |
| requestBatches, env, ImmutableList.of(key2, key3)); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isNull(); |
| assertThat(result.get(key3)).isNull(); |
| return null; |
| } |
| if (c == 2) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key2); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isNull(); |
| key1ObservesTheValueOfKey2.countDown(); |
| return null; |
| } |
| assertThat(c).isEqualTo(3); |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key3); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| return StringValue.of("val1"); |
| } |
| if (skyKey.equals(key2)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| return StringValue.of("val2"); |
| } |
| assertThat(skyKey).isEqualTo(key3); |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey2.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return StringValue.of("val3"); |
| }; |
| tester.putSkyFunction(PartialReevaluationKey.FUNCTION_NAME, f); |
| graph = new InMemoryGraphImpl(); |
| SkyValue resultValue = eval(/* keepGoing= */ true, key1); |
| assertThat(resultValue).isEqualTo(StringValue.of("val1")); |
| assertThat(key1EvaluationCount.get()).isEqualTo(3); |
| } |
| |
| @Test |
| public void partialReevaluationOneDuringAReevaluation( |
| @TestParameter({ |
| "key2,key3,key4", |
| "key2,key3;key4", |
| "key2;key3,key4", |
| "key2;key3;key4", |
| // permute (2,3): |
| "key3;key2,key4", |
| "key3;key2;key4", |
| // permute (2,4): |
| "key4,key3;key2", |
| "key4;key3,key2", |
| "key4;key3;key2", |
| // permute (3,4): |
| "key2,key4;key3", |
| "key2;key4;key3", |
| // permute (2,3,4): |
| "key4;key2;key3" |
| }) |
| String requestBatches) |
| throws InterruptedException { |
| // The parameterization of this test ensures that the partial reevaluation implementation is |
| // insensitive to dep grouping. |
| |
| // This test illustrates another bit of partial reevaluation functionality: when a partial |
| // reevaluation is currently underway, and when another one of its previously |
| // requested-and-not-already-evaluated dependencies finishes evaluation (but not all of them), |
| // another partial reevaluation will run when the current one finishes. |
| |
| // Graph structure: |
| // * key1 depends on key2, key3, and key4 |
| // * key5 depends on key3 (it's here only to detect when key3 signals its parents) |
| |
| // Evaluation behavior: |
| // * key4 will not finish evaluating until key1 has been restarted with the result of key3 |
| // * key1's partial reevaluation observing key2 does not finish until key3 has signaled its |
| // parents (i.e. key1 and key5) |
| // * key3 will not finish evaluating until key1 has been restarted with the result of key2 |
| |
| PartialReevaluationKey key1 = new PartialReevaluationKey("key1"); |
| PartialReevaluationKey key2 = new PartialReevaluationKey("key2"); |
| PartialReevaluationKey key3 = new PartialReevaluationKey("key3"); |
| PartialReevaluationKey key4 = new PartialReevaluationKey("key4"); |
| PartialReevaluationKey key5 = new PartialReevaluationKey("key5"); |
| AtomicInteger key1EvaluationCount = new AtomicInteger(); |
| AtomicInteger key5EvaluationCount = new AtomicInteger(); |
| AtomicInteger key1EvaluationsInflight = new AtomicInteger(); |
| CountDownLatch key1ObservesTheValueOfKey2 = new CountDownLatch(1); |
| CountDownLatch key5ObservesTheAbsenceOfKey3 = new CountDownLatch(1); |
| CountDownLatch key1ObservesTheValueOfKey3 = new CountDownLatch(1); |
| CountDownLatch key3SignaledItsParents = new CountDownLatch(1); |
| SkyFunction f = |
| (skyKey, env) -> { |
| Mail mail = |
| PartialReevaluationMailbox.from( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new)) |
| .getMail(); |
| if (skyKey.equals(key1)) { |
| assertThat(key1EvaluationsInflight.incrementAndGet()).isEqualTo(1); |
| try { |
| int c = key1EvaluationCount.incrementAndGet(); |
| SkyframeLookupResult result = |
| DelegatingSkyframeLookupResult.fromRequestBatches( |
| requestBatches, env, ImmutableList.of(key2, key3, key4)); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isNull(); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| return null; |
| } |
| if (c == 2) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key2); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| key1ObservesTheValueOfKey2.countDown(); |
| assertThat( |
| key3SignaledItsParents.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return null; |
| } |
| if (c == 3) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key3); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| assertThat(result.get(key4)).isNull(); |
| key1ObservesTheValueOfKey3.countDown(); |
| return null; |
| } |
| assertThat(c).isEqualTo(4); |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key4); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| assertThat(result.get(key4)).isEqualTo(StringValue.of("val4")); |
| return StringValue.of("val1"); |
| } finally { |
| assertThat(key1EvaluationsInflight.decrementAndGet()).isEqualTo(0); |
| } |
| } |
| |
| if (skyKey.equals(key2)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| return StringValue.of("val2"); |
| } |
| |
| if (skyKey.equals(key3)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey2.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| assertThat( |
| key5ObservesTheAbsenceOfKey3.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return StringValue.of("val3"); |
| } |
| |
| if (skyKey.equals(key4)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey3.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return StringValue.of("val4"); |
| } |
| |
| assertThat(skyKey).isEqualTo(key5); |
| int c = key5EvaluationCount.incrementAndGet(); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| SkyValue value = env.getValue(key3); |
| assertThat(value).isNull(); |
| key5ObservesTheAbsenceOfKey3.countDown(); |
| return null; |
| } |
| assertThat(c).isEqualTo(2); |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key3); |
| SkyValue value = env.getValue(key3); |
| assertThat(value).isEqualTo(StringValue.of("val3")); |
| key3SignaledItsParents.countDown(); |
| return StringValue.of("val5"); |
| }; |
| tester.putSkyFunction(PartialReevaluationKey.FUNCTION_NAME, f); |
| graph = new InMemoryGraphImpl(); |
| EvaluationResult<SkyValue> resultValues = eval(/* keepGoing= */ true, key1, key5); |
| assertThat(resultValues.get(key1)).isEqualTo(StringValue.of("val1")); |
| assertThat(resultValues.get(key5)).isEqualTo(StringValue.of("val5")); |
| assertThat(key1EvaluationsInflight.get()).isEqualTo(0); |
| assertThat(key1EvaluationCount.get()).isEqualTo(4); |
| } |
| |
| @Test |
| public void partialReevaluationErrorDuringReevaluation(@TestParameter boolean keepGoing) |
| throws InterruptedException { |
| // This test illustrates how the partial reevaluation implementation handles the case in which a |
| // SkyFunction throws an error during a partial reevaluation: the error is ignored if the |
| // partially-reevaluated node has not yet been fully signaled. This maintains the invariant that |
| // nodes are not committed with a value or error while they have deps which may signal them. |
| |
| // Note that Skyframe policy encourages SkyFunction behavior such as this: SkyFunction |
| // implementations should eagerly throw errors even when not all their deps are done, to better |
| // support error contextualization during no-keep_going builds. |
| |
| // Graph structure: |
| // * key1 depends on key2, key3, and key4 |
| // * key5 depends on key3 (it's here only to detect when key3 signals its parents) |
| |
| // Evaluation behavior: |
| // * key4 will not finish evaluating until key1 has been restarted with the result of key3 |
| // * key1's partial reevaluation observing key2 does not finish until key3 has signaled its |
| // parents (i.e. key1 and key5) |
| // * key3 will not finish evaluating until key1 has been restarted with the result of key2 |
| |
| PartialReevaluationKey key1 = new PartialReevaluationKey("key1"); |
| PartialReevaluationKey key2 = new PartialReevaluationKey("key2"); |
| PartialReevaluationKey key3 = new PartialReevaluationKey("key3"); |
| PartialReevaluationKey key4 = new PartialReevaluationKey("key4"); |
| PartialReevaluationKey key5 = new PartialReevaluationKey("key5"); |
| AtomicInteger key1EvaluationCount = new AtomicInteger(); |
| AtomicInteger key5EvaluationCount = new AtomicInteger(); |
| AtomicInteger key1EvaluationsInflight = new AtomicInteger(); |
| CountDownLatch key1ObservesTheValueOfKey2 = new CountDownLatch(1); |
| CountDownLatch key5ObservesTheAbsenceOfKey3 = new CountDownLatch(1); |
| CountDownLatch key1ObservesTheValueOfKey3 = new CountDownLatch(1); |
| CountDownLatch key3SignaledItsParents = new CountDownLatch(1); |
| SkyFunction f = |
| (skyKey, env) -> { |
| Mail mail = |
| PartialReevaluationMailbox.from( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new)) |
| .getMail(); |
| if (skyKey.equals(key1)) { |
| assertThat(key1EvaluationsInflight.incrementAndGet()).isEqualTo(1); |
| try { |
| int c = key1EvaluationCount.incrementAndGet(); |
| SkyframeLookupResult result = |
| env.getValuesAndExceptions(ImmutableList.of(key2, key3, key4)); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isNull(); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| return null; |
| } |
| if (c == 2) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key2); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| key1ObservesTheValueOfKey2.countDown(); |
| assertThat( |
| key3SignaledItsParents.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| throw new SkyFunctionExceptionForTest( |
| "Error thrown during partial reevaluation (1)"); |
| } |
| if (c == 3) { |
| // The Skyframe stateCache invalidates its entry for a node when it throws an error: |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| assertThat(result.get(key4)).isNull(); |
| key1ObservesTheValueOfKey3.countDown(); |
| throw new SkyFunctionExceptionForTest( |
| "Error thrown during partial reevaluation (2)"); |
| } |
| assertThat(c).isEqualTo(4); |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| assertThat(result.get(key4)).isEqualTo(StringValue.of("val4")); |
| throw new SkyFunctionExceptionForTest("Error thrown during final full evaluation"); |
| } finally { |
| assertThat(key1EvaluationsInflight.decrementAndGet()).isEqualTo(0); |
| } |
| } |
| |
| if (skyKey.equals(key2)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| return StringValue.of("val2"); |
| } |
| |
| if (skyKey.equals(key3)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey2.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| assertThat( |
| key5ObservesTheAbsenceOfKey3.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return StringValue.of("val3"); |
| } |
| |
| if (skyKey.equals(key4)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey3.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| return StringValue.of("val4"); |
| } |
| |
| assertThat(skyKey).isEqualTo(key5); |
| int c = key5EvaluationCount.incrementAndGet(); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| SkyValue value = env.getValue(key3); |
| assertThat(value).isNull(); |
| key5ObservesTheAbsenceOfKey3.countDown(); |
| return null; |
| } |
| assertThat(c).isEqualTo(2); |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key3); |
| SkyValue value = env.getValue(key3); |
| assertThat(value).isEqualTo(StringValue.of("val3")); |
| key3SignaledItsParents.countDown(); |
| return StringValue.of("val5"); |
| }; |
| tester.putSkyFunction(PartialReevaluationKey.FUNCTION_NAME, f); |
| graph = new InMemoryGraphImpl(); |
| EvaluationResult<SkyValue> resultValues = eval(keepGoing, key1, key5); |
| assertThat(resultValues.getError(key1).getException()).isInstanceOf(SomeErrorException.class); |
| |
| // key4's signal to key1 races with key1's readiness check after completing with an error during |
| // partial reevaluation 2. If the signal wins, then key1 should be allowed to complete, because |
| // it has no outstanding signals. If the signal loses, then key1 will reevaluate one last time. |
| assertThat(resultValues.getError(key1).getException()) |
| .hasMessageThat() |
| .isIn( |
| ImmutableList.of( |
| "Error thrown during partial reevaluation (2)", |
| "Error thrown during final full evaluation")); |
| if (keepGoing) { |
| assertThat(resultValues.get(key5)).isEqualTo(StringValue.of("val5")); |
| } |
| assertThat(key1EvaluationsInflight.get()).isEqualTo(0); |
| assertThat(key1EvaluationCount.get()).isIn(ImmutableList.of(3, 4)); |
| } |
| |
| @Test |
| public void partialReevaluationOneErrorButNotAllDeps( |
| @TestParameter boolean keepGoing, @TestParameter boolean enrichError) |
| throws InterruptedException { |
| // The parameterization of this test ensures that the partial reevaluation implementation is |
| // insensitive to dep grouping. |
| |
| // This test illustrates partial reevaluation's handling of errors in multiple contexts, with |
| // and without keepGoing, and with the error-observing function enriching or recovering from the |
| // error. |
| |
| // Graph structure: |
| // * key1 depends on key2 and key3 |
| |
| // Evaluation behavior: |
| // * key3 will not finish evaluating until key1 has been restarted with the error result of key2 |
| // (which notably will never happen in no-keepGoing because by then key2 should have |
| // interrupted evaluations) |
| |
| PartialReevaluationKey key1 = new PartialReevaluationKey("key1"); |
| PartialReevaluationKey key2 = new PartialReevaluationKey("key2"); |
| PartialReevaluationKey key3 = new PartialReevaluationKey("key3"); |
| AtomicInteger key1EvaluationCount = new AtomicInteger(); |
| CountDownLatch key1ObservesTheErrorOfKey2 = new CountDownLatch(1); |
| SkyFunction f = |
| (skyKey, env) -> { |
| Mail mail = |
| PartialReevaluationMailbox.from( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new)) |
| .getMail(); |
| if (skyKey.equals(key1)) { |
| int c = key1EvaluationCount.incrementAndGet(); |
| SkyframeLookupResult result = env.getValuesAndExceptions(ImmutableList.of(key2, key3)); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isNull(); |
| assertThat(result.get(key3)).isNull(); |
| return null; |
| } |
| if (c == 2) { |
| if (keepGoing) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key2); |
| } else { |
| // The Skyframe stateCache invalidates everything when starting error bubbling: |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(env.inErrorBubbling()).isTrue(); |
| } |
| assertThrows( |
| "key2", |
| SomeErrorException.class, |
| () -> result.getOrThrow(key2, SomeErrorException.class)); |
| assertThat(result.get(key3)).isNull(); |
| key1ObservesTheErrorOfKey2.countDown(); |
| if (enrichError) { |
| throw new SkyFunctionExceptionForTest("key1 observed key2 exception (w/o key3)"); |
| } else { |
| return null; |
| } |
| } |
| assertThat(c).isEqualTo(3); |
| if (enrichError) { |
| // The Skyframe stateCache invalidates its entry for a node when it throws an error: |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| } else { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key3); |
| } |
| assertThat(keepGoing).isTrue(); |
| assertThrows( |
| "key2", |
| SomeErrorException.class, |
| () -> result.getOrThrow(key2, SomeErrorException.class)); |
| assertThat(result.get(key3)).isEqualTo(StringValue.of("val3")); |
| if (enrichError) { |
| throw new SkyFunctionExceptionForTest("key1 observed key2 exception (w/ key3)"); |
| } else { |
| return StringValue.of("val1"); |
| } |
| } |
| if (skyKey.equals(key2)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| throw new SkyFunctionExceptionForTest("key2"); |
| } |
| assertThat(skyKey).isEqualTo(key3); |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheErrorOfKey2.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| assertThat(keepGoing).isTrue(); |
| return StringValue.of("val3"); |
| }; |
| tester.putSkyFunction(PartialReevaluationKey.FUNCTION_NAME, f); |
| graph = new InMemoryGraphImpl(); |
| EvaluationResult<SkyValue> result = eval(/* keepGoing= */ keepGoing, ImmutableList.of(key1)); |
| |
| if (keepGoing && enrichError) { |
| assertThat(result.getError(key1).getException()).isInstanceOf(SomeErrorException.class); |
| assertThat(result.getError(key1).getException()) |
| .hasMessageThat() |
| .isEqualTo("key1 observed key2 exception (w/ key3)"); |
| } else if (keepGoing) { |
| // !enrichError -- expect key1 to have recovered |
| assertThat(result.get(key1)).isEqualTo(StringValue.of("val1")); |
| } else if (enrichError) { |
| // !keepGoing -- expect key3 to have not completed |
| assertThat(result.getError(key1).getException()).isInstanceOf(SomeErrorException.class); |
| assertThat(result.getError(key1).getException()) |
| .hasMessageThat() |
| .isEqualTo("key1 observed key2 exception (w/o key3)"); |
| } else { |
| // !keepGoing && !enrichError -- expect key2's error |
| assertThat(result.getError(key1).getException()).isInstanceOf(SomeErrorException.class); |
| assertThat(result.getError(key1).getException()).hasMessageThat().isEqualTo("key2"); |
| } |
| |
| assertThat(key1EvaluationCount.get()).isEqualTo(keepGoing ? 3 : 2); |
| } |
| |
| @Test |
| public void partialReevaluationErrorObservedDuringReevaluation() throws InterruptedException { |
| // This test illustrates how the partial reevaluation implementation handles the case in which a |
| // SkyFunction doing a partial reevaluation has a dep which completes with an error during a |
| // no-keep_going build: that partial reevaluation ends, because |
| // ParallelEvaluatorContext.signalParentsOnAbort completely ignores the value returned by |
| // entry.signalDep. The node undergoing partial evaluation may or may not be chosen when |
| // bubbling up the dep's error; in this test it's chosen because it's the only parent. |
| |
| // Note that the partial reevaluation implementation is *not* responsible for this behavior; |
| // rather, it is due to "core" Skyframe evaluation policy. However, this test documents the |
| // expected behavior of partial reevaluation implementations, in case of future changes to |
| // Skyframe. |
| |
| // Graph structure: |
| // * key1 depends on key2, key3, and key4 |
| |
| // Evaluation behavior: |
| // * key4 will not finish evaluating -- it gets interrupted by key2's error and is never resumed |
| // * key3 will not finish evaluating until key1 has been restarted with the result of key2 |
| // * key1 only observes key2's error during error bubbling |
| |
| PartialReevaluationKey key1 = new PartialReevaluationKey("key1"); |
| PartialReevaluationKey key2 = new PartialReevaluationKey("key2"); |
| PartialReevaluationKey key3 = new PartialReevaluationKey("key3"); |
| PartialReevaluationKey key4 = new PartialReevaluationKey("key4"); |
| AtomicInteger key1EvaluationCount = new AtomicInteger(); |
| AtomicInteger key4EvaluationCount = new AtomicInteger(); |
| AtomicInteger key1EvaluationsInflight = new AtomicInteger(); |
| CountDownLatch key1ObservesTheValueOfKey2 = new CountDownLatch(1); |
| CountDownLatch key4WaitsUntilInterruptedByNoKeepGoingEvaluationShutdown = new CountDownLatch(1); |
| SkyFunction f = |
| (skyKey, env) -> { |
| Mail mail = |
| PartialReevaluationMailbox.from( |
| env.getState(ClassToInstanceMapSkyKeyComputeState::new)) |
| .getMail(); |
| if (skyKey.equals(key1)) { |
| assertThat(key1EvaluationsInflight.incrementAndGet()).isEqualTo(1); |
| try { |
| int c = key1EvaluationCount.incrementAndGet(); |
| SkyframeLookupResult result = |
| env.getValuesAndExceptions(ImmutableList.of(key2, key3, key4)); |
| if (c == 1) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(result.get(key2)).isNull(); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| return null; |
| } |
| if (c == 2) { |
| assertThat(mail.kind()).isEqualTo(Kind.CAUSES); |
| assertThat(mail.causes().signaledDeps()).containsExactly(key2); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThat(result.get(key3)).isNull(); |
| assertThat(result.get(key4)).isNull(); |
| key1ObservesTheValueOfKey2.countDown(); |
| return null; |
| } |
| assertThat(c).isEqualTo(3); |
| // The Skyframe stateCache invalidates everything when starting error bubbling: |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(env.inErrorBubbling()).isTrue(); |
| assertThat(result.get(key2)).isEqualTo(StringValue.of("val2")); |
| assertThrows( |
| "key3", |
| SomeErrorException.class, |
| () -> result.getOrThrow(key3, SomeErrorException.class)); |
| assertThat(result.get(key4)).isNull(); |
| throw new SkyFunctionExceptionForTest("Error thrown after partial reevaluation"); |
| } finally { |
| assertThat(key1EvaluationsInflight.decrementAndGet()).isEqualTo(0); |
| } |
| } |
| |
| if (skyKey.equals(key2)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| return StringValue.of("val2"); |
| } |
| |
| if (skyKey.equals(key3)) { |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat( |
| key1ObservesTheValueOfKey2.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) |
| .isTrue(); |
| throw new SkyFunctionExceptionForTest("key3"); |
| } |
| |
| assertThat(skyKey).isEqualTo(key4); |
| assertThat(mail.kind()).isEqualTo(Kind.FRESHLY_INITIALIZED); |
| assertThat(key4EvaluationCount.incrementAndGet()).isEqualTo(1); |
| throw assertThrows( |
| InterruptedException.class, |
| () -> |
| key4WaitsUntilInterruptedByNoKeepGoingEvaluationShutdown.await( |
| TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)); |
| }; |
| tester.putSkyFunction(PartialReevaluationKey.FUNCTION_NAME, f); |
| graph = new InMemoryGraphImpl(); |
| EvaluationResult<SkyValue> resultValues = eval(/* keepGoing= */ false, ImmutableList.of(key1)); |
| assertThat(resultValues.getError(key1).getException()).isInstanceOf(SomeErrorException.class); |
| assertThat(resultValues.getError(key1).getException()) |
| .hasMessageThat() |
| .isEqualTo("Error thrown after partial reevaluation"); |
| assertThat(key1EvaluationsInflight.get()).isEqualTo(0); |
| assertThat(key1EvaluationCount.get()).isEqualTo(3); |
| } |
| |
| // Regression test for b/225877591 ("Unexpected missing value in PrepareDepsOfPatternsFunction |
| // when there's both a dep with a cached cycle and another dep with an error"). |
| @Test |
| public void testDepOnCachedNodeThatItselfDependsOnBothCycleAndError() throws Exception { |
| graph = new InMemoryGraphImpl(); |
| SkyKey parentKey = skyKey("parent"); |
| SkyKey childKey = skyKey("child"); |
| SkyKey cycleKey = skyKey("cycle"); |
| SkyKey errorKey = skyKey("error"); |
| tester.getOrCreate(cycleKey).addDependency(cycleKey); |
| tester.getOrCreate(errorKey).setHasError(true); |
| tester.getOrCreate(childKey).addDependency(cycleKey).addDependency(errorKey); |
| |
| EvaluationResult<StringValue> result = eval(/* keepGoing= */ true, ImmutableList.of(childKey)); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(childKey) |
| .hasCycleInfoThat() |
| .containsExactly(new CycleInfo(ImmutableList.of(childKey), ImmutableList.of(cycleKey))); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(childKey) |
| .hasExceptionThat() |
| .hasMessageThat() |
| .contains(errorKey.toString()); |
| |
| tester |
| .getOrCreate(parentKey) |
| .setBuilder( |
| (key, env) -> { |
| SkyframeLookupResult unusedLookupResult = |
| env.getValuesAndExceptions(ImmutableList.of(childKey)); |
| // env.valuesMissing() should always return true given the graph structure staged |
| // above, regardless of the cached state. (This test case specifically stages a fully |
| // cached graph state and picks on the getValuesAndExceptions method, because that was |
| // the situation of the bug for which this is a regression test.) |
| // |
| // If childKey is legit not yet in the graph, then of course env.valuesMissing() |
| // should return true. |
| // |
| // If childKey is in the graph, then env.valuesMissing() should still return true |
| // because childKey transitively depends on a cycle. |
| assertThat(env.valuesMissing()).isTrue(); |
| return null; |
| }); |
| result = eval(/* keepGoing= */ true, ImmutableList.of(parentKey)); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(parentKey) |
| .hasCycleInfoThat() |
| .containsExactly( |
| new CycleInfo(ImmutableList.of(parentKey, childKey), ImmutableList.of(cycleKey))); |
| assertThatEvaluationResult(result) |
| .hasErrorEntryForKeyThat(parentKey) |
| .hasExceptionThat() |
| .hasMessageThat() |
| .contains(errorKey.toString()); |
| } |
| |
| private void evalParallelSkyFunctionAndVerifyResults( |
| SkyFunction testFunction, |
| QuiescingExecutor testExecutor, |
| AtomicInteger actualRunnableCount, |
| int expectRunnableCount) |
| throws InterruptedException { |
| SkyKey parentKey = skyKey("parentKey"); |
| tester.getOrCreate(parentKey).setBuilder(testFunction); |
| |
| graph = new InMemoryGraphImpl(); |
| ParallelEvaluator parallelEvaluator = |
| new ParallelEvaluator( |
| graph, |
| graphVersion, |
| Version.minimal(), |
| tester.getSkyFunctionMap(), |
| reportedEvents, |
| new EmittedEventState(), |
| EventFilter.FULL_STORAGE, |
| ErrorInfoManager.UseChildErrorInfoIfNecessary.INSTANCE, |
| // Doesn't matter for this test case. |
| /* keepGoing= */ false, |
| revalidationReceiver, |
| GraphInconsistencyReceiver.THROWING, |
| // We ought not need more than 1 thread for this test case. |
| testExecutor, |
| new SimpleCycleDetector(), |
| UnnecessaryTemporaryStateDropperReceiver.NULL); |
| |
| EvaluationResult<StringValue> result = parallelEvaluator.eval(ImmutableList.of(parentKey)); |
| assertThat(result.hasError()).isFalse(); |
| assertThat(result.get(parentKey)).isEqualTo(new StringValue("parentValue")); |
| assertThat(actualRunnableCount.get()).isEqualTo(expectRunnableCount); |
| } |
| |
| @Test |
| public void testParallelSkyFunctionComputation_runnablesOnBothCurrentAndExternalThreads() |
| throws InterruptedException { |
| QuiescingExecutor testExecutor = |
| AbstractQueueVisitor.create("test-pool", 10, ParallelEvaluatorErrorClassifier.instance()); |
| AtomicInteger actualRunnableCount = new AtomicInteger(0); |
| |
| // Let's arbitrarily set the expected size of Runnables as a random number between 10 and 30. |
| int expectRunnableCount = 10 + new Random().nextInt(20); |
| |
| SkyFunction testFunction = |
| (key, env) -> { |
| CountDownLatch countDownLatch = new CountDownLatch(expectRunnableCount); |
| BlockingQueue<Runnable> runnablesQueue = new LinkedBlockingQueue<>(); |
| for (int i = 0; i < expectRunnableCount; ++i) { |
| runnablesQueue.put( |
| () -> { |
| actualRunnableCount.incrementAndGet(); |
| countDownLatch.countDown(); |
| }); |
| } |
| |
| Runnable drainQueue = |
| () -> { |
| Runnable next; |
| while ((next = runnablesQueue.poll()) != null) { |
| next.run(); |
| } |
| }; |
| |
| QuiescingExecutor executor = env.getParallelEvaluationExecutor(); |
| assertThat(executor).isSameInstanceAs(testExecutor); |
| for (int i = 0; i < expectRunnableCount; ++i) { |
| executor.execute(drainQueue); |
| } |
| |
| // After dispatching Runnables to threads on the executor, it is preferred that |
| // current thread also participates in executing some (or even all) runnables. It is |
| // possible that other threads on the executor are all busy and will not be available |
| // for a fairly long time. So having the current thread participate will prevent |
| // current thread from being completely idle while waiting for the runnableQueue to be |
| // fully drained. |
| drainQueue.run(); |
| |
| // It is possible that the last runnable executed on the current thread ends earlier |
| // than what are executed on the other threads. So we need to wait for all necessary |
| // computations to complete before returning. |
| countDownLatch.await(); |
| return new StringValue("parentValue"); |
| }; |
| |
| evalParallelSkyFunctionAndVerifyResults( |
| testFunction, testExecutor, actualRunnableCount, expectRunnableCount); |
| } |
| |
| @Test |
| public void testParallelSkyFunctionComputation_runnablesOnExternalThreadsOnly() |
| throws InterruptedException { |
| QuiescingExecutor testExecutor = |
| AbstractQueueVisitor.create("test-pool", 10, ParallelEvaluatorErrorClassifier.instance()); |
| AtomicInteger actualRunnableCount = new AtomicInteger(0); |
| |
| // Let's arbitrarily set the expected size of Runnables as a random number between 10 and 30. |
| int expectRunnableCount = 10 + new Random().nextInt(20); |
| |
| SkyFunction testFunction = |
| (key, env) -> { |
| CountDownLatch countDownLatch = new CountDownLatch(expectRunnableCount); |
| QuiescingExecutor executor = env.getParallelEvaluationExecutor(); |
| assertThat(executor).isSameInstanceAs(testExecutor); |
| for (int i = 0; i < expectRunnableCount; ++i) { |
| executor.execute( |
| () -> { |
| actualRunnableCount.incrementAndGet(); |
| countDownLatch.countDown(); |
| }); |
| } |
| |
| // We have to wait for all execution dispatched to external threads to complete before |
| // returning. |
| countDownLatch.await(); |
| return new StringValue("parentValue"); |
| }; |
| |
| evalParallelSkyFunctionAndVerifyResults( |
| testFunction, testExecutor, actualRunnableCount, expectRunnableCount); |
| } |
| } |