Skip to content

add http addr flag#9

Merged
altitude merged 1 commit into
mainfrom
dev/cli-flags
Jul 6, 2021
Merged

add http addr flag#9
altitude merged 1 commit into
mainfrom
dev/cli-flags

Conversation

@altitude

@altitude altitude commented Jul 6, 2021

Copy link
Copy Markdown
Member

No description provided.

@altitude altitude merged commit 8fc168e into main Jul 6, 2021
@altitude altitude deleted the dev/cli-flags branch July 19, 2021 14:18
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…back

Two related tightenings surfaced by review:

1. AssertPresent's Preload check is now strict Gen0-only via
   CacheSnapshotter.HasGen0Entry. The old HasEntry(gen0 or gen1) was
   too tolerant: admission's CacheGuaranteed verdict specifically
   asserted a Gen0 hit at case 0, and Path B (CheckCache case 1 +
   gen0-hit → Touch, not AssertPresent) guarantees no rotation between
   admission and apply for AssertPresent-emitting cases. A gen1-only
   state at Preload therefore signals a real bug (torn CheckCache
   read, predicted_index drift, Path B failure) — retryable
   ErrStalePreload is safer than proceeding, because letting the
   order run on a gen1 fallback would panic strict Del if the same
   order both reads AND deletes the key.

   A gen0 tombstone still counts as present (Scope.GetX sees the
   Deleted flag and returns ErrNotFound cleanly — legitimate delete
   between admission and apply, no divergence).

2. Remove the gen0→gen1 fallback in AttributeCache.Get. The fallback
   silently absorbed invariant #6 violations: a read on a key the
   proposer never preloaded that happened to still sit in Gen1 (pre-
   rotation legacy) would succeed instead of surfacing as ErrNotFound.

   Callers that legitimately need to consult Gen1 (MirrorPreload's
   gen1-wins, HasLiveEntry, snapshotter persistence) already use
   explicit Gen1() access. The only production caller of Get was
   KeyStore.Get / GetEntry / Delete — these become strict Gen0-only.

   Concretely for KeyStore.Delete on a gen1-only key: previously it
   fetched via fallback, passed the existence check, then panicked at
   strict AttributeCache.Del. Now it returns ErrNotFound early —
   safer contract on the primitive. The underlying invariant #6
   detection moves to the coverage gate (invariant #9) via Scope.GetX,
   which is where preload-declaration enforcement belongs.

Tests updated:
- TestAttributeCache_Rotate / TestCache_CheckRotationNeeded_* — assert
  strict Gen0-only Get semantics; use ac.Gen1().Get(...) for post-
  rotation entry access.
- TestCache_AllAttributeCachesRotate — same shift.
- TestKeyStoreDelete_PreloadContractViolation → renamed
  TestKeyStoreDelete_Gen1OnlyReturnsNotFound — pins the new clean-
  ErrNotFound contract on gen1-only Delete.
- TestPreload_AssertPresent_AcceptsGen1Fallback → renamed / rewritten
  TestPreload_AssertPresent_RejectsGen1Only — matches strict Gen0
  semantic.
- New TestCacheSnapshotter_HasGen0Entry (replaces HasEntry test) —
  pins the four cases (gen0 live / gen0 tombstone / gen1 live / gen1
  tombstone / absent).

Doc: docs/technical/architecture/core/plan-intent-verification.md —
the intent table now shows HasGen0Entry (strict) for AssertPresent,
with the rationale for choosing Gen0-only over the fallback semantics.
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…sent/ExpectAbsent

Replace per-intent Preload verification (AssertPresent/ExpectAbsent/Touch)
with a single try-promote pass. Every non-Value plan entry is now emitted
as coverage-only Declare and the FSM's MirrorTouch handles gen1→gen0
promotion uniformly at Preload — gen0 hit no-ops, gen1 hit promotes,
neither silently no-ops.

Correctness rests on two orthogonal guardrails, not per-intent checks:
- Admission-side CacheUnreachable (PR #1458): 2+ predicted rotations
  reject the proposal with ErrCacheHorizonExceeded, bounding the
  propose→apply window to at most one rotation.
- Coverage gate (invariant #9): the FSM only reads keys admission
  declared, so Declare + MirrorTouch is a complete substitute for the
  historical assertion machinery.

AttributeCache.Get is gen0-strict. Every declared key that exists
somewhere in cache is promoted to gen0 by MirrorTouch before the
handler runs, so the historical gen1 fallback in Get is redundant;
handler reads see the fresh value directly. Deletes on genuinely
absent keys now surface as clean ErrNotFound at KeyStore.Delete
rather than the strict-Del contract-violation panic.

Removed: Needs.AddExpectAbsent, AttributeSet.ExpectAbsent, all three
assertion intents from the resolver's emission path, plan.ErrStalePreload
sentinel + ErrStalePreloadDescribable adapter, CacheSnapshotter's
HasGen0Entry / HasLiveEntry, and machine.go's per-intent Preload switch.
The proto oneof variants (Touch / AssertPresent / ExpectAbsent) remain
for wire compat but the resolver never emits them.

Docs: plan-intent-verification.md rewritten for the unified model.
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Restores strict AttributeCache.Del (Gen0 must hold the key at delete time)
by shifting the invariant from "admission emits per-intent assertions" to
"Preload uniformly promotes gen1→gen0 for every declared key". The
propose→apply race window is bounded by the admission-side CacheUnreachable
guard (2+ predicted rotations rejected up front, PR #1458), and the FSM
coverage gate (invariant #9) binds the read horizon to admission's
declared preload set — those two guardrails make Declare + MirrorTouch a
complete substitute for the historical AssertPresent / ExpectAbsent /
Touch machinery.

Semantic changes:

- `AttributeCache.Get` is gen0-strict — no gen1 fallback. `MirrorTouch`
  promotes any gen1 entry into gen0 at Preload before handlers run.
- `AttributeCache.Del` returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract violations
  bubble up loudly per invariant #7 instead of silently desyncing.
- `writeCacheTombstone` writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next rotation)
  — keeping cache mem/disk equal for the same applied index (invariant #1).
- `protoSnapshotSlot.MirrorTouch` is a silent no-op when neither gen has
  the key. Under the coverage gate that state is legitimate: a Declare
  entry may cover a key that is only in Pebble (Value intent seeds those)
  or genuinely absent (handler ErrNotFound path).
- Resolver: `CacheGuaranteed` and `CacheNeedsTouch` both emit `Declare`
  now; `CacheMiss + Pebble-load-hit` still emits `Value(v)`. The proto
  variants `Touch` / `AssertPresent` / `ExpectAbsent` stay in raft_cmd.proto
  for wire compatibility with pre-EN-1242 plans but the resolver never
  emits them and the FSM Preload switch treats every non-Value intent
  identically via `MirrorTouch`.

Removed machinery:

- `plan.ErrStalePreload` sentinel (moved to a leaf `planerr` package so
  admission and state can reference `ErrCacheHorizonExceeded` without a
  cycle — that's all `planerr` holds now).
- `CacheSnapshotter.HasGen0Entry` / `HasLiveEntry`.
- The `touch_noop` panic that fired when `MirrorTouch` couldn't find the
  key in either generation.

Docs: `docs/technical/architecture/core/plan-intent-verification.md`
rewritten for the unified model — problem statement, Path A / Path B
race traces, the two guardrails, and the delete-site checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Restores strict AttributeCache.Del (Gen0 must hold the key at delete time)
by shifting the invariant from "admission emits per-intent assertions" to
"Preload uniformly promotes gen1→gen0 for every declared key". The
propose→apply race window is bounded by the admission-side CacheUnreachable
guard (2+ predicted rotations rejected up front, PR #1458), and the FSM
coverage gate (invariant #9) binds the read horizon to admission's
declared preload set — those two guardrails make Declare + MirrorTouch a
complete substitute for the historical AssertPresent / ExpectAbsent /
Touch machinery.

Semantic changes:

- `AttributeCache.Get` is gen0-strict — no gen1 fallback. `MirrorTouch`
  promotes any gen1 entry into gen0 at Preload before handlers run.
- `AttributeCache.Del` returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract violations
  bubble up loudly per invariant #7 instead of silently desyncing.
- `writeCacheTombstone` writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next rotation)
  — keeping cache mem/disk equal for the same applied index (invariant #1).
- `protoSnapshotSlot.MirrorTouch` is a silent no-op when neither gen has
  the key. Under the coverage gate that state is legitimate: a Declare
  entry may cover a key that is only in Pebble (Value intent seeds those)
  or genuinely absent (handler ErrNotFound path).
- Resolver: `CacheGuaranteed` and `CacheNeedsTouch` both emit `Declare`
  now; `CacheMiss + Pebble-load-hit` still emits `Value(v)`. The proto
  variants `Touch` / `AssertPresent` / `ExpectAbsent` stay in raft_cmd.proto
  for wire compatibility with pre-EN-1242 plans but the resolver never
  emits them and the FSM Preload switch treats every non-Value intent
  identically via `MirrorTouch`.

Removed machinery:

- `plan.ErrStalePreload` sentinel (moved to a leaf `planerr` package so
  admission and state can reference `ErrCacheHorizonExceeded` without a
  cycle — that's all `planerr` holds now).
- `CacheSnapshotter.HasGen0Entry` / `HasLiveEntry`.
- The `touch_noop` panic that fired when `MirrorTouch` couldn't find the
  key in either generation.

Docs: `docs/technical/architecture/core/plan-intent-verification.md`
rewritten for the unified model — problem statement, Path A / Path B
race traces, the two guardrails, and the delete-site checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Restores strict AttributeCache.Del (Gen0 must hold the key at delete time)
by shifting the invariant from "admission emits per-intent assertions" to
"Preload uniformly promotes gen1→gen0 for every declared key". The
propose→apply race window is bounded by the admission-side CacheUnreachable
guard (2+ predicted rotations rejected up front, PR #1458), and the FSM
coverage gate (invariant #9) binds the read horizon to admission's
declared preload set — those two guardrails make Declare + MirrorTouch a
complete substitute for the historical AssertPresent / ExpectAbsent /
Touch machinery.

Semantic changes:

- `AttributeCache.Get` is gen0-strict — no gen1 fallback. `MirrorTouch`
  promotes any gen1 entry into gen0 at Preload before handlers run.
- `AttributeCache.Del` returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract violations
  bubble up loudly per invariant #7 instead of silently desyncing.
- `writeCacheTombstone` writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next rotation)
  — keeping cache mem/disk equal for the same applied index (invariant #1).
- `protoSnapshotSlot.MirrorTouch` is a silent no-op when neither gen has
  the key. Under the coverage gate that state is legitimate: a Declare
  entry may cover a key that is only in Pebble (Value intent seeds those)
  or genuinely absent (handler ErrNotFound path).
- Resolver: `CacheGuaranteed` and `CacheNeedsTouch` both emit `Declare`
  now; `CacheMiss + Pebble-load-hit` still emits `Value(v)`. The proto
  variants `Touch` / `AssertPresent` / `ExpectAbsent` stay in raft_cmd.proto
  for wire compatibility with pre-EN-1242 plans but the resolver never
  emits them and the FSM Preload switch treats every non-Value intent
  identically via `MirrorTouch`.

Removed machinery:

- `plan.ErrStalePreload` sentinel (moved to a leaf `planerr` package so
  admission and state can reference `ErrCacheHorizonExceeded` without a
  cycle — that's all `planerr` holds now).
- `CacheSnapshotter.HasGen0Entry` / `HasLiveEntry`.
- The `touch_noop` panic that fired when `MirrorTouch` couldn't find the
  key in either generation.

Docs: `docs/technical/architecture/core/plan-intent-verification.md`
rewritten for the unified model — problem statement, Path A / Path B
race traces, the two guardrails, and the delete-site checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
…as promote primitive

Fixes a strict AttributeCache.Del contract violation on Delete-like
orders when a concurrent Save + rotation runs between propose and apply:

  T0 Admission: CheckCache(k) → CacheMiss → Declare (no seed)
  T1 Concurrent Save(k, v)         → Gen0[k] = v
  T2 Cache rotation                → Gen1[k] = v, Gen0[k] = ∅
  T3 Delete(k) applies             → strict Del sees Gen0∅ → PANIC

The fix relies on two orthogonal guardrails already in force on
release/v3.0:

  1. Admission-side CacheUnreachable rejects any proposal predicting
     2+ rotations between propose and apply (plan.ErrCacheHorizonExceeded,
     gRPC Unavailable / HTTP 503). This bounds the propose→apply race
     to at most one rotation, so any key admission observed anywhere in
     cache is still reachable via Gen1 at Preload.
  2. The coverage gate (invariant #9) binds every FSM cache read to
     admission's declared preload set — a Declare entry can only be
     read by an order that emitted it.

Given those bounds, Preload-time MirrorTouch is a complete substitute
for per-intent verification: for every Declare plan entry, promote any
Gen1 hit into Gen0 before the handler runs. Applied to T3 above, Gen0
gets seeded with v before strict Del runs and the panic is gone.

Semantic changes on the release/v3.0 baseline:

- AttributeCache.Get is gen0-strict — no gen1 fallback. MirrorTouch has
  already promoted anything that exists anywhere in cache before the
  handler reads.
- AttributeCache.Del returns an error (kv.KV signature updated) and
  refuses to fabricate a tombstone from Gen1. Preload-contract
  violations bubble up loudly per invariant #7 instead of silently
  desyncing.
- writeCacheTombstone writes only the current gen0 byte in 0xFF (the
  gen1 byte is left as harmless stale data, purged on the next
  rotation) — keeping cache mem/disk equal for the same applied index
  (invariant #1).
- protoSnapshotSlot.MirrorTouch is a silent no-op when neither gen has
  the key (previous behavior: touch_noop panic). Under the coverage
  gate that state is legitimate.
- Resolver: CacheGuaranteed and CacheNeedsTouch both emit Declare now;
  CacheMiss + Pebble-load-hit still emits Value(v). The proto's Touch
  variant is retained for wire compat with pre-refactor plans still in
  flight during a rolling upgrade (FSM Preload treats it identically to
  Declare via MirrorTouch).

Docs: docs/technical/architecture/core/plan-intent-verification.md
walks the Path A race, the two guardrails, and the strict-Del site
checklist.

Layered on top of PR #1475 (Needs generic dispatch refactor).
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 2, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag pushed a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)
gfyrag added a commit that referenced this pull request Jul 3, 2026
Replaces the systematic MirrorTouch-at-Preload pass with two in-place
primitives on AttributeCache:

  * Get: gen0 → gen1 fallback. Safe under the coverage_bits gate
    (invariant #9) — the fallback can only surface keys the proposer
    explicitly declared.
  * Del: tombstones in Gen0 when Gen0 hits; otherwise lazy-fabricates
    a Gen0 tombstone borrowing Gen1's tag. Gen1's live row stays
    untouched (shadowed by the Gen0 tombstone, purged on the next
    rotation). writeCacheTombstone still writes a single row to the
    Gen0 byte — memory equals disk (invariant #1).

Preload skips coverage-only AttributeCoverage entries entirely (only
seed intents call MirrorPreload). The MirrorTouch method / plumbing
and the AttributeCache.Touch / Cache.TouchByType helpers are deleted
as dead code. The CacheHit verdict collapses "already in Gen0" and
"Gen1-only" into a single admission signal; the FSM's read horizon is
still bounded to admission's declared preload set by coverage_bits.
CacheUnreachable (rejects proposals with ≥2 predicted rotations)
keeps the propose→apply race window bounded to at most one rotation,
which is what the fallback + lazy fabrication rely on.

Doc: docs/technical/architecture/core/plan-intent-verification.md
Regression: internal/infra/cache/keystore_delete_test.go +
internal/infra/state/cache_snapshotter_test.go
  (TestCacheSnapshotter_EN1242_DeleteAfterRotationCrashRestart)

Co-authored-by: Geoffrey Ragot <geoffrey@formance.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