Skip to content

draft(stage7-p0): P0 integration branch — CI evidence only, merge held for F1#29

Merged
sqlrush merged 30 commits into
mainfrom
stage7-p0-integration
Jul 9, 2026
Merged

draft(stage7-p0): P0 integration branch — CI evidence only, merge held for F1#29
sqlrush merged 30 commits into
mainfrom
stage7-p0-integration

Conversation

@sqlrush

@sqlrush sqlrush commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Integration of PR#26 @ 5b8e8ef + PR#25 @ ea2cdc9 + PR#24 @ d8b1d6d on main e7ef126, for 4-node S1/S2 acceptance testing. NOT for merge (F1 gate closed); draft for fast-gate CI evidence only.

Spec: spec-4.6a-grd-recovery-liveness.md / spec-2.29a-reconfig-marker-async-lmon-liveness.md / spec-6.15b-xid-authority-native-era.md

SqlRush added 30 commits July 8, 2026 15:53
Publish and seal native-era XID authority after seed shutdown, adopt
prehistory from per-node anchors, and max-merge startup nextXid before
ShmemVariableCache seeding.

Spec: spec-6.15b-xid-authority-native-era.md
A second cluster.enabled=off seed pass on a SEALED authority re-opens the
native era: clear SEALED at bootstrap so a crash of that pass leaves
joiners fail-closed (53RB5, unsealed) instead of silently adopting the
previous pass's stale high-water -- false-invisible for every xid the
crashed pass consumed.  The pass's own clean shutdown re-publishes and
re-seals monotonically.

Adds cluster_xid_authority_begin_native_run(), the bootstrap enabled=off
arm call, unit coverage (seal kept-hw/cleared-flag/idempotent/re-seal),
and t/361 N4 (sealed pass-1 -> unsealing pass-2 boot -> immediate-kill ->
joiner refuses adoption).

Spec: spec-6.15b-xid-authority-native-era.md
…n-gate window

spec-6.15b D6 flipped both tests to online_join+xid_striping: formation now
passes through the join gate, whose home-block rebuild window transiently
answers 'block master is recovering ... retry is safe' -- even at
connection time, via the pg_authid read.  Retry the first-contact probes
and first DDLs (bounded, 60s); every semantic assert stays strict.

Spec: spec-6.15b-xid-authority-native-era.md
…I3 recheck, P1-1/P1-2 hard assertions, t/363 rename

Review r1 dispositions (0 P0 / 3 P1 / 2 P2, all fixed):

- P1: join Phase-1 PREPARE TIMEOUT/non-ACK now drains best-effort and still
  publishes the staged JOIN_PENDING (the pre-async code ignored PREPARE
  results; aborting reverted the joiner to DEAD and the next tick re-detected
  it and re-bumped the epoch once per deadline period — the exact bump storm
  the staged record forbids).  The revert helper is removed; t/363 gains a
  bounded epoch-advance assertion across the 95s hold window.
- P1: clean-leave staged-ACK handoff re-runs the CL-I3 dead_gen-aware
  version_coherent check before apply (the COMMITTING marker wait now spans
  ticks; the guarded CAS alone misses a dead_gen-only move).
- P1: unit hard assertions — fail-stop fence stage bumps the epoch exactly
  once while the marker is PENDING and publishes only on ACK; node-remove
  re-entry while PENDING reports no false contest and no re-bump.
- P2: node-remove FENCE_ARMING skips REMOVING-marker/stripe-retire pre-work
  while the staged fence marker is in flight (new
  cluster_reconfig_node_removed_fence_stage_pending predicate).
- P2: PG-style banners (Author/IDENTIFICATION/NOTES) on the new files.
- TAP renamed t/358 -> t/363 (358-362 reserved by parallel lanes); nightly
  shard split accordingly.  Unit fixture gains MyBackendType stub (B_INVALID)
  and controllable async-marker stubs.

Local gates (cassert build): cluster_unit 158 binaries, t/363 all legs,
smoke+touched-baseline TAP 13 files/327 tests, cluster_regress 13, PG 219/219,
clang-format 0 violations, scn-cmp + no-clog-overlay clean.  cppcheck reports
one finding in test_cluster_pi_shadow.c:210 — byte-identical to the base
commit, local cppcheck 2.20 version drift, not introduced by this branch.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
…n unsupported seed, .bak read guard, multi-segment unit leg

Review P2-1: the native-era publish+seal now runs only on a TRUE shutdown
checkpoint (END_OF_RECOVERY excluded -- sealing mid-run after crash
recovery would expose a stale high-water to joiners), and an unsupported
native era (MultiXacts / past the prehistory cap) skips the seal with a
WARNING instead of FATALing the checkpointer: the refusal still lands
fail-closed on every joiner (53RB5, unsealed) but the seed can shut down
cleanly instead of wedging in a FATAL loop.

Review P3-4: roll_blob_to_bak PANICs on a read() error instead of
treating it as EOF (a truncated .bak was already rejected downstream by
CRC, but silently swallowing I/O errors broke the module's discipline).

Review P3-3: unit multi-segment prehistory round trip (33 pages across
SLRU segments 0000/0001, per-page byte pattern, short-clone extension).

Spec: spec-6.15b-xid-authority-native-era.md
… hardening

F1: begin_native_run/mark_cluster_era install the transitioned image as
BOTH primary and .bak (write_header_both) -- rolling the pre-transition
image into .bak let a later single-copy corruption fall back to a stale
SEALED (or pre-CLUSTER_ERA) authority, handing joiners the previous
pass's high-water or re-opening era re-entry.

F2: divergent-lineage guard -- before the adopt/skip decision the joiner
byte-compares its local pg_xact prefix [0, min(own_next, native_hw))
against the sealed prehistory blob at 2-bit precision; any contradiction
is FATAL (a same-sysid clone that ran standalone after cloning is not a
pre-seed lineage; neither trusting nor overwriting its bits is sound).
Bypassed only when local oldestXid has advanced past the native range.

t/361: real scram TCP login leg (hba + listener), node0 shared-heap
re-reads after joiner scans (poison proof on the heap, not just CLOG),
a 53RB5 SQLSTATE pin (%e before %q -- %q stops expansion for the
postmaster), the N5 divergent-lineage negative, and the L9
shared_catalog=off dormant leg.

Also: on-disk header layouts locked with StaticAssertDecl; consumers LOG
when the authority is served from the .bak fallback; user manual trimmed
to user-visible effect.

Spec: spec-6.15b-xid-authority-native-era.md
… WAIT_CLUSTER watchdog, event-scoped WAIT_EPOCH escape, dead-node PCM cleanup

A fail-stop reconfig could wedge a settled cluster forever on three stacked
liveness holes: a BLOCKED HW remaster worker was never relaunched within the
same episode (BGW_NEVER_RESTART + launched-latch), WAIT_CLUSTER had no
watchdog while its cluster/HW gates were held, and WAIT_EPOCH's strict
progress gate wedged permanently when an IC-piggybacked epoch bump landed
before the local reconfig event (baseline re-captured as the post-bump
value, cur == old forever).

- HW remaster: per-dead-node result/attempts/next_attempt shmem state; the
  FSM relaunches BLOCKED workers with exponential backoff (cap 60s) up to
  cluster.hw_remaster_retry_max_attempts (SIGHUP-raisable); abnormal worker
  exit marks BLOCKED via before_shmem_exit; the three silent BLOCKED
  returns now LOG their cause.  wal_threads_dir-unset is detected up front
  as BLOCKED_STRUCTURAL: never retried, WARNING-once with the config hint,
  and (level 2) a multi-node shared_catalog=on boot without it now fails
  fast at startup.
- WAIT_CLUSTER: observational watchdog on the rebuild-timeout cadence —
  WARNING with the missing-survivor list, gate states and per-dead-node
  retry state, plus a counter; it never unfreezes anything.
- WAIT_EPOCH: three proof-carrying layers — abort-to-idle on a new event id,
  durable observed-epoch adoption, and an event-scoped coordinator witness
  (REDECLARE_DONE carries the event id; stale-event DONEs are dropped at
  accounting; the equal-epoch escape additionally requires the coordinator's
  DONE to have been accounted after the accept snapshot).  No timeout-only
  advance exists; without proof the shards stay frozen.
- Dead-node PCM cleanup: the GRD dead-sweep clears the dead node's
  x_holder/s_holders/pi_holders/master_holder and pending-X residue (LOG
  summary + pcm/dead_cleanup_entries counter), and a local-master S state
  with no local residency can now upgrade S->X through the standard
  invalidate/ACK path instead of failing closed forever — post-kill DDL
  recovers instead of timing out on cluster locks.
- Serve-gate scope alignment: the merged-materialization check in
  gcs_block phase_for_tag now uses the same thread-recovery scope policy as
  the unfreeze gate, removing the permanent-RECOVERING mismatch outside the
  gate's applicable scope (in-scope behavior unchanged).
- Observability: hw retry/exhausted + grd cluster_gate_timeout /
  wait_epoch_escape + pcm dead_cleanup_entries dump keys; S4-reject and HW
  fail-closed diagnostics.

Tests: unit relaunch-decision table (8 rows) + event-scoped DONE rejection/
witness advance + PCM dead-cleanup forms incl. pending-X; t/293 gains the
same-episode self-heal and retry-exhaustion legs (unfreeze-extend positive
+ watchdog WARNING greps); t/337 gains the level-2 fail-fast negative leg;
new t/362 runs a 4-node shared_catalog formation, kill -9, and asserts
remaster convergence, post-kill DDL, cleanup counters and the 0-wedge
invariant end to end.  Local gates (cassert): unit 158 binaries, t/293 +
t/337 + t/362 + smoke subset, cluster_regress 13/13, PG regress 219/219.

Spec: spec-4.6a-grd-recovery-liveness.md
t/358-361 are reserved by parallel lanes (spec-7.2 / S-xid), t/363 by the
S-dead lane; occupancy verified against origin branches.  The 4-node
formation + kill -9 + convergence run gets its own shard for wall-clock
isolation.

Spec: spec-4.6a-grd-recovery-liveness.md
…adow unit

Same inline suppression + reason as the other cluster_unit main()
signatures (test_cluster_grd.c precedent); a newer local cppcheck
flags it, the CI baseline version does not.
…y baseline + async pre-bump WAIT_EPOCH wedge

The ship-gate nightly on the marker-async head surfaced two lane-introduced
regressions that the fast-gate matrix does not cover:

1. Dump-category baseline miss: reconfig telemetry added a pg_cluster_state
   category (55 -> 56), but three cross-node TAPs that assert the total
   (t/204/205/206) were not updated — only the locally-run L405 subset was.
   Bump them to 56 with the spec-2.29a note.

2. Async pre-bump WAIT_EPOCH wedge (behavior regression): the async fence /
   join-Phase-1 / node-remove marker staging bumps the membership epoch at
   stage-entry but only publishes the reconfig event once the voting-disk
   marker ACKs — now several LMON ticks later instead of the pre-async
   single-tick spin.  In that window the coordinator's OWN GRD recovery IDLE
   tick re-captured the already-bumped epoch as its WAIT_EPOCH baseline, so
   the P0 accept that followed the publish read old == cur and froze the
   affected shards forever — the spec-4.6a section-0 shape, here triggered by
   the coordinator on itself with no IC piggyback (reproduced deterministically
   as t/293 fail-stop remaster + t/326 node-leave shard reopen).

   Fix: while any pre-bump stage is live, the GRD IDLE tick holds its last
   stable pre-reconfig baseline instead of re-capturing
   (cluster_reconfig_has_pending_prebump_stage guard).  This restores the
   pre-async bump->publish atomicity as seen by GRD without touching the epoch
   bump timing or any user-visible reconfig contract.  Adds the
   cluster_grd_recovery_event_old_epoch accessor + a baseline-hold unit test.

Local gates (cassert): unit 158 binaries (incl. baseline-hold test), t/293 +
t/325 + t/326 family all green (were red), t/204/205/206 + observability
TAPs, PG regress 219/219, clang-format/headers/scn-cmp clean.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
…sion ②b, others-dead)

The spec-2.29a marker-async change turned the clean-leave COMMITTING marker
into an async, multi-tick wait.  In a slow environment (nightly Linux) the
leaving node finishes its drain and stops heart-beating inside that wait
window, so CSSD marks it SUSPECTED->DEAD and bumps the global dead_generation
— and the coherence gate, which compared that SCALAR dead_generation, wrongly
read it as a third-party death intruding mid-drain and escalated an otherwise
healthy clean leave (t/310 dormant-member legs, t/331 clean_leave x idle).
apply_clean_leave_as_coordinator bumps + publishes atomically, so this is NOT
the baseline-hold wedge (②a); it is the coherence predicate counting the
leaving node's OWN expected DEAD.

Fix: compare an others-dead bitmap (the dead set EXCLUDING the leaving node)
instead of the scalar dead_generation, at all four coherence sites
(CL_COHERENT macro, drive_drain commit-point, staged-ACK re-check,
non-staged pre-check).  cl_state snapshots the others-dead set at bind; the
pure predicate does a bitmap compare (fail-closed on a missing view) and is
unit-tested against the reflexive-case matrix.  Every third-party incoherence
still escalates (epoch move OR a non-leaving node entering the others-dead
set); only the leaving node's own expected transition is tolerated.  8.A /
CL-I3 protection is unchanged — argued in spec-2.29a §②b with a case matrix.

Local gates (cassert): policy unit reflexive-case matrix, cluster_unit 158
binaries, PG regress 219/219, clang-format/headers/scn-cmp clean.  NOTE:
t/310/331 are timing-sensitive — the false escalation only reproduces in a
slow environment, so the fix is verified by nightly, not locally (a local
run passes the assertions).  A separate pre-existing cassert teardown SIGABRT
flake in t/310 (isolated: reproduces without this change) is unrelated.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
…hash)

The P6 all-done gate and the WAIT_EPOCH coordinator witness keyed the
cross-node DONE on event_id, but event_id folds the sender-local
cssd_dead_generation, which drifts with each survivor's private flap
observation history and never converges across nodes: any flap asymmetry
made the survivors compute different ids for the same episode, drop each
other's DONEs, and wedge P6 forever — nondeterministically reintroducing
the permanent-freeze shape this branch exists to remove.

The quorum-accepted dead SET is what actually converges, so the DONE
payload now carries a hash over the dead bitmap alone (same kernel as
the event_id hash, riding the same request-id field pair — no envelope
change), stamped per episode at P0 accept.  Accounting, the P6 gate and
the witness compare against the stamp; event_id stays a purely local
accept-dedup scope.  A late DONE from a previous episode still cannot
back a new one: a coordinator re-election changes the dead set (hash
mismatch) and a same-node re-death rides a higher epoch through the
interposed JOIN bump (epoch conjunct).  Pre-accept frames now mismatch
the previous stamp and are dropped; senders re-announce every tick, so
accounting always lands after the accept snapshot — closing the
first-event pre-accept window as a side effect.

Spec: spec-4.6a-grd-recovery-liveness.md (Amendment v1.2 R2/R4)
Revert the scope short-circuit that returned NORMAL for a dead static
master's blocks wherever online thread recovery cannot run (the default
configuration and every >2-node deployment): it skipped both the
is_materialized cold-block door and the redo-coverage lost-write door,
so a committed write only the dead node saw could be silently read
stale from shared storage.  No other guard sits on that read path — the
GRD freeze ends with the episode, the HW gate covers only extend
high-water marks, and the page-SCN checks ride the ship path.

Out of scope the posture is now an explicit bounded retryable error
(53R9L, hint updated to name the way out: restart the failed node, or
enable online thread recovery in a supported scope) for every tag whose
static master is dead — including never-written blocks, which the
cold-block door cannot distinguish from not-yet-replayed ones — and the
door reopens the moment the failed node stops being DEAD.  In-scope
behavior is byte-identical.  The now-orphaned scope helper is removed.

t/362 asserts the honest two-outcome contract on fresh connections
(success or explicit SQLSTATE — 53R9L or the TT-unknown visibility
door — never a hang), rides pre-warmed sessions for convergence
observability, and pins the D12 cleanup counter to a positive delta;
t/293's post-kill extend leg follows the same posture.

Spec: spec-4.6a-grd-recovery-liveness.md (Amendment v1.2 R1/R3)
…grade cleanup

- Order the worker's terminal-result store after its backoff-deadline
  store (pg_memory_barrier) so the relaunch decider never pairs a fresh
  BLOCKED with a stale deadline and skips one backoff wait.
- A structural cause discovered only by the WORKER (SIGHUP race) now
  leaves the retry deadline unset so the FSM's next tick takes the
  MARK_STRUCTURAL branch and emits the once-per-episode operator
  WARNING with the configuration hint.
- Wrap the local S->X upgrade in PG_TRY and release the temporary S
  claim when the ACK wait throws (cancel via CHECK_FOR_INTERRUPTS),
  not only on the false return; document why the completed upgrade
  hard-resets the local S refcount (the X grant subsumes all local S
  declarations).  Linker-only exception stubs for the units that link
  cluster_pcm_lock.o.

Spec: spec-4.6a-grd-recovery-liveness.md (Amendment v1.2 R5/R6/R7/R9)
…e-bump shmem bit

Two review-r2 items on top of the ②b others-dead coherence fix:

P2-1 — the leaving node's barrier-tick own-commit latch (cl_leaving_barrier_tick)
inferred 'my leave committed' from epoch>baseline AND the others-dead bitmap
unchanged.  The bitmap is not monotone: a third-party node that false-fail-
stopped (bumping the epoch) then recovered leaves CSSD hysteresis DEAD->ALIVE,
so the bitmap rebounds to its bound value while the scalar dead_generation only
advances.  If the leaver's first epoch>baseline observation lands after that
rebound it would mis-latch a leave the survivor coordinator actually refused,
suppress the barrier-deadline escalation, and hang forever in BARRIER_WAIT.
Restore the scalar conjunct (epoch>baseline AND others_dead==bound AND
dead_gen==baseline) via the pure, unit-tested cluster_clean_leave_own_commit_
latched predicate.  This does not reintroduce the ②b false positive: the leaver's
OWN alive->DEAD never bumps ITS OWN dead_generation (a node does not observe
itself dead), so on the leaver side the scalar stays at baseline through its
drain.  Survivor-side coherence keeps the bitmap-only check.  Adds the rebound
negative-leg unit (others_dead unchanged + dead_gen advanced -> escalate).

t/274 — a backend-context coordinator (the fail-stop inject test) advances the
epoch then SYNCHRONOUSLY waits for the fence marker before publishing
(MyBackendType != B_LMON path).  During that seconds-long wait the epoch is
bumped but unpublished, and a concurrent LMON GRD IDLE tick re-captures the
post-bump epoch as its WAIT_EPOCH baseline -> old==cur -> the thread replay
slot never reaches DONE -> HG#1b/HG#5 done-poll timeout.  The ②a baseline-hold
guard only reads LMON-process-local staged flags, invisible to a backend's
pre-bump window.  Publish it in shmem: ReconfigShmem.prebump_sync_active set
before the bump, cleared at every return; the guard reads the shmem bit OR the
local stage.  8.A (durable-marker-before-publish) unchanged.

Reproduced the wedge locally at 87f7930 (HG#1b slot-DONE poll times out
forever), then verified the fix: t/274 goes 18/18 green (exit 2 -> 0).  This
also fixes the long-standing main flake (the wedge is deterministic once LMON
is freed from the sync-marker park; it was merely probabilistic before) — the
t/274 known-flake / L66 registration should be revised.

Local gates (cassert): cluster_unit 158 binaries (incl. own-commit-latched +
version-coherent matrices), t/274 18/18, PG regress 219/219, clang-format/
headers/scn-cmp clean.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
The leaving node's barrier-tick own-commit latch must be backed by
direct evidence (the durable COMMITTED marker confirmation for THIS
leave attempt) and must be immune to third-party transient false-DEAD
flap noise: the scalar dead_generation is monotone, so a flap during
the leave window advances it forever and the r2 P2-1 three-conjunct
inference then refuses a healthy committed leave until the barrier
deadline escalates it (nightly t/331 C1/C4 false-escalation, run
28948167577).  The reworked U3b matrix pins:
  (a) evidence, no noise            -> latch
  (d) evidence + flap noise         -> still latch  (RED on current code)
  (c) no evidence, any noise state  -> never latch (deadline escalation
      stays armed; the r2 P2-1 refused-leave mis-latch wedge stays shut)

Known-red TDD commit: leg (d) fails on the current three-conjunct
predicate; the follow-up commit flips the latch to marker evidence.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
A crash between write_header_both's two durable renames leaves the
transitioned flag (unseal / CLUSTER_ERA stamp) in the primary copy only,
with a stale pre-transition .bak behind it.  Nothing on the restart path
rewrote the pair -- the bootstrap gates skipped the transition call when
the primary already showed the new flag, and the transition functions
early-returned on the same observation -- so the stale .bak survived the
whole native run.  Any later transient primary read failure then fell
back to it: a stale SEALED .bak hands joiners the previous pass's
high-water (false-invisible + xid reissue), and a stale pre-CLUSTER_ERA
.bak re-opens the native-era re-entry guard.

Make the two flag transitions idempotent re-asserts: the bootstrap calls
them unconditionally on every boot of their respective arm, and they
rewrite BOTH copies until both validate with the post-transition flags
(a missing or invalid copy counts as unsettled and is restored too).
Once both copies are settled the call is a no-write no-op, so the
steady-state boot cost is two header reads.  All fail-closed semantics
(53RB5 refusals, CLUSTER_ERA re-entry FATAL, corrupt-authority PANIC)
are unchanged.

Red-first unit coverage: torn-window tests for both transitions
(complete the transition, re-install the old image as .bak, re-run the
boot path, assert the .bak is repaired and that a primary read failure
falls back to the repaired image, not the stale one).

Spec: spec-6.15b-xid-authority-native-era.md
…osed)

The divergent-lineage prefix check broke out of its comparison loop and
returned CONSISTENT on the FIRST missing local pg_xact page.  That is
only sound for a limit that ends where the local tree ends; pg_xact
front truncation (SimpleLruTruncate removes whole low segments once
every xid they cover is frozen past oldestXid) means a joiner can have
segment 0000 legitimately absent while later segments are present AND
divergent -- the first-page ENOENT then declared the clone CONSISTENT
without ever comparing the surviving pages, and the adopt arm overwrote
the joiner's own live-xid outcome pages with the seed's bits
(false-visible / false-invisible).

Pass the caller's recovery-anchor oldestXid into the check and start
the comparison at oldestXid's own 2-bit slot: bits below it are frozen
truth that CLOG never consults again and whose on-disk survival is an
accident of truncation timing, so they are exempt whether the pages
survive or not (neither truncation nor tuple freezing rewrites
surviving CLOG bytes, so this can never hide live evidence).  Within
the comparable range [oldestXid, min(own_next, native_hw)) a missing
local page is an anomaly -- pg_xact always covers [oldestXid, nextXid)
on a well-formed node -- and now returns UNAVAILABLE (fail-closed
53RB5 at the caller), never CONSISTENT.  Sub-byte boundaries at
oldestXid are masked at 2-bit precision.

Red-first unit coverage: front-truncated divergent clone must DIVERGE
(returned CONSISTENT pre-fix), divergence strictly below oldestXid
stays CONSISTENT (frozen exemption, mid-byte lead mask), oldestXid at
the divergent slot DIVERGES, and a hole inside the comparable range is
UNAVAILABLE (the old truth-table leg expecting CONSISTENT for a missing
segment encoded exactly the blind spot and now expects UNAVAILABLE).

Spec: spec-6.15b-xid-authority-native-era.md
…not inference

Nightly t/331 C1 (clean_leave x idle) / C4 (leave_remove x idle) regressed
at the r2 P2-1 head (run 28948167577; parent green): the leaver's
barrier-tick own-commit latch inferred its commit from
epoch>baseline + others-dead bitmap unchanged + scalar dead_generation
unchanged.  The scalar is monotone, so a third-party transient
SUSPECTED->DEAD->ALIVE flap on the leaver's local CSSD view during the
leave window advances it forever; the latch then refuses a leave the
coordinator ACTUALLY committed and the immediate-escalate arm (or the
barrier deadline) aborts it: phase ends ABORTED_ESCALATE -> IDLE, never
'committed', breaking the C1/C4 drains+commits assertions.  Reverting to
bitmap-only would re-open the r2 P2-1 wedge (rebound mis-latch of a
REFUSED leave suppresses escalation forever), so neither inference is
usable.

Replace inference with direct evidence: latch <=> the durable COMMITTED
marker for THIS leave attempt is confirmed.  The evidence already reaches
the leaver with zero extra IO: the coordinator sends the nonce-bound
LEAVE_COMMITTED only after its qvotec ACKs the COMMITTED marker
majority-durable (there is no runtime voting-disk read path on the
leaver, and none is needed - the LMON tick budget this spec protects is
untouched).  cl_committed_handler now routes through a pure identity
gate (self-addressed + currently leaving + per-attempt nonce + committed
epoch past the bound baseline; fail-closed on any mismatch, so a stale
confirmation - and through it a stale COMMITTED marker from a previous
leave of the same node - can never false-latch) and records the attested
committed epoch E before publishing the evidence flag; the barrier tick
consumes E instead of re-reading its possibly-stale local epoch view.

The latch and the P1-V0.7 exit gate collapse into one step (the evidence
IS the durable-truth-source confirmation).  Escalation semantics are
unchanged: no evidence by the barrier deadline -> the existing
cl_escalate path, which is exactly what bounds a refused leave (it never
gets a COMMITTED marker).  Third-party flaps no longer matter: the
coherence observations are now contract inputs the predicate must ignore
(pinned by the U3b unit matrix, red-first in the parent commit) and feed
only a flap-noise LOG at the latch.  Survivor-side coherence sites
(drive_drain / staged-ACK / pre-check) are untouched.

Unit: U3b matrix flipped green; new U3c pins the evidence identity gate
(match / stale-nonce / misrouted / not-leaving / epoch-not-advanced).
Local gates: t/331 x4 all green (6/6), t/310 24/24, t/363 18/18,
t/274 18/18 (prebump shmem-bit stays green), cluster_unit 158 binaries,
cluster_regress 13/13, PG core 219/219, clang-format-18 +
comment-headers clean.

Spec: spec-2.29a-reconfig-marker-async-lmon-liveness.md
WAIT_BARRIER / WAIT_CLUSTER carried only epoch-coherence guards, so a
survivor whose CSSD detected a coordinator-folded multi-death late kept
announcing REDECLARE_DONE under its stale dead-bitmap hash: peers that
stamped the full set dropped those frames on the composite key and P6 —
which has no timeout by design — wedged permanently (survivor-behind
detection skew).  Add a mid-episode guard that aborts to IDLE on a newer
local event whose dead-bitmap hash differs: the next tick re-consumes
the event, re-stamps recovery_event_bitmap_hash, re-runs recovery
against the fuller dead set and re-announces DONE under the new
composite key.  Same-set dead_generation drift is absorbed without
episode churn (the event_id keeps its local ABA-scoping role).  The
coordinator-behind direction needs no handling: the coordinator's own
later detection bumps the epoch again and the epoch guards abort.

Correct the stamp-site / accounting / hash-helper comments that
overstated the bitmap as quorum-ratified (each node stamps its OWN
accepted event's bitmap; convergence is eventual via the CSSD deadband
plus this guard), and assert a stamped episode hash is never 0 — 0 is
reserved as unstamped in the mark_peer_done drop test, and a JOIN
episode's all-zero bitmap hashes to a fixed nonzero constant.

Unit: survivor-behind growth leg (H({1}) stamped at the folded epoch,
grown {1,2} event arrives -> abort, re-stamp, full-set remaster, DONE
accounting converges) plus a same-set drift no-churn leg; both go red
with the guard disabled.

Spec: spec-4.6a-grd-recovery-liveness.md (r3-P2-2, r3-P3a)
…arrier

hw_remaster_record_terminal stores the backoff deadline before the
terminal result with a release fence between the stores, but the LMON
relaunch decider loaded result-then-deadline with no read-side pairing:
on a weakly-ordered CPU it could still pair a fresh BLOCKED with a stale
zero deadline and skip one backoff wait.  Insert pg_read_barrier()
between the result and deadline loads (attempts needs no fence — it is
written only by the single-writer LMON FSM) and cross-reference the
pairing at the write site.

Spec: spec-4.6a-grd-recovery-liveness.md (r3-P3b)
The serve-gate unseal (static master no longer CSSD-DEAD -> NORMAL) is
heartbeat liveness, not a direct recovery-completion signal.  Document
the traced proof of why returning NORMAL is nevertheless safe: CSSD —
the only heartbeat sender — is spawned by the phase-4 driver at the
PM_RUN transition, after the startup process completed the node's crash
recovery against shared storage; and even under a stale-ALIVE view a
fetch cannot complete against a still-recovering node, because the
master-side handler default-denies until the node is an in-quorum
MEMBER and only the (equally post-PM_RUN) QVOTEC process can establish
the quorum lease after a restart wiped shmem.  Deny replies surface as
bounded 53R9L, endpoint unavailability as bounded 53R90, and no path
falls back to a silent local storage read.  Replaces the previous
wording that asserted recovery-completion semantics without grounding.

Spec: spec-4.6a-grd-recovery-liveness.md (r3-P2-1 prove-safe)
… leg

The Amendment v1.2 (R1) serve-gate fix had no test lock: the t/362
two-outcome legs stay green under a reverted gate (their success arm
accepts a NORMAL serve of a dead-master page) and no unit links the real
phase decision.  Add L4h: after the kill, with the dead node never
restarted, once the observed survivor's own recovery episode reached
IDLE, a FRESH backend on a cold survivor runs a wide
pg_class/pg_attribute scan that MUST fail with exactly 53R9L — no
success arm, bounded return.  An out-of-scope formation never publishes
the dead node's materialization proof, so every dead-master page stays
RECOVERING while the node is down; the scan spans dozens of catalog
pages hashed ~1/4 to the dead node and cold-reads them past the 16MB
test pool, and it touches no dead-node-written rows (heap INSERTs only,
no DDL), so 53R9L is the only legal outcome.  Verified red-first: with
the gate reverted the suite fails at exactly this leg while every other
leg stays green.

The episode-IDLE wait polls the pre-warmed session with the exact query
shape it ran pre-kill and compares in Perl: a cast or operator added to
the SQL performs fresh syscache lookups that themselves cold-read a
dead-master catalog page and trip 53R9L (the very mechanism under
test).  Also accept the 53R51 write-fence rejection in the pre-existing
two-outcome write legs: a transient lease/epoch fence right after the
reconfig is an explicit, retryable fail-closed rejection inside the
same honest contract.

Spec: spec-4.6a-grd-recovery-liveness.md (r3-P1-1 TAP hard leg)
@sqlrush sqlrush force-pushed the stage7-p0-integration branch from 2acd537 to bfe00bc Compare July 9, 2026 02:03
@sqlrush sqlrush merged commit ea777c7 into main Jul 9, 2026
5 checks passed
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