bazel l1: L1-resident LRU observation flusher - #38
Conversation
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>
e5bad9b to
4fdb83c
Compare
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>
Validation performedUnit / race / build (head
Behavioral pins (all in-tree tests):
Staging E2E (2-node staging L1 fleet, real Bazel traffic via cold-cache smoke builds):
Rollout posture: binary is opt-in ( |
|
Exact-head staging gate complete ( |
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), taggedlru=truefor 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 ANDBAZEL_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.
RecordACAccessadmits viaTryLock; 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.(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.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 -raceon all touched packages; fullgo test ./...; repeated flusher race runsgo test -raceworkflow green on this head