Skip to content

Add opt-in zstd wire compression to GrpcStore transfers - #2596

Merged
MarcusSorealheis merged 9 commits into
TraceMachina:mainfrom
erneestoc:ec/grpc-wire-compression
Jul 30, 2026
Merged

Add opt-in zstd wire compression to GrpcStore transfers#2596
MarcusSorealheis merged 9 commits into
TraceMachina:mainfrom
erneestoc:ec/grpc-wire-compression

Conversation

@erneestoc

@erneestoc erneestoc commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Problem

NativeLink accepts and advertises REAPI compressed-blobs/zstd (#2527),
but never sends it: every GrpcStore resource name is plain blobs/,
so worker→CAS output uploads and worker←CAS input downloads ship raw bytes
in both directions regardless of any compression setting. On
bandwidth-limited links this leaves the full compression ratio on the
table for NativeLink's own traffic.

Mechanism

Opt-in GrpcSpec.experimental_remote_cache_compression. Uploads and
full-blob downloads of blobs ≥64KiB use compressed-blobs/zstd (client
encodes at zstd level 1 — measured 3.7GB/s single-core, never the
bottleneck at ≤1Gbps; level 3 bought nothing on the test corpus). Ranged
reads and small blobs stay identity. Downloads digest-verify at EOF;
decoder-detected corruption is terminal (InvalidArgument), while
retryable mid-stream transport failures resume via an identity read at the
forwarded offset. Compressed uploads are explicitly non-resumable
mid-stream (the server contract): a transient failure fails fast with the
transport error after one attempt instead of burning the retry budget on
replays the server must reject. The streaming codec moved
nativelink-servicenativelink-util (service re-exports; server keeps
level 3).

Measured (token-bucket throttled real TCP, through this feature path, byte-verified both directions)

link payload wall (off → on) wire bytes
1Gbps compressible 96MiB (3.7:1) 712ms → 205ms up, 706ms → 205ms down 101.0MB → 26.9MB
100Mbps compressible 24MiB 1975ms → 492ms 25.2MB → 6.7MB
both incompressible random unchanged unchanged

3.5–4.4× wall on compressible artifacts at both link speeds and in both
directions; zero wall or size penalty on incompressible data.

Notes

Tests

9 feature tests incl. fails-without-fix regressions for non-resumable
uploads (old code: 4 attempts then terminal InvalidArgument; new: 1
attempt, prompt transport error) and early-completion return, plus a
deterministic corrupt-download-is-terminal property test. Suites:
cargo check --tests --workspace clean; bazel store+config+service+util
68/68 with clippy pedantic + nightly rustfmt aspects.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC


This change is Reviewable

Review update (2026-07-22)

The wire-compression path was hardened after adversarial review:

  • Compressed download feed/decode/pump tasks now cancel together on decoder failure, avoiding hangs when an upstream stream remains open.
  • Compressed upload early-completion paths drain the caller's remaining input without blocking the RPC completion; this preserves proxy-chain producer behavior.
  • Boolean compression configuration now supports the same expansion/parsing behavior as the other config booleans.
  • Added and registered service integration coverage for corrupt-download cancellation and early-completion uploads.

Focused Cargo and Bazel wire-compression suites, formatting checks, and git diff --check pass. The feature remains opt-in; compressed uploads remain non-resumable by design.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nativelink Ready Ready Preview, Comment Jul 29, 2026 6:39pm
nativelink-aidm Ready Ready Preview, Comment Jul 29, 2026 6:39pm

Request Review

NativeLink's internal hops never used REAPI compressed-blobs: GrpcStore
built plain blobs/ resource names for every upload and read, so worker
output uploads and input fetches shipped raw bytes in both directions
regardless of any compression setting (the zstd support from the server
work is accept/advertise only).

New opt-in GrpcSpec.experimental_remote_cache_compression: uploads and
full-blob downloads of blobs at or above 64KiB stream through zstd
(level 1; higher levels measured slower with no meaningful wire
reduction) using compressed-blobs/zstd resource names. Ranged reads and
small blobs keep the identity path. Downloads decode with size and
digest verification; a retryable mid-stream failure resumes via the
identity path at the already-forwarded uncompressed offset. The shared
streaming codecs move from nativelink-service to nativelink-util (the
service re-exports them), since nativelink-store cannot depend on the
service crate.

Also fixes a latent decoder bug found by the new tests: a blob whose
decompressed size is an exact multiple of the 128KiB decoder output
buffer made the loop poll the finished decoder once more, and the
new-frame input hint made the EOF check misreport a fully decoded
stream as "ended in the middle of a zstd frame" — spuriously failing
compressed uploads of exact-multiple-size blobs from any client.

Measured (token-bucket throttled real TCP, byte-verified, off -> on):
96MiB 3.7:1-compressible blob @1gbps: upload 712ms -> 203ms and
download 706ms -> 204ms (3.5x), wire 101MB -> 27MB; @100Mbps 24MiB:
1975ms -> 494ms (4.0x). Incompressible payloads: identical wall time,
no size penalty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
Hoist the two function-local enums above the first statements of their
functions and restore alphabetical order in the service test suite list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
@MarcusSorealheis

Copy link
Copy Markdown
Member

This has been tested and is looking good for our customers.

@MarcusSorealheis

Copy link
Copy Markdown
Member

@amankrx I think we want to try and get it in a 1.6.4 release.

@MarcusSorealheis

Copy link
Copy Markdown
Member

@amankrx

Match the identity path's idiom from the cast_possible_wrap cleanup on
main: saturating try_into instead of a bare usize-to-i64 cast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC

@MarcusSorealheis MarcusSorealheis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MarcusSorealheis
MarcusSorealheis merged commit 64ca974 into TraceMachina:main Jul 30, 2026
50 checks passed
walter-zeromatter added a commit to Reactor-Inc/nativelink that referenced this pull request Jul 30, 2026
Follow-ups to the wire-compression work added in TraceMachina#2596.

`get_part_compressed` classified pump-stage failures with
`is_retryable_code`, but pump errors come from our own writer and
`buf_channel` reports a dropped receiver as `Internal`, which counts
as retryable. A consumer that hung up mid-download therefore returned
`Ok(Some(forwarded))` and made `get_part` issue a second, full-size
identity Read streaming the remainder into a channel nobody was
reading. Pump failures are now always terminal: they mean either the
consumer went away or the decoder aborted, and the decoder's verdict
is already reported by the arm above it since `try_join!` polls
decode first.

The fallback also treated a late failure from another stage as a
reason to resume even when the download had already completed and
been verified. The pump now records that it forwarded the decoder's
EOF -- which the decoder only sends after the blob passes its size
and digest checks -- and a late error after that point resolves as
success instead of resuming into an already-closed writer.

On a genuine resume, the identity request now asks for exactly the
undelivered tail. It previously kept the caller's original `length`,
which the entry guard only permits when it covers the whole blob, so
the resumed request ran past the end of the blob and relied on the
server clamping it.

The compressed upload path is now taken only for real digest keys: a
string key's `into_digest()` hashes the key itself, which the
remote's mandatory digest verification would reject. The background
drain spawn is also skipped when the encoder finished cleanly and the
reader is already at EOF.

Tests: new cases in the service suite for the aborted-consumer and
resume paths. The resume path had no coverage at all -- the fake
ByteStream now honors `read_offset`/`read_limit` so the resumed range
can be asserted byte-exactly, and grew a mid-frame abort mode plus a
mixed-entropy payload helper, since `make_content` compresses far
enough that multi-megabyte blobs fit in one wire chunk. Resume is
asserted both with `length: None` and with an explicit `length`
covering the blob; only the latter over-requested before this change,
since `read_limit: 0` already means "to the end" on the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants