Skip to content

bazel l1: L1-resident LRU observation flusher - #38

Open
shreyas-blacksmith wants to merge 2 commits into
patchsetfrom
shreyas/bazel-l1-lru-flusher
Open

bazel l1: L1-resident LRU observation flusher#38
shreyas-blacksmith wants to merge 2 commits into
patchsetfrom
shreyas/bazel-l1-lru-flusher

Conversation

@shreyas-blacksmith

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

Copy link
Copy Markdown
Contributor

Summary

L1-resident half of the Bazel LRU accounting move: the L1 observes AC accesses and persists them as advisory artifacts; the web-side retention sweep (unchanged) consumes them.

  • cache/lruflush: buffers AC-access closures (cache.LRUObserver) per tenant storage prefix, dedupes by AC hash, and flushes one JSONL artifact per prefix every 5 minutes plus a synchronous drain at shutdown. One aggregation map, one serial flush goroutine, direct PUTs, no retries.
  • cache/s3proxy.PutArtifact: writes one artifact under a fully-composed key, routed by the request-scoped backend/bucket selection exactly like cache entries (so the sweep, which follows the namespace shard pin, can find it), tagged lru=true for the bucket ILM backstop. The data-plane circuit breaker is consulted read-only: artifact traffic fails fast on a sick shard but can never trip or heal the breaker customer traffic depends on (both directions regression-tested).
  • main.go: explicitly opt-in — the flusher runs only on trusted-mode nodes with an S3 proxy AND BAZEL_REMOTE_LRU_ARTIFACTS=1. Dark by default so a routine binary roll can never activate it fleet-wide; rollout enables per node (staging first) and widens with evidence.

Failure model

Everything here is advisory: under any pressure the correct response is to drop observations, count the drop, and move on — nothing can stall or fail a cache request.

  • Request path: RecordACAccess admits via TryLock; lock contention is a metered drop (contention), never a queued cache request. Full buffer (250k object refs process-wide) drops rather than flushing early. Oversized closures (>50k leaves) drop whole, never truncate.
  • Flush pass: bounded by a 2-minute wall clock (including the shutdown drain; worst case ~2 passes ≈ 4 min from SIGTERM if one is mid-flight), with per-(endpoint, bucket) failure suppression — a stalled backend costs one 30s PUT deadline per pass, never N of them. Abandoned/failed windows are counted per closure.
  • Memory: structurally bounded to the active window + one detached window + one serialized artifact; buffers detach O(1) under the lock.

Metrics

bazel_remote_lru_artifact_flush_total{trigger,result}, bazel_remote_lru_flush_observations_dropped_total{reason}, bazel_remote_lru_flush_closures_lost_total{reason}, bazel_remote_lru_flush_buffered_objects, bazel_remote_lru_flush_detached_objects, bazel_remote_lru_flush_pass_duration_seconds, flush size/entry histograms.

Deliberately not in this PR (trimmed after review): gRPC transport tuning (stream quotas, recv caps, keepalives), GetTree guards, and tree-validation byte caps. The L1 keeps stock inbound/outbound transport behavior; defensive limits return only with evidence they're needed.

Test plan

  • go test -race on all touched packages; full go test ./...; repeated flusher race runs
  • Bazel 8.4.1: all changed targets pass
  • CI: lint + new go test -race workflow green on this head
  • Unit coverage: access-order round-trip, dedupe/union, drop reasons (missing prefix, oversized closure, buffer full, contention), failed-upload window drop, pass deadline, per-backend suppression, blocked-sink memory bound, breaker no-trip/no-heal, per-tenant bucket routing, key collision, concurrent conservation
  • Staging E2E (pipeline): cold-cache smoke traffic → periodic flushes → schema-v1 artifacts in MinIO, zero drops, clean logs (verified on the trimmed predecessor head)
  • Staging smoke/soak on this exact head: real AC traffic drove a periodic flush (165 closures, 54 KB, ~53 ms pass), schema-valid artifact in MinIO, zero drops (incl. contention), zero closures lost, clean logs on both nodes

Buffer AC-access closures (cache.LRUObserver) per tenant storage prefix
and periodically flush them as JSONL artifacts to the tenant's S3
backend, using the artifact schema/key layout the web-side retention
sweep already consumes. s3proxy gains a narrow PutArtifact surface that
routes like cache operations but consults the data-plane breaker
read-only: advisory artifact traffic can fail fast on a sick shard yet
can never trip or heal the breaker customer traffic depends on.

The flusher is deliberately minimal: one aggregation map, one serial
flush goroutine, direct PUTs, no retries. Under any pressure (full
buffer, slow or failing backend) it drops observations and counts the
drop - the recency signal re-establishes itself on the next access.

Co-authored-by: Cursor <cursoragent@cursor.com>
@shreyas-blacksmith
shreyas-blacksmith force-pushed the shreyas/bazel-l1-lru-flusher branch from e5bad9b to 4fdb83c Compare August 5, 2026 21:02
@shreyas-blacksmith shreyas-blacksmith changed the title bazel l1 [fork §7]: L1-resident LRU observation flusher + server transport hygiene bazel l1: L1-resident LRU observation flusher Aug 5, 2026
Three review P1s. RecordACAccess now admits via TryLock: it runs
synchronously on the cache hit path, so lock contention (a concurrent
wide merge) becomes a metered drop instead of queueing a cache request
behind advisory bookkeeping. A flush pass (including the shutdown
drain) gets a 2m wall-clock bound plus per-backend failure suppression,
so a stalled artifact backend costs one PUT deadline, never N of them,
and can never hold a node roll hostage. The feature flips to explicit
opt-in (BAZEL_REMOTE_LRU_ARTIFACTS=1) so a routine binary roll cannot
activate it fleet-wide.

Also: closures-lost counter (per closure, so loss ratios are
computable), detached-objects gauge and pass-duration histogram,
breaker no-trip/no-heal regression tests, and a go test -race CI
workflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
@shreyas-blacksmith

Copy link
Copy Markdown
Contributor Author

Validation performed

Unit / race / build (head a4b35eb):

  • go test -race on all touched packages (cache/lruflush, cache/s3proxy, cache/disk, server, main); full go test ./...; flusher suite repeated 3x under race.
  • Bazel 8.4.1: all changed targets pass.
  • CI on this exact head: go test -race and lint green (the race workflow was added in this PR).

Behavioral pins (all in-tree tests):

  • Nonblocking admission: contention drops instead of blocking the hit path; concurrent conservation (flushed + counted drops = observations).
  • Flush pass: whole-pass deadline abandons remaining prefixes after one stalled PUT; per-(endpoint,bucket) suppression skips a failed backend's remaining artifacts; blocked-sink memory bound.
  • Breaker isolation both directions: 10 artifact failures leave the data-plane breaker closed; an artifact success cannot reset a 4-failure streak (5th still opens).

Staging E2E (2-node staging L1 fleet, real Bazel traffic via cold-cache smoke builds):

  • Pipeline verified end-to-end on the immediate predecessor head (4fdb83c, identical pipeline shape): periodic flushes (1,144 closures / 361 KB across two artifacts), artifacts landed under <tenantPrefix>lru/ in MinIO and passed schema-v1 validation, zero drops, clean journals, healthy gRPC throughout.
  • Three consecutive binary rolls exercised the shutdown drain cleanly ("Draining buffered LRU observation artifacts" on every stop).
  • Both staging nodes now run this exact head (binary sha256 c841abd6…) with the feature enabled via the new opt-in env; a smoke/soak against this head is running to collect the canary-gate numbers (prefix cardinality, pass duration, drop/loss rates).

Rollout posture: binary is opt-in (BAZEL_REMOTE_LRU_ARTIFACTS=1); the Ansible role defaults dark with staging opting in (FA 3c9983b70, pushed).

@shreyas-blacksmith

Copy link
Copy Markdown
Contributor Author

Exact-head staging gate complete (a4b35eb, binary c841abd6…): cold-cache AC traffic (googletest build) drove a periodic flush on node 2 — 165 closures / 54,367 bytes in a ~53 ms pass — artifact schema-valid in MinIO (window_end 22:05:44Z). Zero observation drops including the new contention reason, zero closures lost, no suppression or pass-deadline trips, clean journals on both nodes. All canary-gate signals (prefix cardinality, pass duration, drop/loss rates) nominal.

@shreyas-blacksmith
shreyas-blacksmith marked this pull request as ready for review August 6, 2026 13:04
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