Skip to content

Fix rewinding bugs with --remote_cache_async - #30675

Draft
fmeum wants to merge 3 commits into
bazelbuild:masterfrom
fmeum:rewound-action-synchronizer-fixes
Draft

Fix rewinding bugs with --remote_cache_async#30675
fmeum wants to merge 3 commits into
bazelbuild:masterfrom
fmeum:rewound-action-synchronizer-fixes

Conversation

@fmeum

@fmeum fmeum commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Description

Output upload tasks were never unregistered: outputUploadTasks entries were only removed when the corresponding action was rewound, so every asynchronously uploaded spawn left behind an entry retaining the action and the upload's future for the rest of the build. Because registerOutputUploadTask threw on a second registration for the same action, an action uploading outputs twice would also have failed the build. Today this could only happen for remote include scanning since other multi-spawn actions all happen to return true from mayModifySpawnOutputsAfterExecution(), but that isn't necessarily going to remain the case. Tasks are now unregistered on completion, and multiple concurrent tasks per action are permitted.

Cancelling an upload before it started blocked forever: Guava's InterruptibleTask#run skips the task body entirely if the future is already done, so a cancel(true) landing between submit() returning and the virtual thread entering the body meant neither uploadDone.countDown() nor onUploadComplete ever ran. The rewinding thread then waited on the latch forever while holding the write lock, and the completion callback was never invoked, stranding any spawn deduplicating against that execution. The upload is now guarded by a claim that either the background task or the canceller wins, and the winner runs the completion callback.

Motivation

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 August 11, 2026 15:18
This guards against interrupts and Errors causing locks to be held indefinitely.

Along the way add more profiler spans.
Actions generated by an `ActionTemplate` have `TreeFileArtifact`s as
outputs, whose generating action key is that of the expansion action.
Consumers, however, depend on the parent tree artifact and thus lock the
key of the *template*, so a rewound expansion action acquired a write
lock that no consumer ever contended on and could prepare for its
re-execution while its outputs were still being read.

Both the outputs of a rewound action and the inputs of any action are
now mapped to the generating action key of the outermost tree artifact
containing them, which is the template for the outputs of an expansion.
This key is shared by all output trees of a template and is thus still a
single lock per action - the proof of deadlock freedom relies on this.

Since the actions of a single expansion may form a chain, an action can
have an input that is guarded by the very key it holds the write lock
of. As the locks aren't reentrant, a rewound action skips the read lock
of that key.

Also keys `outputUploadTasks` by `ActionLookupData` instead of by the
`Action` instance, which isn't stable across rewinding for actions
generated by an `ActionTemplate`.
Fixes two issues in the synchronization of rewound actions with the
consumers of their outputs:

* Output upload tasks were only ever removed from `outputUploadTasks`
  when the corresponding action was rewound, so they accumulated (and
  retained the action as well as its spawn result) for the duration of
  the build. Since `registerOutputUploadTask` threw on a second
  registration for the same action, an action uploading its outputs
  twice would also have failed the build. Tasks are now unregistered on
  completion and multiple concurrent tasks per action are permitted.

* Cancelling an output upload before the background task started running
  blocked forever: the body of a task submitted to an `ExecutorService`
  is skipped entirely if its future is cancelled before it starts, so
  neither the latch nor the completion callback (which closes the
  in-flight execution in `RemoteSpawnCache`) ever ran. The upload is now
  started via `execute` and cancelled by interrupting its thread, which
  always runs the body and thus the completion callback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant