Skip to content

bazel l1 [fork 6]: owed-upload ledger — shedding becomes deferral, not loss - #34

Draft
shreyas-blacksmith wants to merge 5 commits into
shreyas/bazel-l1-piece-5-minio-edgefrom
shreyas/bazel-l1-piece-6-owed-uploads
Draft

bazel l1 [fork 6]: owed-upload ledger — shedding becomes deferral, not loss#34
shreyas-blacksmith wants to merge 5 commits into
shreyas/bazel-l1-piece-5-minio-edgefrom
shreyas/bazel-l1-piece-6-owed-uploads

Conversation

@shreyas-blacksmith

@shreyas-blacksmith shreyas-blacksmith commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Restores the L1 ⊆ MinIO subset invariant (design ledger 30) that lossy write-through shedding violated. FindMissingBlobs answers 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).

  • Owed ledger (cache/s3proxy/owed.go): queue-full sheds in Put and terminal upload failures in UploadFile (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).
  • Sweeper: re-enqueues owed items through the normal upload queue — same workers, same breaker (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.
  • Coalescing: duplicate enqueues (same identity) are dropped at the door against an in-flight set. Matrix fan-out delivers the same missing blob from every host at once; MinIO's create-if-absent would 412 all but one; each queued duplicate pins an open FD — the queue's true scarce resource.
  • Settlement: created or already_exists settles debt; terminal failure re-records it; the in-flight key is released on terminal outcome.
  • Wiring: standalone-only (WithOwedLedgerDir from config/proxy.go; BlobSource back-reference injected in main.go after disk-cache construction — metricsDecorator promotes the method). The host-side embedded proxy keeps best-effort semantics by omission.
  • New series: 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-scoped OpenOwedBlob
  • Staging re-test 2026-08-03 (goal-state 5f9076a, both L1 nodes + fleet on train tip): all bars pass — backlog 40,719 == drops, rejected 0; drain to zero in 40.3 min (sweeper-cap-bound at 1,024/min); 40/40 shed digests convergence-proven in MinIO; mid-backlog restart resumed from snapshot and drained; read p99 1.50 ms during drain vs 1.38 ms baseline. Known residual (documented roll contract): in-queue items are not restart-persistent (~85k lost at the test restart); SIGTERM drain-to-ledger is the candidate fast-follow.

Made with Cursor


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled. (Staging)

shreyas-blacksmith and others added 2 commits August 3, 2026 18:37
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
shreyas-blacksmith force-pushed the shreyas/bazel-l1-piece-6-owed-uploads branch from 5f9076a to efe6c73 Compare August 3, 2026 22:38
shreyas-blacksmith and others added 3 commits August 4, 2026 08:43
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>
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.

1 participant