Skip to content

fix(cluster): PCM-X convert-queue closure — dirty-N flush, churn recovery, self-ACK, rejoin contract#62

Merged
sqlrush merged 9 commits into
mainfrom
fix/nightly-clusterquad-xid-striping
Jul 18, 2026
Merged

fix(cluster): PCM-X convert-queue closure — dirty-N flush, churn recovery, self-ACK, rejoin contract#62
sqlrush merged 9 commits into
mainfrom
fix/nightly-clusterquad-xid-striping

Conversation

@sqlrush

@sqlrush sqlrush commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Closes out the convert-queue lane for the S3 acceptance queue.

What

  • Pre-grant dirty N pages: relation extension / redo dirt at the N-source image prep is legal, not corruption; flush-then-BUSY (WAL-first) instead of fusing the runtime (root cause of the t/247 / t/362 write-load wedge).
  • Transient churn recovery: four sites stopped treating benign concurrent progress (promotion / round advance / two-phase cancel windows) as corruption — follower snapshot STALE re-dispatches via REFRESH_ROLE, cleanup cancel retries on a refreshed identity-exact handle, the ensure claimed arm rechecks under the ticket domain lock, and dispatch BAD_STATE is damped per ticket before the fail-closed verdict. CORRUPT-class results still fuse immediately.
  • Master self INVALIDATE_ACK dispatches through the tag shard (real loopback, same-tag ordering) instead of the IC dest=self no-op.
  • Blocker-stage source namespace + terminal-leg responder classification exactness in the master queue engine.
  • Harness: pair/quad tests gate DML on PCM-X formation ACTIVE; quad shared-catalog implies online_join + xid_striping.
  • t/370 rejoin leg: striping substrate armed; the L5 historical read pins the interim bounded fail-closed contract while the survivor write-fence authority republish gap stays deferred (Spec: spec-5.22-online-join-cold-formation.md, Amend v1.1).
  • t/400: fixed an illegal do-while "last" that killed the file on the healthy path; new nightly shard closes the t/400/t/404 coverage gap. The t/400 L3 4-node hot-block liveness leg is red on the main base too (bisected: pre-existing, not from this branch) and is the next diagnosis lane.
  • Fixtures/baselines: t/248 raw pg_xact bits, t/365 convert-queue X-transfer contract, counter baseline refreshes (t/018, t/215/216, t/401).

Test plan

  • cluster_unit full suite green (incl. new retry-table / damping-streak / source-contract pins)
  • cluster_regress green
  • clang-format 0 violations (580 files)
  • TAP band green: t/243 t/247 t/248 t/362 t/365 t/370 t/401 (247 asserts)
  • Flake loops: t/247 13/13, t/248 3/3, t/362 3/3, t/370 4/4, t/346 regression 2/2, t/404 1/1
  • fast-gate CI green (this PR)
  • nightly full green before ship tag

SqlRush added 9 commits July 18, 2026 18:37
…losed contract

The L5 online-rejoin leg ran without the striping quartet and without a
shared per-thread WAL root: a recycled ITL ref then has no derivable
origin, and the L3 dead-member episode leaves HW remaster structurally
blocked, wedging the rejoin episode in WAIT_CLUSTER.  Arm the t/346
striping quartet (online_join / quorum_poll / join_convergence /
xid_striping) plus wal_threads_root so the admission window and the
verdict chain are actually exercised.

Post-admission, the surviving origin still refuses to serve: its durable
write-fence authority stays bound to the pre-rejoin epoch (survivor-side
republish gap).  Pin the interim contract instead of the historical
8-row read-back: the read must fail bounded with the exact TT-recycled
error, verdict wire and origin fence refusals must grow, and the
below-horizon / served counters must stay flat.  The 8-row read-back
becomes the future acceptance once the survivor republish lands.

Spec: spec-5.22-online-join-cold-formation.md (Amend v1.1)
…rness

CONNECTED proves the DATA channel, not application-protocol readiness:
PCM-X formation follows HELLO asynchronously, so a test that writes
through a live pair immediately after connect races the runtime gate.
Add wait_for_pcm_x_active to ClusterPair (both runtimes must publish
ACTIVE) and arm it in t/243 and t/247 before the first cross-node DML.
Quad shared-catalog rigs additionally imply online_join + xid_striping,
the substrate their formation now requires.
… shard

The generic IC path treats dest=self as a successful no-op, but this
application ACK is the drop proof a resource master that is also an S
holder must consume before the PCM-X transfer can advance.  Stage the
local arm through the tag-sharded DATA ring so its LMS worker performs
a real loopback dispatch with same-tag ordering; remote ACKs keep their
direct DATA connection.  The shard router accepts the ACK opcode for
the local node accordingly.
… classification

set_generation belongs to the holder-local tag, so two exact holders can
legitimately publish the same numeric generation: complete the namespace
with the authenticated source tuple before treating a stage begin as a
duplicate, and require the previous holder set to have reached the
graph-commit boundary before it may be replaced.  A non-current source
can only be replaying its last published set (STALE).

The terminal retry driver scans responders from node zero each pass;
once an earlier responder ACKed, a later responder may own the one
reliable leg.  Classify a completed responder before the generic
cross-responder BUSY check so that leg cannot hide behind the first
completed node, and treat a same-phase leg still armed after its
responder bit committed as the structural split it is.
A dirty N page at the PCM-X N-source image prep is legitimate, not
corruption evidence: relation extension (PageInit + MarkBufferDirty)
and recovery redo both dirty a page before any PCM grant exists, so the
first cluster-aware writer meets its own pre-grant dirt.  The N-source
contract serves STORAGE bytes and overwrites the resident copy, so
consuming the page as-is would discard the newer local bytes (a lost
write) — and judging it CORRUPT closed the whole runtime over a normal
extension.  Push the local bytes out first (WAL-first FlushBuffer under
a shared content lock, reserve+pin taken under the header lock) and
report BUSY: one flush converges the state and the image pump retries
against a clean page.  BM_IO_ERROR is judged before the dirty branch on
both passes; the second-pass check stays BUSY since REVOKING already
blocks data writes and residual dirt is idempotent hint traffic.  The
pcm_own source contract pins the new ordering.
…he runtime

Four sites treated benign concurrent progress as corruption and fused
the whole PCM-X runtime into RECOVERY_BLOCKED:

* A follower WFG snapshot returning STALE is the same slot-churn signal
  (promotion / round advance) the claim site already recovers from with
  a refresh lookup.  Route it to the exact re-dispatch via the new
  REFRESH_ROLE retry action instead of the kill path.
* The cleanup CANCEL_LOCAL arm cancelled with the same stale handle and
  fused on STALE.  Cancelling releases rather than confers authority,
  so rebuild the handle from the live slot (identity-exact lookup) and
  retry, bounded; a vanished membership is already terminal.
* An ACTIVE_PROBE ticket observed CLAIMED with its GRD pending-X cookie
  absent can be another actor mid two-phase (a claimed cancel clears the
  cookie before it finalizes; the serve path clears by identity).  The
  already-claimed ensure arm now mirrors the reserve-path recheck under
  the ticket domain lock before calling it BAD_STATE.
* One dispatch BAD_STATE observation is damped per ticket in process-
  local memory; only a streak surviving consecutive periodic drive
  ticks reaches the fail-closed verdict, with lowest-streak recycling
  under table pressure so a real wedge keeps its count.  CORRUPT /
  INVALID / NO_CAPACITY / COUNTER_EXHAUSTED still fuse immediately.

Unit tests pin the retry-table row, the damping streak semantics, and
the four recovery arms in source.
…queue lane

t/248 verifies the falsified pg_xact fixture by reading the two on-disk
status bits directly instead of txid_status(), whose local nextXid
horizon check does not apply to remote-origin xids.  t/365 upgrades the
2PC leg to the convert-queue contract: after churn forces terminal
verdict resolution the queue transfers X to the peer, so the leg now
asserts both cross-node increments chain onto the decided 2PC value
instead of pinning the former deferred-image availability boundary, and
accepts the queue reservation-conflict message as an explicit
fail-closed judge.  t/018, t/215/t/216 and t/401 refresh dump-key and
emit-row counts for the lane observability additions.
t/400 (PCM-X convert-queue 4-node liveness) and t/404 (crash-rejoin
stale-read/write fence) were in no nightly shard: the matrix covered
392-397 and 401-403, so neither ran in any CI tier.  Give them their own
shard — the quad formation and the full crash/restart cycle dominate the
wall clock (L342: every new t/ file lands in a shard the same commit).
…n poll

A do{}while body is not a loop block in Perl, so the "last" in the
self-handoff drain poll was a runtime error — and it fired exactly when
the polled counters advanced (the healthy path), killing the test after
assert 57 with exit 29.  Convert the poll to while(1) with an explicit
deadline exit.  Never caught because t/400 was in no CI shard until the
stage7-queue-liveness-rejoin shard landed.
@sqlrush
sqlrush merged commit 12ab61c into main Jul 18, 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