diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java index f38c1e3dd1f620..d368e5981ad4be 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java @@ -143,6 +143,8 @@ import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.Phaser; @@ -156,6 +158,7 @@ * cache and execution with spawn specific types. */ public class RemoteExecutionService { + private final Reporter reporter; private final boolean verboseFailures; private final Path execRoot; @@ -164,11 +167,14 @@ public class RemoteExecutionService { private final String commandId; private final DigestUtil digestUtil; private final RemoteOptions remoteOptions; - @Nullable private final RemoteCache remoteCache; - @Nullable private final RemoteExecutionClient remoteExecutor; + @Nullable + private final RemoteCache remoteCache; + @Nullable + private final RemoteExecutionClient remoteExecutor; private final TempPathGenerator tempPathGenerator; - @Nullable private final Path captureCorruptedOutputsDir; - private final Cache merkleTreeCache; + @Nullable + private final Path captureCorruptedOutputsDir; + private final Cache> merkleTreeCache; private final Set reportedErrors = new HashSet<>(); private final Phaser backgroundTaskPhaser = new Phaser(1); @@ -344,7 +350,7 @@ public boolean mayBeExecutedRemotely(Spawn spawn) { } @VisibleForTesting - Cache getMerkleTreeCache() { + Cache> getMerkleTreeCache() { return merkleTreeCache; } @@ -418,11 +424,30 @@ private MerkleTree buildMerkleTreeVisitor( MetadataProvider metadataProvider, ArtifactPathResolver artifactPathResolver) throws IOException, ForbiddenActionInputException { - MerkleTree result = merkleTreeCache.getIfPresent(nodeKey); - if (result == null) { - result = uncachedBuildMerkleTreeVisitor(walker, metadataProvider, artifactPathResolver); - merkleTreeCache.put(nodeKey, result); + // Deduplicate concurrent computations for the same node. It's not possible to use + // MerkleTreeCache#get(key, loader) because the loading computation may cause other nodes to be + // recursively looked up, which is not allowed. Instead, use a future as described at + // https://github.com/ben-manes/caffeine/wiki/Faq#recursive-computations. + var freshFuture = new CompletableFuture(); + var priorFuture = merkleTreeCache.asMap().putIfAbsent(nodeKey, freshFuture); + if (priorFuture != null) { + try { + return priorFuture.join(); + } catch (CompletionException e) { + Throwable cause = checkNotNull(e.getCause()); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof ForbiddenActionInputException) { + throw (ForbiddenActionInputException) cause; + } else { + checkState(cause instanceof RuntimeException); + throw (RuntimeException) cause; + } + } } + MerkleTree result = uncachedBuildMerkleTreeVisitor(walker, metadataProvider, + artifactPathResolver); + freshFuture.complete(result); return result; }