Skip to content

Commit 6d6fa81

Browse files
tjgqcopybara-github
authored andcommitted
Deduplicate concurrent computations of the same Merkle tree.
Currently, it's possible for concurrent actions to end up computing the same Merkle tree, even when the cache is enabled. This change makes it so that a later action waits for the completion of the computation started by an earlier action. Progress on #17923. Closes #17995. PiperOrigin-RevId: 522319291 Change-Id: I68ab952ed6357027ec71a67a104f91a684a7a040
1 parent 8e359e7 commit 6d6fa81

File tree

1 file changed

+31
-7
lines changed

1 file changed

+31
-7
lines changed

src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java

+31-7
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@
143143
import java.util.SortedMap;
144144
import java.util.TreeMap;
145145
import java.util.TreeSet;
146+
import java.util.concurrent.CompletableFuture;
147+
import java.util.concurrent.CompletionException;
146148
import java.util.concurrent.ConcurrentLinkedQueue;
147149
import java.util.concurrent.Executor;
148150
import java.util.concurrent.Phaser;
@@ -168,7 +170,7 @@ public class RemoteExecutionService {
168170
@Nullable private final RemoteExecutionClient remoteExecutor;
169171
private final TempPathGenerator tempPathGenerator;
170172
@Nullable private final Path captureCorruptedOutputsDir;
171-
private final Cache<Object, MerkleTree> merkleTreeCache;
173+
private final Cache<Object, CompletableFuture<MerkleTree>> merkleTreeCache;
172174
private final Set<String> reportedErrors = new HashSet<>();
173175
private final Phaser backgroundTaskPhaser = new Phaser(1);
174176

@@ -344,7 +346,7 @@ public boolean mayBeExecutedRemotely(Spawn spawn) {
344346
}
345347

346348
@VisibleForTesting
347-
Cache<Object, MerkleTree> getMerkleTreeCache() {
349+
Cache<Object, CompletableFuture<MerkleTree>> getMerkleTreeCache() {
348350
return merkleTreeCache;
349351
}
350352

@@ -418,12 +420,34 @@ private MerkleTree buildMerkleTreeVisitor(
418420
MetadataProvider metadataProvider,
419421
ArtifactPathResolver artifactPathResolver)
420422
throws IOException, ForbiddenActionInputException {
421-
MerkleTree result = merkleTreeCache.getIfPresent(nodeKey);
422-
if (result == null) {
423-
result = uncachedBuildMerkleTreeVisitor(walker, metadataProvider, artifactPathResolver);
424-
merkleTreeCache.put(nodeKey, result);
423+
// Deduplicate concurrent computations for the same node. It's not possible to use
424+
// MerkleTreeCache#get(key, loader) because the loading computation may cause other nodes to be
425+
// recursively looked up, which is not allowed. Instead, use a future as described at
426+
// https://github.com/ben-manes/caffeine/wiki/Faq#recursive-computations.
427+
var freshFuture = new CompletableFuture<MerkleTree>();
428+
var priorFuture = merkleTreeCache.asMap().putIfAbsent(nodeKey, freshFuture);
429+
if (priorFuture == null) {
430+
// No preexisting cache entry, so we must do the computation ourselves.
431+
try {
432+
freshFuture.complete(
433+
uncachedBuildMerkleTreeVisitor(walker, metadataProvider, artifactPathResolver));
434+
} catch (Exception e) {
435+
freshFuture.completeExceptionally(e);
436+
}
437+
}
438+
try {
439+
return (priorFuture != null ? priorFuture : freshFuture).join();
440+
} catch (CompletionException e) {
441+
Throwable cause = checkNotNull(e.getCause());
442+
if (cause instanceof IOException) {
443+
throw (IOException) cause;
444+
} else if (cause instanceof ForbiddenActionInputException) {
445+
throw (ForbiddenActionInputException) cause;
446+
} else {
447+
checkState(cause instanceof RuntimeException);
448+
throw (RuntimeException) cause;
449+
}
425450
}
426-
return result;
427451
}
428452

429453
@VisibleForTesting

0 commit comments

Comments
 (0)