Add opt-in join-the-flight ByteStream write dedup - #2591
Closed
erneestoc wants to merge 2 commits into
Closed
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
N concurrent ByteStream Write streams for the same digest are N full uploads over the wire and N full drains through the store stack. With the new opt-in ByteStreamConfig.experimental_write_dedup, exactly one concurrent upload per digest is elected the leading upload; concurrent duplicates drain-and-discard their stream while waiting and are acked with the REAPI early WriteResponse only once the leading upload durably commits. Uploads of already-durable digests complete early after a single existence check. If the leading upload fails or is cancelled, waiters receive a retryable ABORTED via a send-on-drop guard and one retry becomes the new leader. Resumed streams bypass dedup to preserve resume semantics. Waiters must drain their request stream: unread bytes would pin the HTTP/2 connection flow-control window and deadlock a leading upload sharing the connection (found by benchmark). Measured (real gRPC stack, byte-verified, dedup off -> on): 228x256KiB simultaneous same-digest uploads: server-received bytes 59.9MB -> 17.1MB (3.5x), store updates 228 -> 1; 64x2MiB staggered burst: 136.5MB -> 8.8MB (15.5x), wall 42ms -> 3ms. Metrics group write_dedup: flights_joined, early_completes, leader_failures, bytes_saved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
erneestoc
force-pushed
the
ec/bytestream-write-dedup
branch
from
July 22, 2026 01:24
ba86bab to
71a2b54
Compare
This was referenced Jul 22, 2026
erneestoc
marked this pull request as draft
July 22, 2026 19:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
N concurrent
ByteStream.Writestreams for the same digest are N fulluploads over the wire and N full drains through the store stack. Any build
whose actions fan out identical artifacts (e.g. hundreds of actions each
producing the same runtime library copies or plists) pays this as an upload
storm: the clients race
FindMissingBlobsbefore any upload commits, allsee "missing", and all upload.
A community profile of a ~9k-action iOS build measured client→CAS upload as
the single largest overhead bucket (7,468s wall across 7,245 actions), with
~228 actions producing identical outputs.
Mechanism
New opt-in
ByteStreamConfig.experimental_write_dedup. For each new upload(resumed streams bypass dedup to preserve resume semantics):
single-flight slot keyed by
DigestInfo, then runs one existence check.If the digest is already durable it responds immediately with the
spec-mandated early
WriteResponse(committed_size= full size, or-1for
compressed-blobsuploads). Otherwise it uploads exactly as before.drain-and-discard their request stream while awaiting the leading
upload's durable commit, then respond with the same early
WriteResponse.Draining is load-bearing: unread request bytes would pin the HTTP/2
connection flow-control window and deadlock a leading upload sharing
the connection (found by benchmark; the naive don't-read design hangs).
Store::update()returns. If the leading upload fails or is cancelled atany await point, a send-on-drop guard broadcasts a retryable
ABORTEDtoall waiters (retryable for both Bazel's and NativeLink's retriers) and one
retry becomes the new leader. The flight is removed from the map before
the broadcast, so late arrivals always start a fresh flight.
The REAPI spec explicitly defines this behavior ("if another client has
already completed the upload — which may occur in the middle of a single
upload if another client uploads the same blob concurrently — the request
will terminate immediately … the client should not attempt to retry").
This is the wire-dedup half of the upload-path work; #2592 (worker output
upload batching) is the companion that batches small worker outputs into
BatchUpdateBlobs — the two partition upload traffic and compose additively.
Measured (M-series, real gRPC stack, in-process transport, byte-verified)
N concurrent same-digest uploads, dedup off → on:
update()per digest in every shape —on a real worker fast tier that is one fsync instead of hundreds.
after their first chunks. Single-chunk blobs (≤64KiB) see storage-side
dedup only (their payload rides in the first message).
has()per new upload, no other change.Safety analysis
bytestream tests unchanged and green).
persist_stream_on_disconnect_timeout_s) untouched: uploads whoseUUID is already tracked bypass dedup entirely. A failed leader still parks
for resume; if it resumes while a retry-elected leader is mid-flight the
store-level dedup (Flush in detect_duplicate_upload #2528 et al.) covers the double write.
instances defer dedup to the terminal CAS server; a
GrpcStoreclientuploading against a dedup server tolerates the early response (covered by
test).
Drop(same pattern asFlushCoalescer's send-on-drop).Tests
write_dedup_completes_early_for_existing_blobwrite_dedup_fresh_upload_unchangedwrite_dedup_joins_inflight_upload(waiter blocks until leader commits)write_dedup_waiter_retries_after_leader_failure(ABORTED + retry wins)write_dedup_compressed_early_complete_returns_negative_onegrpc_store_client_tolerates_early_complete(bench file)pedantic + nightly rustfmt aspects;
cargo check --tests --workspace.Metrics:
write_dedupgroup —flights_joined,early_completes,leader_failures,bytes_saved.Review-round additions (8-angle adversarial review, pre-PR)
Fixed:
GrpcStore::updatenow drains its unconsumed reader after anearly-completed write RPC. Empirically verified (64MiB mid-stream test):
without this, the upload RPC succeeds but the upstream sender coupled to
the reader fails with "receiver disconnected", so proxy chains
(
fast_slow{slow: grpc}) failed deterministically for blobs that alreadyexist upstream. This is a latent bug on main against ANY early-completing
CAS (buildbarn, bazel-remote); this PR both triggers and fixes it.
write_dedup.leader_failuresonly counts failures with waiters actuallyjoined (previously every routine failed upload with the flag on counted).
record_write_result; single flightsettlement site in the waiter drain loop.
bytes_saveddocumented as an approximate upper bound (first-messagepayload can't be subtracted; compressed waiters counted at uncompressed
size).
Known limitations (documented, follow-ups):
active_uploads, soQueryWriteStatusfor a disconnected waiter reports no progress and theclient restarts from offset 0 (bytes are drained, not stored, so resume
has nothing to resume into). Follow-up: uuid→flight side map.
one slow leader can hit their own RPC deadlines and retry-loop until the
leader settles. Opt-in flag; workloads with highly asymmetric client
bandwidth on identical large blobs should keep it off or we add a size
cap knob as a follow-up.
persist_stream_on_disconnect_timeout_scan overlap with a retry-electednew leader (two full uploads; storage dedup reconciles — correctness
holds, bandwidth win temporarily inverts under flaky leaders).
has()per new upload. For FindMissingBlobs-guardedclients this is usually a miss; it is also exactly the path that produced
the 15.5× staggered-storm win. Compose with
existence_cacheto make thecheck cheap.
(success if the leader committed) — permitted by the REAPI early-
termination contract; noted because the same malformed stream without a
concurrent flight would error.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
This change is
Review update (2026-07-22)
The implementation was hardened after adversarial review:
The flag remains opt-in. Waiters still share the leader's outcome and do not provide resumable progress of their discarded upload stream.
Focused Cargo and Bazel ByteStream/GrpcStore suites, formatting checks, and
git diff --checkpass.