Add opt-in CDC chunked uploads to GrpcStore#2595
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
c2e91a4 to
04b9fe9
Compare
04b9fe9 to
3e1dad9
Compare
3e1dad9 to
8b8b284
Compare
NativeLink's own transfers never used the REAPI chunking extension: GrpcStore's split_blob/splice_blob only forward a chunking client's RPCs, so worker output uploads move huge blobs whole on every build. With the new opt-in GrpcSpec.experimental_chunked_uploads, blobs at or above min_blob_size_bytes are split locally with FastCDC 2020 (identical parameters to the server's on-demand splitter so chunk digests are shared), only the chunks the backend reports missing are transferred in BatchUpdateBlobs groups, and the blob is assembled remotely with SpliceBlob. Windowed single-pass pipeline bounds memory to ~8MiB; blobs that could exceed max_chunk_count take the plain streaming path up front since a chunked upload cannot fall back once the stream is consumed; a chunk evicted before the splice surfaces as a retryable ABORTED. Measured (256MiB artifact, real gRPC over counted TCP, byte-verified): 5% clustered churn transfers 10.5% of the blob (9.5x); a 64KiB insertion transfers 0.4% (FastCDC resynchronizes where fixed blocks would resend everything); an idempotent re-upload transfers 0.03%. Cold uploads pay one hash+chunk CPU pass (~590ms floor for 256MiB), negligible on bandwidth-bound links. Metrics group chunked_uploads: chunked_uploads_total, chunks_sent, chunks_deduped, bytes_sent, bytes_deduped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
8b8b284 to
72f912f
Compare
53cabaa to
a230c00
Compare
a230c00 to
59dd3be
Compare
…oads * upstream/main: Fix clippy::cast_possible_wrap (TraceMachina#1476) docs(config-reference): regenerate for NativeLink (TraceMachina#2629)
a3dbfb7 to
1cfcde0
Compare
|
@erneestoc All I did here was catch it up to (merge in) main, add documentation, and add some validation logic to inform users/agents at startup that try to enable chunked uploads without the required backend capabilities that this feature requires. |
|
There are a couple fast follows so upon your approval you should merge. |
MarcusSorealheis
left a comment
There was a problem hiding this comment.
LGTM - pending author's re-review.
|
and @amankrx |
|
@MarcusSorealheis We hit a correctness failure while testing this PR CDC uploads on a filesystem-backed, size-capped CAS: after ~17.5 minutes, actions failed with Lost inputs no longer available remotely for the same 78-byte bundletool_experimental.build_data.txt digest, and Bazel’s automatic retry failed immediately on that digest too. The likely cause is CDC’s temporary storage/recency behavior: a large output exists as retained chunk blobs plus the assembled blob. Before #2627, SpliceBlob promotes the chunks in LRU order; when the assembled blob is committed under CAS pressure, eviction can choose an unrelated, still-action-cache-referenced input. That leaves AC pointing at bytes no longer in CAS. #2627 addresses that mechanism by demoting the CDC chunks before the splice commit’s eviction pass. It retains chunks normally, but makes them preferred eviction candidates under pressure—trading an incremental CDC hit for build correctness. We think #2595 should be reviewed/merged with #2627 (or an equivalent fix), not independently. We will validate the pair with a cold-cache correctness run first, then a controlled incremental large-artifact rebuild. Note that #2627 still requires sufficient CAS headroom; it prevents the chunk-recency eviction pathology but cannot make a genuinely undersized CAS safe. |
Problem
CDC chunking (#2497) covers the client↔CAS edge, but NativeLink's own
transfers don't use it:
GrpcStore'ssplit_blob/splice_blobare proxyforwarders only, so workers upload huge output artifacts whole on every
build, even when almost all of their content is already in the CAS from a
previous version.
Mechanism
Opt-in
GrpcSpec.experimental_chunked_uploads { min_blob_size_bytes (8MiB default), avg_chunk_size_bytes (512KiB default), max_chunk_count }. Blobsat/above the threshold are FastCDC-split with the same parameters as the
server's on-demand splitter (shared chunk digests), each ~8MiB window does
FindMissingBlobs→BatchUpdateBlobsfor the missing chunks only, thenone
SpliceBlobassembles the blob remotely. Single pass, memory boundedto the window. Blobs that could exceed
max_chunk_counttake the plainpath up front (the chunked path cannot fall back mid-stream). The digest
function is resolved once at
update()entry and threaded explicitly, sochunk hashes, the
digest_functionfield, and the blob digest cannotdiverge.
Measured (256MiB artifact, real gRPC over counted TCP, byte-verified)
Honest scope notes:
uniformly scattered small edits touch every chunk and save nothing —
falsified explicitly during benching.
min(8, available_parallelism)and doesthe same total CPU work as serial hashing (narrower burst, not more
work). The hash stage follows the instance digest function: measured
serial 473ms (SHA256) vs 117ms (BLAKE3) per 256MiB, cold chunked upload
872ms vs 478ms — BLAKE3 deployments roughly halve the cold cost.
the blocking pool (profiled: hashing was ~4x the FastCDC boundary pass;
parallelizing cut the pass from ~590ms to ~190ms per 256MiB). Still a
real cost on fast LANs, irrelevant when bandwidth-bound — hence opt-in
with a size floor. Pipelining window hashing with the previous window's
network flush is a named follow-up, as is a worker-side chunk-map cache
for re-upload short-circuiting.
GetCapabilitiesprobe yet: the operator must only enable thisagainst a chunking-capable backend (first large upload fails loudly
otherwise). Probe is a named follow-up.
experimental_remote_cache_compression(chunks transfer uncompressed);documented on both config fields.
Tests
7 chunked-upload tests (routing below threshold and above chunk-count cap,
chunk+splice byte-identity, missing-chunks-only transfer, splice-NotFound
semantics, duplicate
missing_blob_digeststolerance, config validation).Suites:
cargo check --tests --workspaceclean; bazel store+config 34/34with clippy pedantic + nightly rustfmt aspects. Metrics group
chunked_uploads.Related: #2591 / #2592 / #2593 (upload-path series); the download side
(worker chunk cache) is a design-stage follow-up.
Production-failure follow-up: a cold-build test surfaced eviction amplification on a size-capped CAS (chunks + assembled blob store each large output twice, and splice promotes chunks in the LRU, evicting unrelated still-referenced blobs). Root cause and server-side fix (post-splice chunk demotion) are in #2627, which predates this PR (any chunking client since #2497 triggers it); this PR's config docs now carry the ~2x-headroom sizing warning.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
This change is
Review update (2026-07-22)
The CDC upload path was hardened after adversarial review:
This feature remains opt-in and applies to NativeLink worker/StoreDriver uploads; ordinary external ByteStream uploads are unchanged. There is still no capability/seed negotiation, so operators must enable it only against a backend supporting the required Split/Splice behavior.
Focused Cargo and Bazel chunked-upload suites, formatting checks, and
git diff --checkpass.