Skip to content

fix(cluster): shared-catalog native-era XID authority + prehistory adoption#24

Open
sqlrush wants to merge 7 commits into
mainfrom
stage7-p0-xid-authority
Open

fix(cluster): shared-catalog native-era XID authority + prehistory adoption#24
sqlrush wants to merge 7 commits into
mainfrom
stage7-p0-xid-authority

Conversation

@sqlrush

@sqlrush sqlrush commented Jul 8, 2026

Copy link
Copy Markdown
Owner

What

Under cluster.shared_catalog, xids consumed by the seed node's pre-formation (cluster.enabled=off) load were invisible to every other node: a joiner's transaction horizon stayed at its pre-seed clone point, so seed rows were silently judged "in the future" (catalog rows, roles and tables missing on joiners; txid_status() erroring; role login failing), and any unhinted seed tuple risked a wrong local-CLOG verdict being hint-stamped onto shared pages.

This adds a durable shared XID authority + native-era pg_xact prehistory under cluster.shared_data_dir:

  • global/pgrac_xid_authority: never-lowered native-era xid high-water with SEALED / CLUSTER_ERA lifecycle flags (torn-safe write, .bak fallback, fail-closed read).
  • global/pgrac_xid_prehistory: CRC-guarded raw pg_xact page run [0, native_hw), published at the seed's clean shutdown checkpoint, adopted into a pre-seed-lineage joiner's local pg_xact (proven via its per-node recovery anchor) before StartupCLOG(), fsynced.
  • StartupXLOG max-merges the joiner's nextXid with the sealed high-water (horizon reconciliation, decoupled from redo start).
  • Multi-node shared_catalog now requires cluster.xid_striping=on (startup vet, new SQLSTATE 53RB5 family).
  • A follow-up native-era run unseals the authority at boot so a crashed pass can never expose a stale high-water to joiners.
  • Merged recovery no longer advances the local xid allocator past foreign-thread record xids.

Tests

  • cluster_unit: test_cluster_xid_authority (layout lock, classify truth table, seal/flag monotonicity, unseal, prehistory publish/adopt byte round trip).
  • TAP t/361 (new, in nightly stage6-crossnode-b): green path (pre-seed clone adopts prehistory, seed rows/roles visible, txid_status consistent, login works) + negatives (striping-off vet, era re-entry, corrupt authority, unsealed crash-seed, crashed multi-pass).
  • t/337/t/339/t/346 flipped to online_join+xid_striping posture; first-contact probes retry the documented "retry is safe" join-gate window.
  • Local: cluster_unit 158 binaries, cluster_regress 13/13, PG regress 219/219 (cassert build); lint/cppcheck/tidy clean.

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

SqlRush added 3 commits July 8, 2026 16: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
@sqlrush sqlrush force-pushed the stage7-p0-xid-authority branch from 6db1da2 to 964e587 Compare July 8, 2026 08:53
SqlRush added 4 commits July 8, 2026 17:08
…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
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
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