Repository navigation
disk: store client-compressed cas uploads as received, verify digests in the background - #54
Open
shreyas-blacksmith wants to merge 5 commits into
Conversation
… in the background A ByteStream Write of a compressed-blobs/zstd resource used to be decoded in the gRPC layer, then hashed and re-encoded into 1 MiB chunks by casblob.WriteAndClose, so a client-compressed upload cost more CPU on the cache than a raw one. CAS entries are already namespaced per storage prefix, so a blob that does not match its digest can only affect its uploader. With BAZEL_REMOTE_VERBATIM_ZSTD_UPLOADS=<workers> set, PutZstd writes the client's zstd stream to disk as a single-chunk casblob with no decode, hash or encode on the request path, and a background worker decodes it once to check size and sha256, evicting the entry on mismatch. The proxy write-through happens only after verification. Blobs larger than a uint32 chunk, or arriving while more than 65536 blobs await verification, fall back to the decoding path. Ranged reads of single-chunk blobs stream past the offset instead of decoding the whole blob into memory. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
…size Reject a compressed body longer than any valid encoding of the declared size before it can take more disk than was reserved, stop the verifier one byte past the declared size, cap what reads of an unverified single-chunk blob can emit, and stream ranged reads for every single-chunk blob rather than only those over 1 MiB. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The declared-size limit applied only to ranged reads; a full read took the generic remainder==0 path and returned every decoded byte. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… uncapped reader Readers with an unknown expected size (-1) got an empty body from the declared-size cap; use the header's uncompressedSize, which is the declared size for these blobs. The verifier reads through GetUncheckedReadCloser so a frame that expands past the declared size is seen and rejected rather than truncated into a match. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nk size A stored-as-received blob of exactly 1 MiB has chunkSize equal to the default, so the chunk-size test misclassified it and the verifier evicted a valid blob as decode_error. One chunk means two offsets; use that. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bb4b1ea. Configure here.
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.

Summary
Stacked on #53 (base is that branch). Off by default.
Today a client-compressed upload costs the L1 more CPU than a raw one. The gRPC layer decodes the client's zstd stream, then
casblob.WriteAndClosehashes the raw bytes and re-encodes them into 1 MiB chunks. So turning on--remote_cache_compressionon the client makes write storms worse, not better.CAS entries are already namespaced per storage prefix (
FileLocationForStoragePrefixID). A blob that does not match its digest can only hurt the tenant that uploaded it. That makes inline verification optional, so this PR moves it off the request path.With
BAZEL_REMOTE_VERBATIM_ZSTD_UPLOADS=<n>set (n = verifier workers):flowchart LR subgraph before [Before] direction TB a1[Write compressed-blobs/zstd] --> a2[Go zstd decode] --> a3[sha256] --> a4[zstd encode 1 MiB chunks] --> a5[disk + proxy] end subgraph after [After] direction TB b1[Write compressed-blobs/zstd] --> b2[frame to disk as one chunk] --> b3[commit, reply OK] b3 -.-> b4[background: decode once, check size + sha256] b4 -->|ok| b5[proxy write-through] b4 -->|mismatch| b6[evict entry] end before ~~~ afterFlow in code:
server.(*grpcServer).putUpload— compressed uploads callcache.PutZstdfirst. It returnsdisk.ErrVerbatimUnavailablebefore reading any bytes when the feature is off, the blob is larger than auint32chunk, or more than 65536 blobs are already waiting for verification. Then the existing decode +Putpath runs unchanged. Identity uploads are untouched.disk.(*diskCache).PutZstd→put(..., verbatim)→casblob.WriteZstdFrameAndClose: writes the normal casblob header withchunkSize = sizeand one chunk, copies the client's stream into it, fills the offset table. No decode, hash, or encode. The copy stops atMaxCompressedSize(size)(zstd's compress bound), so a stream longer than asize-byte blob can legitimately compress to is rejected before commit.commit, the job (key, hash, size,random, detached ctx) goes on the verifier queue.verifyVerbatimreopens the file and decodes it once throughGetUncheckedReadCloser(no size cap, so a frame that expands past the declared size is seen), checks byte count and sha256. On any failure itRemoveKeys the entry if therandomstill matches (so a later re-upload is not evicted). On success it does the proxyPutthatputskipped, usingcontext.WithoutCancel(ctx)so the storage prefix and S3 backend selection survive the request.header.singleChunk(), i.e. two offsets in the table) decode throughskipDecodedand are capped at the header'suncompressedSize, so they never emit more than the declared size and no longerDecodeAllthe whole blob into memory for a ranged read.Metrics:
bazel_remote_verbatim_zstd_puts_total,..._fallback_total{reason=disabled|size|backlog},..._verify_total{result=ok|hash_mismatch|size_mismatch|decode_error|read_error|gone},..._verify_pending.Not covered:
BatchUpdateBlobswithCompressor_ZSTDstill decodes inline (grpc_cas.go); Bazel uses ByteStream for anything over its batch limit, which is what matters for the write storms.Risks
Verdict: Data pending — staging write storm with
--remote_cache_compressionon the client. Unit tests cover round trip (incl. exactly 1 MiB), ranged reads, unknown-expected-size reads, mismatch/oversize eviction and the no-consume fallback; no load measurement yet.Worry about: between commit and verification a wrong blob is servable to its own tenant: a compressed Read at offset 0 returns the frame the client sent, uncapped; identity and ranged reads are capped at the declared size. Bazel checks digests on download so the symptom is a download error for that tenant on that blob until the verifier evicts it (seconds to minutes, visible as
verify_pending). Only applies when a client sends bad data.Checked, not worried: cross-tenant poisoning (CAS keyed under the storage prefix); existing on-disk blobs (format unchanged, only
chunkSizediffers for new blobs, readers already handle arbitrary chunk sizes); disk over-consumption (compressed body bounded byMaxCompressedSizebefore commit); unbounded unverified data (backlog cap falls back to inline verification); proxy receiving unverified blobs (write-through deferred untilok); feature off (env var unset → every compressed upload takes the old path, tests pass with fakes returningErrVerbatimUnavailable).If it goes wrong: rising
verify_total{result!="ok"}or tenant download errors; unsetBAZEL_REMOTE_VERBATIM_ZSTD_UPLOADSand restart, already-stored single-chunk blobs remain readable by the old binary.Link to Devin session: https://app.devin.ai/sessions/29afc24d202a4deab5cfbb2c46c87104
Open in Devin Desktop: https://app.devin.ai/desktop/session/29afc24d202a4deab5cfbb2c46c87104?variant=devin
Requested by: @shreyas-blacksmith
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.✅ Compressed upload stored as received, verified in background
After still stores the client's zstd frame byte-for-byte as one chunk, reads back intact both ways, and the verifier records ok; before re-encodes into eight 1 MiB chunks.
Merge-base
This PR
b707af6f8 of 9 claims checked
On an earlier commit
On an earlier commit
On an earlier commit
Warning
2 things look different from the PR description
Proof on
b707af6f(frombb4b1ea9)View full Proof report
Need help on this PR? Tag
@codesmith-botwith what you need. (Staging)