Fix NullPointerException in SkyframeLookupContinuation.doLookup(). The code mistakenly assumed that eviction could not occur concurrently with SkyFunction.compute. Synchronizes abandon and process to avoid concurrent modification of skyframeLookups. PiperOrigin-RevId: 979963369 Change-Id: Iee108a1420706d38952f9e9ee825751778d78a0d
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyframeLookupContinuation.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyframeLookupContinuation.java index 53566b3..5c3ac8f 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyframeLookupContinuation.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyframeLookupContinuation.java
@@ -36,6 +36,9 @@ * * <p>This class is designed to reside in {@link SkyKeyComputeState}. In particular, note that * {@link #abandon} should be called. + * + * <p>This class is thread-safe because {@link #abandon} may be called concurrently by a background + * thread (e.g. state eviction) while {@link #process} is executing on an evaluator thread. */ public final class SkyframeLookupContinuation { private final ArrayDeque<SkyframeLookup<?>> skyframeLookups; @@ -75,7 +78,7 @@ * occurring in other threads) or null if a Skyframe restart is needed */ @Nullable - public ListenableFuture<?> process(LookupEnvironment env) + public synchronized ListenableFuture<?> process(LookupEnvironment env) throws InterruptedException, SkyframeDependencyException, LookupAbandonedException { return switch (state) { case LOOKUP -> doLookup(env); @@ -90,7 +93,7 @@ * <p>This must be called if the lookups cannot be completed, for example, if {@link * SkyKeyComputeState#close} is called on any containing compute state or if there's an error. */ - public void abandon(LookupAbandonedException exception) { + public synchronized void abandon(LookupAbandonedException exception) { for (SkyframeLookup<?> lookup : skyframeLookups) { lookup.abandon(exception); }
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java index c446016..d5cd263 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java
@@ -37,6 +37,7 @@ import com.google.devtools.build.lib.skyframe.serialization.DeferredObjectCodec.DeferredValue; import com.google.devtools.build.lib.skyframe.serialization.DependOnFutureShim.ObservedFutureStatus; import com.google.devtools.build.lib.skyframe.serialization.SharedValueDeserializationContext.PeerFailedException; +import com.google.devtools.build.lib.skyframe.serialization.SharedValueDeserializationContext.SkyframeLookup; import com.google.devtools.build.lib.skyframe.serialization.SharedValueDeserializationContext.StateEvictedException; import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.NoCachedData; import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.RetrievalContext; @@ -65,11 +66,16 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import javax.annotation.Nullable; import org.junit.Test; @@ -911,6 +917,67 @@ } @Test + public void abandon_concurrentWithDoLookup_synchronizesSafely() throws Exception { + var parent1 = new AtomicReference<Object>(); + var lookup1 = + new SkyframeLookup<AtomicReference<Object>>( + new ExampleKey("a"), parent1, AtomicReference::set); + var parent2 = new AtomicReference<Object>(); + var lookup2 = + new SkyframeLookup<AtomicReference<Object>>( + new ExampleKey("b"), parent2, AtomicReference::set); + + var lookups = new ArrayDeque<SkyframeLookup<?>>(); + lookups.add(lookup1); + lookups.add(lookup2); + var resultFuture = SettableFuture.create(); + var continuation = new SkyframeLookupContinuation(lookups, resultFuture); + + var doLookupEntered = new CountDownLatch(1); + var abandonStarted = new CountDownLatch(1); + var executor = Executors.newSingleThreadExecutor(); + Future<?> abandonFuture; + try { + abandonFuture = + executor.submit( + () -> { + try { + doLookupEntered.await(); + abandonStarted.countDown(); + continuation.abandon(new StateEvictedException()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + ListenableFuture<?> processResult = + continuation.process( + new EnvironmentForUtilities( + k -> { + doLookupEntered.countDown(); + try { + abandonStarted.await(); + // Brief pause to allow the background thread to attempt abandon(), + // verifying that it blocks on continuation's monitor until doLookup finishes. + Thread.sleep(20); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return null; // Returns null to trigger a restart. + })); + abandonFuture.get(); + assertThat(processResult).isNull(); + + // Subsequent process() call after restart: lookups were abandoned, so it returns resultFuture + // cleanly. + assertThat(continuation.process(new EnvironmentForUtilities(k -> null))) + .isSameInstanceAs(resultFuture); + } finally { + executor.shutdown(); + } + } + + @Test public void exceptionWhileWaitingForResult_throwsException() throws Exception { var fingerprintValueService = FingerprintValueService.createForAnalysisCacheTesting(); var analysisCacheServiceData = new HashMap<ByteString, ByteString>();