Fix compressed-download failure classification in GrpcStore wire compression - #4
Closed
walter-zeromatter wants to merge 2 commits into
Closed
Conversation
Review follow-ups to the GrpcStore wire-compression work. `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. Also: only take the compressed upload path for real digest keys (a string key's `into_digest()` hashes the key itself, which the remote's mandatory digest verification would reject), and skip the background drain spawn when the encoder finished cleanly and the reader is already at EOF. Tests: two new cases in the service suite. 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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `length: None` form of a resumed read was already correct before the range fix, because `read_limit: 0` means "to the end" on the wire. The form that actually over-requested is an explicit `length` that covers the blob, so run the same resume assertions for both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Review follow-ups on top of TraceMachina#2596 (
erneestoc:ec/grpc-wire-compression, atead7c03b). Base branch here is that PR's head, mirrored into this repo so the diff reads as review deltas only.The feature itself checks out: REAPI conformance is right (first-request
write_offsetuncompressed / subsequent cumulative-compressed,read_limit: 0mandatory on compressed reads, ranged reads forced to identity), the exact-multiple decoder fix is real, and the premise is accurate — before TraceMachina#2596 nothing in NativeLink ever requestedcompressed-blobs, only accepted and advertised it, so every worker↔CAS hop shipped raw bytes.Fixes
Pump-stage failures were classified as retryable.
get_part_compressedran pump errors throughis_retryable_code, but pump errors come from our own writer andbuf_channelreports a dropped receiver asInternal, which that function calls retryable. A consumer that hung up mid-download therefore returnedOk(Some(forwarded)), andget_partopened a second full-size identityReadstreaming 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, sincetry_join!polls decode first. The identity path has always treated writer errors as terminal (RetryResult::Err), so this also removes an inconsistency.A late error could discard a completed, verified download. If the upstream delivered the whole frame and then failed on the response trailer, decode and pump could both finish — writer closed, digest verified — while feed reported a retryable error, and the result was still a resume into an already-closed writer. 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
buf_channelsurfaces a sender dropped without EOF as an error, not as EOF), so that flag is a sound completion signal. A late error after it resolves as success.A resume kept the caller's original
length. The entry guard only admits alengththat covers the whole blob, so a resumed request fromoffset = forwardedran past the end of the blob and relied on the server clamping it. It now asks for exactlysize - forwarded.Two smaller ones. The compressed upload path is now gated on
is_digest_keylike the download path — for a string keyinto_digest()hashes the key itself, which the remote's mandatory digest verification would reject (unreachable today only because the 64KiB threshold outruns any plausible key length, but the asymmetry invites a future bug). And the background drain spawn is skipped when the encoder finished cleanly, where the reader is already at EOF anddrain()returns immediately.Tests
The resume path — the subtlest logic in the feature — had no coverage at all. The fake ByteStream now honors
read_offset/read_limitso a resumed range can be asserted byte-exactly, and grew a mid-frame abort mode. It also grew a mixed-entropy payload helper:make_contentcompresses far enough that an 8MiB blob encodes into a single 64KiB wire chunk, so the original interruption test could never fire mid-frame.Three new cases, all fails-without-fix:
interrupted_compressed_download_resumes_via_identity/..._with_explicit_length— assert the blob comes back byte-identical, exactly one fallback, a non-zero resume offset, andoffset + limit == size. Pre-fix, the explicit-length form requests past the end.compressed_download_consumer_hangup_does_not_refetch— pre-fix this issues an identity re-read into a dead channel (identity_reads == 1); post-fix zero.Verification:
cargo test: servicegrpc_wire_compression_test12/12, utilwire_compression_test2/2, storegrpc_store_test8/8 andgrpc_read_batching_test7/7.cargo clippy -p nativelink-store -p nativelink-service --testsclean; nightlycargo fmt --checkclean.End-to-end run against a real Bazel client
Two NativeLink tiers in one process — Bazel → front tier
:50051(fast_slowof a local filesystem store over agrpcslow tier) → upstream tier:50061(filesystem CAS/AC,remote_cache_compression: true). Thefast_slowwrapper is load-bearing for the test: a baregrpcstore is short-circuited by the ByteStream/CAS services, which downcast toGrpcStoreand forward the client's resource name verbatim, bypassingupdate/get_partand therefore wire compression entirely.Workload:
bazel build //lib/zerotracein an unrelated ~3.4k-action Rust/C++ repo, disk cache disabled, 1168 executed actions, 184MB of CAS content. Bazel↔front is identity in every arm (Bazel was not given--remote_cache_compression), so the arm-to-arm delta in loopback bytes is the front↔upstream leg — the code under test.Both arms stored byte-identical content (184MB each side, Bazel digest-verified every downloaded output), and the compressed arms logged zero errors or warnings beyond the startup connection race. Caveats worth stating plainly: single run per arm, loopback has no bandwidth limit, so the wall times are not a controlled latency measurement — the upload deltas are within noise (the local build dominates) and the download arm's 4.9s is one sample. The byte ratios are the trustworthy numbers. Downloads compress less than uploads because most of what Bazel actually fetched sits below the 64KiB threshold and stays identity.
Left alone deliberately
InvalidArgument/Unimplementedat compressed-stream start propagate, so one wrong flag upstream fails every blob ≥64KiB. The fallback machinery already exists (Ok(Some(0))), so a download could fall back once and warn loudly — but silent downgrade hides misconfiguration, and the current behavior is documented and tested. Reads as a call for the feature owner, not a review fix.StoreDriver::update.BatchReadBlobsstill sendsacceptable_compressors: vec![]andBatchUpdateBlobsis uncompressed — consistent with the feature's small-blob reasoning.zstdis now a dependency ofnativelink-util, hence transitively of nearly every crate. Unavoidable givennativelink-storecannot depend onnativelink-service.🤖 Generated with Claude Code