bazel l1 [fork 6]: owed-upload ledger — shedding becomes deferral, not loss - #34
Draft
shreyas-blacksmith wants to merge 5 commits into
Draft
Conversation
Restore the L1 ⊆ MinIO subset invariant that lossy write-through
shedding violated: FindMissingBlobs answers local-first, so a blob
whose backend upload was shed is reported present forever and never
reaches MinIO.
- Queue-full sheds and terminal upload failures (failed PUT, breaker
refusal) record the upload identity in a bounded, snapshotted
per-backend owed ledger instead of being silently lost.
- A background sweeper re-enqueues owed items through the normal
queue (same workers, breaker, outcome accounting), yielding to
live traffic: breaker closed + queue under half capacity only.
Locally-evicted owed blobs settle as void (honestly missing
everywhere; the client re-upload path heals both tiers).
- Duplicate enqueues coalesce against an in-flight identity set
(kind+hash+prefix+bucket): matrix fan-out re-delivers the same
blob from many hosts, MinIO create-if-absent 412s all but one,
and every queued duplicate pins an open FD.
- Standalone-only: wired via WithOwedLedgerDir + a BlobSource
back-reference injected after disk-cache construction. The
host-side fallback proxy keeps best-effort semantics.
- New series: owed_uploads gauge, owed_sweep_total{result},
owed_rejected_total, upload_queue_coalesced_total.
Co-authored-by: Cursor <cursoragent@cursor.com>
…ttle race Self-review findings on the owed-upload piece: - P0: the ledger dir lived inside the cache root, and the disk cache's startup scan hard-fails on unexpected top-level dirs — every standalone boot with an S3 proxy crashed with 'unexpected dir: s3-owed'. The dir name is now a shared cache.OwedLedgerDirName constant and both scan paths skip it like lost+found; regression test constructs the disk cache over a populated root. - P2: the sweeper's evicted-blob void-settle could race a concurrent client re-Put whose upload coalesced against the sweeper's own in-flight claim, erasing the only record that MinIO lacks the object. Owed entries carry a per-add seq; coalesced Puts re-record their debt, and void settlement is seq-conditional (settleVoid) so a stale observation no-ops. Upload-success settlement stays unconditional (proof the object exists discharges any seq). - P2: OpenOwedBlob no longer holds the global disk mutex across os.Open (a sweep batch is hundreds of opens; losing the eviction race surfaces as the open error callers already treat as void). - P3: snapshot filenames append a hash of the raw backend key (sanitization is lossy; colliding keys would clobber snapshots). - P3: a failed snapshot write re-dirties the ledger so the next pass retries instead of stretching the 15s staleness window forever. Co-authored-by: Cursor <cursoragent@cursor.com>
shreyas-blacksmith
force-pushed
the
shreyas/bazel-l1-piece-6-owed-uploads
branch
from
August 3, 2026 22:38
5f9076a to
efe6c73
Compare
Breaker recovery relied on read-path probes ("calls arrive constantly"),
but an idle node has none: after a MinIO outage the breaker stayed open
forever, sweepOwedOnce returned early at its breaker gate, and the owed
ledger never drained — recovery of the subset invariant was traffic-gated
(observed live on staging-2, 2026-08-04 chaos drill: 12 debts frozen for
200s+ post-recovery until an unrelated read probed the breaker closed).
When the sweeper holds debts and finds the breaker non-closed it now
volunteers a bucket-existence check as the half-open recovery probe,
bounded by sweepProbeTimeout. allow() still enforces the open-window
cooldown, so this costs at most one cheap RPC per sweep pass while the
backend stays dark, and the ledger drains on the pass after recovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
…ists BucketExists issues HEAD-bucket, which prefix-scoped MinIO policies reject with 403 — a permanent probe failure that re-opened the breaker every sweep pass on staging-2 (6 half-open -> open flaps in 3 minutes) while the ledger stayed frozen. StatObject on a sentinel key is the read path's exact call shape, so it needs only permissions the deployed credentials demonstrably hold, and its NoSuchKey answer already counts as success in breakerReadOutcome. Probe failures now log the underlying error so a policy-shaped failure is visible in the journal. Co-authored-by: Cursor <cursoragent@cursor.com>
A synthetic sentinel key fails the same way HEAD-bucket did: prefix-scoped MinIO policies answer Access Denied for any path outside the tenant prefixes this node serves, so the probe re-opened the breaker every sweep pass (staging-2, 2026-08-04, "probe failed: Access Denied"). Stat the object key of an owed entry instead — by construction a path these credentials can write and one the Contains path HEADs routinely — so the probe only fails when the backend is genuinely sick. Co-authored-by: Cursor <cursoragent@cursor.com>
6 tasks
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
Restores the L1 ⊆ MinIO subset invariant (design ledger 30) that lossy write-through shedding violated.
FindMissingBlobsanswers local-first, so a blob whose backend upload was shed was reported present to every client forever — never re-uploaded, never reaching MinIO: invisible to rehydration, new ring members, and footprint accounting. Surfaced as P1 by the 2026-08-01 staging load experiment (~1.9M sheds during the write staircase).cache/s3proxy/owed.go): queue-full sheds inPutand terminal upload failures inUploadFile(failed PUT, breaker refusal) record the upload identity (kind + hash + resolved prefix + resolved bucket). Bounded at 2^18 entries per backend (~50 MiB; at capacity new debt is rejected with a counter — older entries sit closer to LRU eviction, which self-resolves them). Snapshotted atomically beside the cache dir every sweep pass, reloaded at startup (≤15 s loss window).ExecuteNoProbe), same outcome accounting — and yields absolutely: runs only while the breaker is closed and the queue is under half capacity. An owed blob already evicted locally settles as void: it is then honestly missing everywhere and the normal client re-upload heals both tiers.createdoralready_existssettles debt; terminal failure re-records it; the in-flight key is released on terminal outcome.WithOwedLedgerDirfromconfig/proxy.go;BlobSourceback-reference injected inmain.goafter disk-cache construction —metricsDecoratorpromotes the method). The host-side embedded proxy keeps best-effort semantics by omission.bazel_remote_s3_owed_uploads(gauge — the live L1⊆MinIO divergence),owed_sweep_total{result},owed_rejected_total,upload_queue_coalesced_total.Deliberately NOT owed: items in the queue at process stop (the roll contract's bounded loss — owing every enqueue would make the ledger a high-churn queue mirror), and host-tier forwards (hosts own no durable tier).
Test plan
go test ./cache/s3proxy/ ./cache/disk/ ./config/— new tests: shed→owed→sweep→repay round-trip against fake S3, duplicate coalescing, breaker-open and busy-queue sweep yield, evicted-blob void settlement, snapshot reload + corrupt-snapshot recovery, capacity rejection, raw-bytes/prefix-scopedOpenOwedBlobMade with Cursor
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled. (Staging)