143
143
import java .util .SortedMap ;
144
144
import java .util .TreeMap ;
145
145
import java .util .TreeSet ;
146
+ import java .util .concurrent .CompletableFuture ;
147
+ import java .util .concurrent .CompletionException ;
146
148
import java .util .concurrent .ConcurrentLinkedQueue ;
147
149
import java .util .concurrent .Executor ;
148
150
import java .util .concurrent .Phaser ;
@@ -168,7 +170,7 @@ public class RemoteExecutionService {
168
170
@ Nullable private final RemoteExecutionClient remoteExecutor ;
169
171
private final TempPathGenerator tempPathGenerator ;
170
172
@ Nullable private final Path captureCorruptedOutputsDir ;
171
- private final Cache <Object , MerkleTree > merkleTreeCache ;
173
+ private final Cache <Object , CompletableFuture < MerkleTree > > merkleTreeCache ;
172
174
private final Set <String > reportedErrors = new HashSet <>();
173
175
private final Phaser backgroundTaskPhaser = new Phaser (1 );
174
176
@@ -344,7 +346,7 @@ public boolean mayBeExecutedRemotely(Spawn spawn) {
344
346
}
345
347
346
348
@ VisibleForTesting
347
- Cache <Object , MerkleTree > getMerkleTreeCache () {
349
+ Cache <Object , CompletableFuture < MerkleTree > > getMerkleTreeCache () {
348
350
return merkleTreeCache ;
349
351
}
350
352
@@ -418,12 +420,34 @@ private MerkleTree buildMerkleTreeVisitor(
418
420
MetadataProvider metadataProvider ,
419
421
ArtifactPathResolver artifactPathResolver )
420
422
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
+ }
425
450
}
426
- return result ;
427
451
}
428
452
429
453
@ VisibleForTesting
0 commit comments