cache: the Store admission/serve sidecar seam (RPZ phase 4 prerequisite) - #601
Merged
Conversation
…byte serve The admission/serve seam RPZ's response-IP phase builds on, policy-free by itself (docs/rpz-design.md §5.6 item 6). Three pieces: - CacheEntry carries an atomic sidecar pointer beside its immutable payload, the same shape as the prefetch claim: opaque policy state a wired evaluator computed from the entry's own stored records. nil is load-bearing — unknown, never clean. - Admission: a single evaluator wired into the Store stamps every entry at every door — the SetFromResponse funnel, the prefetch CAS replacement, and the compatibility Set (the audit found three doors where the design counted on the writer alone). An entry that predates the wiring is evaluated and stamped by the first decoded serve. - Serve: a WireHitGate is consulted before every record-bearing byte serve — the wire-born exact hit, the Msg-born inline byte path, and the composed chase, which shows the gate one sidecar per segment in chain order (a whole-chain verdict on the alias would go stale when the target refreshed under it). A false verdict declines to the decoded path; composite denial classes carry no records and are not gated. Wired through Setup's marker pattern (SidecarPolicyProvider -> SidecarPolicySetter), first provider wins. Unwired, the seam is one nil field check per hit: the existing zero-alloc hit-class pins run unchanged, and every stamp and gate call is mutation-pinned by the seam tests.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #601 +/- ##
==========================================
- Coverage 86.89% 86.88% -0.02%
==========================================
Files 179 180 +1
Lines 22732 22801 +69
==========================================
+ Hits 19754 19810 +56
- Misses 2968 2981 +13
Partials 10 10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…erve, counting after the commit
Both findings land on the same joint and one contract change closes them:
the gate's boolean became a verdict, and accounting left it entirely.
- Judge{WireHit,WireChase} return Serve, Decode, or Restamp, and are
pure decisions over the sidecars. Decode means policy wants the full
message and the sidecar itself was fine; Restamp means the sidecar is
unusable — unevaluated, or stamped under a generation the gate no
longer accepts. The decoded serve acts on Restamp by re-evaluating the
entry's records and CASing over the judged pointer, stale and nil
alike, so a policy reload no longer strands live entries on the
decoded path until eviction. The verdict is judged once per hit,
before the byte/decoded split, internal serves included — the
Msg-path chase reaches its segments through internal sub-queries,
which is where a stale segment gets its restamp.
- Count{WireHit,WireChase} fire once per byte-served hit, after the
transport accepted the bytes — past the last point a serve can still
fall back to the decoded path (writer readiness, chain mismatch,
build, the fallback sentinel) and be counted a second time by the
policy writer there. A transport failure after the commit counts,
matching the response-counts-as-written parity the serve paths
already keep.
Every placement is pinned: stale-restamp against a nil-only CAS,
fallback-after-approval counting nothing on both the Msg-born and
wire-born paths, committed serves counting exactly once on both, and
the chase count with its segments.
Moving the segment array out of the gate branch for the commit-time count made it visible to escape analysis through the interface call, so every ungated chase paid a heap allocation the hit-class pins forbid — CI caught it. The slice is now made on the gated branch only and stays in scope for the count; an ungated chase touches nothing.
…path to the ungated cost The gated chase still allocated: the slice handed to JudgeWireChase escaped through the interface call, so wiring the gate put a heap allocation on every cache-contained chase hit — the zero-allocation hit contract holds with policy live, not only without it. The chain is now a bounded value type (SidecarChain, capacity asserted against the chase depth at compile time) passed by value through Judge and Count, which closes the slice-escape class in the type system rather than in review. The new pin closes the blind spot the finding named: the allocation CI never wires a gate, so a baseline-relative AllocsPerRun test serves the exact and chase hits against two caches — one bare, one behind an allocation-free Serve gate — and refuses any gated surplus. Verified to fire: an escaping allocation planted on the gated branch reads 3.0 against a 2.0 baseline.
m3ok
approved these changes
Aug 31, 2026
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.
The seam docs/rpz-design.md §5.6 requires before the response-IP phase: policy state carried beside cache entries, stamped at admission, judged at the byte serve. This PR is deliberately policy-free — no RPZ logic, no behavior change while unwired — so the seam's design can be reviewed on its own, as the design doc prescribes.
Design
The sidecar.
CacheEntrygains one atomic pointer beside its immutable payload (the same shape as the existing prefetch claim — a mutable atomic the entry carries without being copied). The cache never reads the value.nilis load-bearing: an absent sidecar means unknown, never clean — because a fourth admission door may exist someday, and the audit indeed found one the design's callsite list didn't (below).Admission. One evaluator, wired into the
Store, stamps every entry with the result of evaluating that entry's own stored records — the truth, post-filter, never the client's copy. The design named three doors (writer, resolver's direct store, prefetch replacement); reading the code found they funnel through three construction sites, and all three stamp:setFromResponseWithKey— the writer'sSetFromResponseWithKey/Scopedand the resolver'sSetFromResponseWithCutfunnel here;ReplaceIfCurrent— the prefetch CAS replacement;Cache.Set→SetEntryWithKey— the compatibility door for prefetch write-backs and plugin callers, which the design's list did not carry.An entry admitted before the seam was wired carries
nil; the first decoded serve evaluates the message it already built and CAS-stamps it, so such an entry pays the Msg path once, not forever.Serve. A
WireHitGateis consulted before every record-bearing byte serve, with the sidecar(s) the reply would be built from:serveHitFromWire),handleCacheHit),serveChaseHit) — one sidecar per segment, in chain order: a whole-chain verdict on the alias entry would go stale the moment the target refreshed under it (§5.6 item 4).A false verdict declines to the decoded path, where the policy middleware's own response writer sees a full message. Composite denial classes (subtree cuts, failure state) carry no stored records and are never gated. The gate signature is entry-only by design: per-query policy state stays in the policy middleware, which steers a query off the byte path entirely by withholding its writer's wire capability — so the gate never needs the request, and a verdict is a pure function of the stored state.
Wiring. Setup's marker pattern:
SidecarPolicyProvider(rpz, in the next PR) →SidecarPolicySetter(cache), first provider wins with a warning, injected before the pipeline publishes so the atomic store is the happens-before barrier — same discipline as the existing Store/Queryer wiring.Cost while unwired
One nil field check per hit and 8 bytes per entry. The existing zero-alloc hit-class pins (
TestServeRawWireHitAllocatesNothing, the acquire pins) run unchanged and green.Verification
-raceon middleware + cache, lint clean.Phase 4 proper (rpz-ip triggers, the match-list sidecar values, serve-time merge, winner-bounded counting) follows in its own PR on top of this seam.