Skip to content

Let TaskDeduplicator callers decide which executions to join - #30516

Open
fmeum wants to merge 4 commits into
bazelbuild:masterfrom
fmeum:claude/task-deduplicator-can-join
Open

Let TaskDeduplicator callers decide which executions to join#30516
fmeum wants to merge 4 commits into
bazelbuild:masterfrom
fmeum:claude/task-deduplicator-can-join

Conversation

@fmeum

@fmeum fmeum commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Description

TaskDeduplicator deduplicated executions by key alone, so callers with different requirements either shared an execution that didn't satisfy all of them or had to be separated into different keys, which also gives up sharing in the direction where it is safe.

Every execution is now started with caller-provided attributes and every caller supplies a predicate over them, so the join relation can be asymmetric: a caller that needs little joins an execution that produces more, while a caller that needs more starts its own and takes over as the execution that subsequent callers see. The decision is made inside a single ConcurrentHashMap.compute, which also lets the retry loop around a concurrently canceled execution go away. executeIfNew and executeUnconditionally are replaced by a single execute.

maybeJoinExecution is removed as its only use is questionable: Having a DISCARD build wait for a KEEP build is not a clear performance win since the latter may issue network calls.

This also fixes a caller that cancels its future releasing its reference to the shared execution twice: IndividuallyCancelableFuture completed itself with setFuture, which cancels its argument when it has already been canceled. This was harmless as used today, but not obvious without referring to Guava code and potentially problematic for future changes.

Motivation

Simplify the interface of TaskDeduplicator and make its inner working more obvious. At the same time, make the reuse of ongoing executions more explicit and flexible in MerkleTreeComputer.

Build API Changes

No

Checklist

  • I have added tests for the new use cases (if any).
  • I have updated the documentation (if applicable).

Release Notes

RELNOTES: None

fmeum added 3 commits July 29, 2026 13:47
TaskDeduplicator deduplicated executions by key alone, so callers with different requirements either shared an execution that didn't satisfy all of them or had to be separated into different keys, which also gives up sharing in the direction where it is safe.

Every execution is now started with caller-provided attributes and every caller supplies a predicate over them, so the join relation can be asymmetric: a caller that needs little joins an execution that produces more, while a caller that needs more starts its own and takes over as the execution that subsequent callers see. The decision is made inside a single ConcurrentHashMap.compute, which also lets the retry loop around a concurrently canceled execution go away. executeIfNew and executeUnconditionally are replaced by a single execute.

MerkleTreeComputer's BlobPolicy is such a relation: a KEEP computation is satisfied by an ongoing KEEP_AND_REUPLOAD one, but not the other way around, since KEEP wouldn't reupload the blobs that the remote cache lost. Both policies share a key, and KEEP_AND_REUPLOAD relied on executeUnconditionally to stay off a KEEP computation. That dropped the ongoing execution and started a new one in two separate map operations, so a KEEP computation installed by a concurrent caller in between was joined after all and the reupload silently didn't happen.

Also fix a caller that cancels its future releasing its reference to the shared execution twice: IndividuallyCancelableFuture completed itself with setFuture, which cancels its argument when it has already been canceled. The outcome is now copied over manually. The stray release is inert today because the execution has left the map by the time it lands, but the invariant is load-bearing for any change that keeps completed executions around.

Claude-Session: https://claude.ai/code/session_014jwpqcsMgZBr1aAxuKCDax
A DISCARD computation only needs the root digest, which is known as soon as the tree has been built, but the future of an uploading computation only completes after ensureInputsPresent has pushed the subtree's blobs to the CAS. Joining one therefore traded a local tree build for a wait on the network, and it did so on the path that gates whether an action runs at all. Under dynamic execution, the case it was written for, it coupled the local branch's cache check to the remote branch's upload - precisely the dependency that dynamic execution exists to avoid.

Uploading computations are kept under a separate cache key, so dropping the explicit cross-key lookup is all that is needed. It was the only caller of TaskDeduplicator.maybeJoinExecution, which is removed along with it.

Claude-Session: https://claude.ai/code/session_014jwpqcsMgZBr1aAxuKCDax
A KEEP_AND_REUPLOAD computation is satisfied by an ongoing computation with the same policy, but only if that computation started after this one discovered that the remote cache had lost the blobs. An earlier reupload may already have uploaded them before they were lost, in which case joining it doesn't restore anything and the retry fails with the same CacheNotFoundException.

executeUnconditionally used to provide this implicitly: it dropped the ongoing computation whatever its policy, so a reupload never joined one that had started earlier. Expressing the requirement as a predicate over the blob policy alone lost that, since KEEP_AND_REUPLOAD accepts KEEP_AND_REUPLOAD.

The in-flight attributes therefore also carry a sequence number, claimed before the computation is registered, so that every computation that is already ongoing has a lower one than the call that is looking at it. Only reuploads compare it; a weaker request ignores it and keeps joining any computation that retains enough.

Claude-Session: https://claude.ai/code/session_014jwpqcsMgZBr1aAxuKCDax
Comment thread src/main/java/com/google/devtools/build/lib/concurrent/TaskDeduplicator.java Outdated
@fmeum
fmeum marked this pull request as ready for review July 29, 2026 15:08
@fmeum
fmeum requested a review from a team as a code owner July 29, 2026 15:08
@github-actions github-actions Bot added team-Remote-Exec Issues and PRs for the Execution (Remote) team awaiting-review PR is awaiting review from an assigned reviewer labels Jul 29, 2026
@fmeum

fmeum commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

@bazel-io fork 9.3.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR is awaiting review from an assigned reviewer team-Remote-Exec Issues and PRs for the Execution (Remote) team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant