Skip to content

fix(cluster): crash-rejoin re-declare barrier (Shape A) + cold-GRD fail-closed#58

Merged
sqlrush merged 3 commits into
mainfrom
fix/s3-crash-rejoin-redeclare-barrier
Jul 15, 2026
Merged

fix(cluster): crash-rejoin re-declare barrier (Shape A) + cold-GRD fail-closed#58
sqlrush merged 3 commits into
mainfrom
fix/s3-crash-rejoin-redeclare-barrier

Conversation

@sqlrush

@sqlrush sqlrush commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Closes the crash-rejoin fail-OPEN hole for cluster.online_join=off: a node that crash-restarts into a running cluster no longer self-admits to a writable MEMBER with an empty GRD (silent stale reads / silently-diverging writes on self-home blocks — committed data could vanish from disk with zero errors).
  • Detector = prior-incarnation voting-disk self-slot ALIVE flag (clean shutdown blanks it; a crash leaves it), latched once at qvotec startup before READY. Fires on BOTH slow rejoin (survivor already re-epoched) and fast rejoin inside the survivor dead-deadband where the epoch signal is blind.
  • Fence order is 8.A-correct: arm the self join-fence FIRST, then demote self_join_admitted; the off-path boot barrier (offpath_boot_decided=0) keeps self-home blocks RECOVERING for the whole incarnation; writes fail closed 53R60, reads 53R9L. Full-outage crash co-boot now fences ALL nodes (explicit refusal) instead of silently forming — admission self-heal is the spec-5.22 follow-up.
  • fix 2 defense-in-depth: pure predicate cluster_gcs_cold_grd_watermark_verdict {PROVE, SKIP, FAIL_CLOSED} wired into fallback_verify_refresh with an extension-block whitelist (blockNum >= smgrnblocks), truth-table unit tested.
  • Layout coordination: QvotecShmem offset 64..71 reserved for the convert-queue lane's self_incarnation (byte-identical placeholder, StaticAssert-pinned); prior_unclean_death at offset 72.

Test plan

  • t/404 crash-rejoin harness (TDD RED->GREEN): read/write fail-closed legs, 40-iteration boot-window leg (silent0=0), node0 data survival, full-outage double-fence, clean restart
  • Gatekeeper independent gates: unit clean 178/178, t/404 14/14 + t/401 21/21 + t/402 14/14 (rerun green after load-flake attribution), check-format 0, check-comment-headers 0
  • Author gates: five static gates 0, regress 13/13, PG 219/219, t/017 PASS
  • fast-gate CI green (this PR)

Spec: spec-5.16 (extended), spec-5.22 follow-up registered

SqlRush added 3 commits July 15, 2026 18:16
Transplants the bounce-read/write P0 probes into a committed 2-node TAP
encoding the fix contract (Rule 8.A): a crash-rejoined node's home-block
read must be coherent or fail-closed (never a silent 0), its write must
be coherent or fail-closed (never a silent divergent/lost commit).

RED on main 6ef83c2: L4 (silent 0 read) + L5 (silent split write,
node1=1 node0=64) fail; L3 diagnostic measures the membership split
(both nodes see the rejoiner as member with online_join=off).

No backend change — RED harness only.  Fix (crash-rejoin re-declare
barrier + cold-GRD watermark fail-closed) is gated on a boot-semantics
decision reported to the gatekeeper.

Spec: spec-5.16 (extended to online_join=off crash-rejoin).
…e A checkpoint)

Closes the fail-OPEN crash-rejoin hole for the SLOW-rejoin case and the
boot-to-decision race; the FAST-rejoin-within-deadband residual needs a
formation-level signal (escalated, NOT claimed done).

Mechanism (fail-closed, off path only; online_join=on untouched):
 - New per-incarnation shmem flag ClusterGrdShared.offpath_boot_decided
   (init 0).  The phase gate (cluster_gcs_block.c) fences a self-home
   block RECOVERING while the flag is 0, so from process start every
   backend fail-closes self-home reads AND writes (both reach the gate
   via cluster_pcm_lock_acquire_buffer) — zero cold-serve window.
 - cluster_reconfig_offpath_rejoin_tick(): the online_join=off counterpart
   of the joiner self-tick.  already_running -> arm self-fence({self})
   then demote self_join_admitted to 0 (8.A: never raised to 1 before the
   fence is armed); bootstrap-at-initial or single node -> lift the barrier.

Measured (scratchpad bounce-read-p0/rejoin-state-measured.log):
 - SLOW rejoin (survivor advanced epoch first): tick detects
   already_running, arms the fence, self demotes to JOINING. WORKS.
 - FAST rejoin (node restarts within the survivor dead-deadband, survivor
   still at epoch INITIAL): already_running is false and
   bootstrap_quorum_at_initial cannot distinguish a co-boot from a
   long-running-but-not-yet-reconfigured peer -> mis-lifts -> t/404 L4/L5
   still RED.  This gap is pre-existing and affects online_join=on too
   (existing tests only do slow rejoins).  The durable prior-self-epoch
   signal is erased by qvotec_poll_once (writes self slot before reading,
   cluster_qvotec.c:578) — robust closure is a formation-level change.

Spec: spec-5.16 (extended to online_join=off crash-rejoin).
…ail-closed

Closes the P0 that a crash-rejoined node with cluster.online_join=off
cold-serves its home blocks from a wiped GRD: a silent stale READ and a
silently-diverging WRITE that, after flush ordering, durably LOSES a
peer's committed rows (diagnosed 2026-07-15, both reproduced minimally on
main including the committed-write loss).

Fix 1 — the off-path counterpart of the online joiner tick.  A per-
incarnation shmem flag ClusterGrdShared.offpath_boot_decided (init 0)
makes the phase gate fence self-home blocks RECOVERING from process start
(before any LMON tick), so reads AND writes fail-closed with zero cold-
serve window.  cluster_reconfig_offpath_rejoin_tick() then classifies the
incarnation:
 - crash-rejoin  -> arm the join fence + demote self_join_admitted to 0
   (writes 53R60) and LEAVE offpath_boot_decided 0, so the boot barrier
   persists as the read fence for the incarnation.  The epoch-keyed join
   fence is a no-op on a fast rejoin (this node is still at
   CLUSTER_EPOCH_INITIAL, so the monotonic-max fence epoch cannot rise);
   the epoch-independent boot barrier carries the read fence.
 - clean cold-bootstrap / single node -> set the flag, serve normally.

The crash-rejoin signal is the prior-incarnation voting-disk self-slot's
ALIVE bit (守门裁决 07-15: ALIVE, not epoch — a fast rejoin leaves BOTH
sides at INITIAL, so an epoch test is blind; a clean shutdown clears
ALIVE via qvotec_clear_self_alive_on_clean_shutdown while a crash leaves
it set).  qvotec's startup ghost-detect latches it into
QvotecShmem.prior_unclean_death (offset 72; the convert-queue lane owns
64..71 — StaticAssert-pinned, this lane rebases after queue merges).

Fix 2 (defense in depth) — cluster_gcs_cold_grd_watermark_verdict(): an
InvalidScn master watermark under an active self-fence FAILs CLOSED
instead of SKIPping (the wiped watermark could mask a stale block),
except for a genuine extension block (never cross-node written).  Pure,
header-only, unit-tested truth table; wired into fallback_verify_refresh.

Approved semantic consequence: a full-outage crash co-boot fences every
node (53R60) — no silent auto-formation; the cluster waits for online
admission (spec-5.22 cold-formation follow-up) or a clean restart.  A
CLEAN full shutdown clears ALIVE and co-boots normally.

Tests: t/404 (crash-rejoin read/write/boot-race/full-outage/clean-restart
legs, all GREEN), fold unit truth table, t/249 grd_recovery roster
31->32 (offpath_crash_rejoin_fenced dump key).

Gates: unit clean 178/178, t/017+401+402+404 PASS, cluster_regress 13/13,
PG 219/219, format/headers/scn-cmp/ges-mode/clog all 0.  (t/249's
cooperative-rebind timeout legs L6/L3/L13 flake under mac load — confirmed
pre-existing on a binary without these changes; the roster ripple passes.)

Known-scoped-out (守门裁决): a CLEAN fast-restart of a cluster member
(without clean-leave) shares the hazard but is out of this fix's scope
(the ALIVE bit is cleared on clean shutdown); clean departures should run
clean-leave flush/handoff.

Spec: spec-5.16 (extended to online_join=off crash-rejoin); Rule 8.A.
@sqlrush
sqlrush force-pushed the fix/s3-crash-rejoin-redeclare-barrier branch from de3a4e1 to 32edefe Compare July 15, 2026 13:15
@sqlrush
sqlrush merged commit 7da2233 into main Jul 15, 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