Skip to content

feat(cluster): cross-node write-write MVCC coordination (terminal-writer chaining)#27

Merged
sqlrush merged 13 commits into
mainfrom
spec-7.1a-write-write
Jul 9, 2026
Merged

feat(cluster): cross-node write-write MVCC coordination (terminal-writer chaining)#27
sqlrush merged 13 commits into
mainfrom
spec-7.1a-write-write

Conversation

@sqlrush

@sqlrush sqlrush commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the cross-instance write-write coordination band (D0-D6):

  • D0 terminal remote writer chaining behind new GUC cluster.crossnode_write_write (default off, PGC_SUSET): a local DML that runs into a TERMINAL remote writer maps the authoritatively-resolved outcome onto the native TM_Result contract (remote UPDATE committed -> TM_Updated with single-hop new-version probe; DELETE -> TM_Deleted; aborted -> TM_Ok) instead of unconditionally failing closed (53R9H). Unprovable outcomes keep the fail-closed floor; GUC off keeps the pre-existing behavior byte-identical.
  • heap_lock_tuple gains the same treatment for remote WRITER holders (previously fell through to a native XactLockTableWait on a foreign raw xid) honoring LockWaitSkip/LockWaitError.
  • Authority-backed clear of a released/aborted remote xmax (fixes the foreign-class hint suppression silently swallowing the cross-node lock cleanup).
  • Covers gate on SCN total order: replaces the raw cross-thread LSN compare (measured false-refuse; latent false-pass) with an SCN conclusiveness demand co-sampled at the origin and carried in the previously-zero authority trailer bytes (wire size unchanged).
  • Live overlay miss now pulls the origin's own-TT verdict on demand instead of staying UNKNOWN forever.
  • Foreign-holder floors for every remaining native wait/chase sibling (lock_tuple multixact wait, update-chain walker entry/members, KeyShare pre-sleep chase) plus the missing VM all-visible re-check in the heap_delete cluster branch.
  • Observability: 5 new counters in the visibility dump category; t/223 baseline updated in-commit.

Test plan

  • cluster_unit 158/158 (new: writer-chain truth table U0 9/9; covers gate truth table rewritten 16/16) — cassert build
  • cluster_regress 13/13
  • PG regress 219/219 (enable-cluster build)
  • smoke TAP subset (010/030/050/200/223/226) green; t/280 + t/281 (cross-node writer/locker waits) green
  • hygiene gates: check-format / comment-headers / scn-cmp-gate / no-clog-overlay / tidy all rc=0 (cppcheck: 1 pre-existing style hit in an untouched test file)
  • fast-gate all jobs green (PR-level)
  • 2-node write-write TAP legs (parallel test session; layer-tagged, shared-undo legs KNOWN-BLOCKED)

Note: t/209 fails locally on this machine identically on unmodified main (CREATE TABLE GES acquire timeout under its cluster.pcm_grd_max_entries=0 conf) — pre-existing, not introduced here.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md

SqlRush added 12 commits July 9, 2026 22:11
A local DML that runs into a TERMINAL remote writer holder previously
failed closed unconditionally (SQLSTATE 53R9H).  Behind the new GUC
cluster.crossnode_write_write (default off, PGC_SUSET) the writer-wait
bridge now resolves the holder's outcome from cluster authority (page
ITL ref + TT overlay + origin live-IC verdict; never the local
CLOG/ProcArray view of a foreign xid) and maps it onto the native
TM_Result contract:

- remote UPDATE committed -> TM_Updated after the single-hop new
  version is probed reachable + chain-valid; the callers route the
  result into their native failure exits, which fill tmfd from the
  on-page tuple (ctid/xmax real values, cmax = InvalidCommandId per
  the native other-transaction contract)
- remote DELETE committed -> TM_Deleted
- remote writer aborted   -> TM_Ok (released xmax cleared, caller
  re-validates and continues)
- anything unprovable     -> the pre-existing 53R9H floor (fail closed)

heap_lock_tuple gains the same treatment for remote WRITER holders,
which previously fell through to the native XactLockTableWait on a
foreign raw xid (alias/hang); LockWaitSkip/LockWaitError are honored
before the bridge blocks.  A committed foreign deleter is never
stamped HEAP_XMAX_INVALID (shared-page hint poisoning).

With the GUC off the fail-closed floor is unchanged.

New pure policy file cluster_writer_chain.c (outcome -> TM_Result
mapping) with full truth-table unit coverage; cluster_unit 158/158,
cluster_regress 13/13 locally green.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
spec-6.15 D7 taught SetHintBits to suppress xmax hint stamping when the
xid is another node's class (native CLOG/ProcArray answers are void for
foreign xids).  That suppression silently also swallowed the spec-5.2
cross-node cleanup that clears a RELEASED remote lock-only xmax after
the cluster authority proved the holder terminal -- reopening the
window where compute_new_xmax_infomask folds a dead remote xid into a
node-local MultiXact that aliases on every peer.

Add cluster_heap_stamp_released_xmax_invalid(): a direct, authority-
backed HEAP_XMAX_INVALID stamp (no commit-LSN interlock is needed for
released/aborted marks) used by the writer-wait bridge callers and the
heap_lock_tuple terminal handlers -- the only sanctioned bypass of the
foreign-class suppression, because the fact comes from cluster TT /
live-IC verdict authority, never from a native reading of the foreign
xid.  The spec-7.1a D0 aborted-writer paths depend on the stamp landing
so the native re-determination sees HEAP_XMAX_INVALID instead of
consulting HeapTupleHeaderIsOnlyLocked on a foreign xid.

Ordinary SetHintBits suppression for foreign-class xids is unchanged
(committed foreign outcomes are never stamped; readers re-resolve via
cluster authority).

cluster_unit 158/158, cluster_regress 13/13 locally green.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…w LSN

The live-authority covers gate admitted a recycled/remote xid resolution
only when the origin's flush LSN was numerically at/after the tuple's
page LSN.  Under per-thread WAL that raw compare is unsound: a page last
written by another WAL thread carries an LSN from a different stream, so
live resolutions were refused persistently (measured) and the reverse
direction could false-admit (latent).

Replace the page-LSN anchor with an SCN total-order conclusiveness
demand (AD-008 Lamport): the origin co-samples its SCN clock with the
served content (before the content read, same ordering argument as the
existing high-water sample) and ships it in the previously must-be-zero
authority-trailer bytes (wire size unchanged; a zero trailer from an
older peer reads as absent and refuses).  The requester demands
conclusiveness for its snapshot read_scn -- or, on no-snapshot legs
(terminal-writer resolution), for its own clock sampled BEFORE the
fetch -- and admits only when the shipped clock is at/after the demand.
Every shipped SCN is Lamport-observed before the gate can refuse, so a
refusal self-heals on retry.  Epoch equality and the downstream
tt_generation wrap qualification are unchanged; all refusals keep the
pre-existing fail-closed boundary.

cluster_unit 158/158 (covers truth table rewritten: false-refuse
regression + false-pass guard + absent-sample/absent-demand refusals),
cluster_regress 13/13 locally green.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…dict

A LIVE remote ITL ref (slot still bound to the asked xid) whose TT
overlay lookup missed stayed UNKNOWN forever when the origin's
tt_status_hint propagation was lost: fail-closed (53R97) but never
self-healing, so the affected scan could not complete.

Behind cluster.crossnode_write_write, escalate such a miss to the
origin's complete own-TT verdict on demand -- the same shipped live-IC
verdict machinery the recycled-slot resolution uses; no shared-undo
data plane is involved and the origin answers only for its own xids
with terminal outcomes.  Exact terminal outcomes install the exact-key
memo; a below-horizon bound is returned snapshot-relative and never
memoized.  A genuinely in-progress holder still resolves UNKNOWN (no
verdict is served) and keeps the fail-closed retry, which converges
once the holder terminates.  GUC off keeps the UNKNOWN floor
byte-identical.

cluster_unit 158/158 locally green.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…wait/chase sibling

Sweep of every native XactLockTableWait / MultiXactIdWait / update-chain
chase in heapam.c for paths a node-foreign xid or foreign-marker
multixact could still reach natively (L349/L352 sibling discipline):

- heap_lock_tuple multixact wait: a remote-marker multi reached the
  native MultiXactIdWait (node-local multixact ids alias across
  instances); floor it 53R9H before the wait, mirroring the writer-wait
  bridge's multi floor in heap_update/heap_delete.
- heap_lock_tuple KeyShare pre-sleep chain chase: a remote-marker multi
  updater seeded heap_lock_updated_tuple, whose entry decode
  (MultiXactIdGetUpdateXid) reads the local pg_multixact; floor at the
  call site (the marker needs the page).
- heap_lock_updated_tuple entry: floor a non-multi node-foreign chain
  root (defense in depth behind the call-site floors).
- heap_lock_updated_tuple_rec: floor a chain member whose xmin or xmax
  is node-foreign (or a remote-marker multi) before the native
  DidAbort/DidCommit/member-decode judging -- cross-node update-chain
  locking is the spec-7.1a Q9 multi-hop forward; fail closed retryably
  instead of mis-walking.
- heap_delete cluster post-wait re-validation: add the native VM
  all-visible re-check the cluster branch was missing (the lock is
  dropped across the wait; an unpinned VM page that became all-visible
  must restart from l1 exactly as the native locker path does).

All floors are strictly tightening (fail-closed on paths that were
natively unsound); no admit path is widened.

cluster_unit 158/158, cluster_regress 13/13 locally green.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
Five new counters in the 'visibility' dump category (same atomic
bump/read shape as the spec-3.14 D8 family):

- writer_chain_resolved_count / writer_chain_failclosed_count: the
  terminal-writer bridge chaining outcome split (53R9H keeps bumping
  the pre-existing vis_conflict_failclosed_count as well)
- xmax_resolved_count: positive foreign-xmax verdict consumptions in
  the MVCC keeps-visible gate and the SatisfiesUpdate fork
- overlay_refresh_count: live overlay misses resolved by the D4 origin
  pull
- covers_scn_refuse_count: covers-gate refusals under the D3 SCN
  regime (both the block leg and the verdict leg)

No new wait event: the chaining paths introduce no new blocking
primitive (the next-version probe rides the ordinary cluster-coherent
buffer read and the writer wait keeps the existing cross-node TX
enqueue wait event).

t/223 visibility-category baseline 6 -> 11 keys updated in-commit.
cluster_unit 158/158; smoke TAP subset (010/030/050/200/226) + t/223
all green locally.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…riers

The trailer byte-carrier helpers take the value as 'v' (the
expected_pi_watermark_scn_bytes precedent) and the covers-gate header
prose avoids a raw comparator on an _scn name -- both hits were
lint-shape only; no behavior change.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…ence, not a verdict

The CP3 single-block fetch fast leg trusted a shipped TT slot's
COMMITTED stamp as a terminal commit.  Durable COMMITTED stamps land at
pre-commit (2PC COMMIT PREPARED stamps before the commit record), so a
stamped-then-crashed xid is still in-doubt and concluding from the
stamp alone is a false-committed hazard for the read fast leg, the
writer chain and the overlay pull alike.  Route every COMMITTED proof
through the origin verdict leg, whose C1b TransactionIdDidCommit
cross-check refuses a stamped-then-crashed xid; only ABORTED proofs
keep the fast path (an abort stamp is terminal and irreversible).

Routing every stamp to the verdict leg turns the serve's wrap-suspect
refusal (a live slot whose stamped scn sits below the retention
horizon) from an unreachable corner into a routine outcome, which
would leave such xids permanently unresolvable.  The serve now answers
that shape with the existing COMMITTED_BELOW_HORIZON bound over
max(stamped scn, max gated-recycle horizon) -- the same frozen-bound
consumer contract as the zero-match arm, with the C1b check unchanged
-- and still refuses when no gated recycle bounds a possible
recurrence.

t/346 evolves with the substrate it now exercises: a live remote ref
whose overlay lookup misses is resolved by the origin pull, which is
armed by cluster.crossnode_write_write, so the pair boots with it on
(as it already does for xid striping), and the L5 warm read retries
through the leg-(e) first-read transient.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…er-chain probe

A committed remote cross-partition UPDATE leaves the moved-partitions
sentinel in the old tuple's t_ctid.  The D0 new-version probe passed it
to ReadBuffer unchecked: the sentinel block is InvalidBlockNumber ==
P_NEW, so every such wake silently extended the old partition by one
empty page before the probe failed.  Split the chain decision on the
sentinel (keeping the CWO_UNRESOLVABLE fail-closed floor) and refuse
any forward pointer whose block is invalid or beyond the relation's
end at the probe entry.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
…eign predicate

The l4 chain-walk and chain-root fail-closed floors used
cluster_xid_foreign_class_cheap, whose contract is suppression-only:
it over-reports below-floor pre-striping local xids as foreign,
turning a purely local update chain into a spurious cross-node write
conflict.  Switch the three floors to cluster_xid_provably_foreign
(false when underivable, preserving native authority over pre-striping
history), matching the visibility-side floors.  The SetHintBits
suppression sites keep the cheap predicate by contract.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
t/365 covers today's write-write boundary (in-flight remote-writer
conflicts fail closed, retries converge with no lost update, arming
the GUC invents no resolution for an undecided writer), the C1b
verdict-routing leg (a fetched COMMITTED stamp is finalized via the
origin verdict leg -- rtvis_verdict_wire_count moves), the
moved-partitions no-bloat invariant, and the 2PC in-doubt hard
boundary (a PREPARED xact's value is never served nor chained; COMMIT
PREPARED heals the read and the owner closes the value chain over the
deferred-transfer boundary).  The writer-wait chain gain legs, the
crash-in-window injection and the below-floor chain-walk leg are
KNOWN-BLOCKED in the file header with their blockers.

Spec: spec-7.1a-cross-instance-write-write-mvcc-coordination.md
@sqlrush sqlrush force-pushed the spec-7.1a-write-write branch from dbbcb63 to 4d353df Compare July 9, 2026 14:13
The merge-window rebase onto main unioned the spec-7.2 cluster_ic_duty_lazy
and spec-7.1a cluster_crossnode_write_write externs; the hand-resolved line
used a tab before the trailing comment. clang-format-18 collapses it to a
single space. Format-only; no semantic change.
@sqlrush sqlrush merged commit d2ddf5a 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