Skip to content

Commit 0cefd60

Browse files
authored
Merge pull request #45 from sqlrush/fix/native-xid-prehistory
GCS-race round 2/3: joiner native-xid visibility, GCS_BLOCK_DONE completion proof, xid wrap barrier
2 parents eb627f4 + b30d27f commit 0cefd60

70 files changed

Lines changed: 4015 additions & 227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/nightly.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,12 @@ jobs:
209209
- { name: stage7-lms-pool, ranges: "364 367", unit: false, regress: false }
210210
# t/365 spec-7.1a cross-instance write-write MVCC coordination.
211211
- { name: stage7-write-write, ranges: "365-365", unit: false, regress: false }
212-
# t/366 spec-7.2a GCS block dedup capacity + eager reclaim. Own shard:
213-
# 2-node shared_catalog bring-up + cross-node distinct-read dedup
214-
# pressure + injected retransmit-dedup correctness needs the wall
215-
# clock.
216-
- { name: stage7-gcs-dedup-capacity, ranges: "366-366", unit: false, regress: false }
212+
# t/366 spec-7.2a GCS block dedup capacity + eager reclaim, plus
213+
# t/371 GCS_DONE mixed-version compat (suppressed pre-protocol
214+
# binary at the default cap). Own shard: 2-node shared_catalog
215+
# bring-up + cross-node distinct-read dedup pressure + injected
216+
# retransmit-dedup correctness needs the wall clock.
217+
- { name: stage7-gcs-dedup-capacity, ranges: "366 371", unit: false, regress: false }
217218
# t/359 mxid-stripe gapwalk + t/368 multixact member-serve refuse
218219
# (spec-7.1 family; 368 renamed from the double-occupied t/360 per
219220
# the hub number ledger -- the lms data-plane faults file keeps 360).

src/backend/access/transam/clog.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,58 @@ TransactionIdSetPageStatusInternal(TransactionId xid, int nsubxids,
395395
XactCtl->shared->page_dirty[slotno] = true;
396396
}
397397

398+
/*
399+
* ClusterClogAdoptNativeStatus
400+
* PGRAC (GCS-race round-2 RC-E): SLRU-coherent repair write of one
401+
* native-era commit status during the post-recovery prehistory verify.
402+
*
403+
* A pre-seed base-backup joiner adopts the sealed native prehistory into
404+
* its pg_xact files before recovery; replaying the backup's WAL window can
405+
* then re-zero a CLOG page the adopt had filled (CLOG extend records), so
406+
* xids beyond the replay window on that page read IN_PROGRESS again. The
407+
* StartupXLOG-tail verify detects those holes against the CRC-validated
408+
* blob and repairs them HERE -- through the SLRU, never behind its back --
409+
* before the coverage latch may enable native-era LOCAL routing.
410+
*
411+
* No WAL: the authoritative WAL for these bits is the seed node's own
412+
* native-era history, already durable in the sealed blob; a crash before
413+
* the next CLOG flush just re-runs the same idempotent verify+repair on
414+
* the following boot. Runs single-threaded (startup process before
415+
* backends are admitted), status must be terminal, and the slot must
416+
* currently read IN_PROGRESS (TransactionIdSetStatusBit asserts that).
417+
*/
418+
void
419+
ClusterClogAdoptNativeStatus(TransactionId xid, XidStatus status)
420+
{
421+
int pageno = TransactionIdToPage(xid);
422+
int slotno;
423+
424+
Assert(TransactionIdIsNormal(xid));
425+
426+
/*
427+
* Terminal-only, enforced at RUNTIME (round-2 review F2 / calibration 1):
428+
* a sealed prehistory can legitimately carry SUB_COMMITTED bits (a
429+
* native-era crash mid-subcommit leaves them behind, and the blob has no
430+
* child->parent map to resolve them), and an Assert vanishes in
431+
* production builds. Materializing a non-terminal status as local truth
432+
* would launder an unprovable state; fail closed instead.
433+
*/
434+
if (status != TRANSACTION_STATUS_COMMITTED &&
435+
status != TRANSACTION_STATUS_ABORTED)
436+
ereport(FATAL,
437+
(errcode(ERRCODE_DATA_CORRUPTED),
438+
errmsg("cannot adopt non-terminal native-era commit status %d for transaction %u",
439+
(int) status, xid),
440+
errhint("The sealed native prehistory carries an unresolvable status; "
441+
"this node must stay fail-closed.")));
442+
443+
LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
444+
slotno = SimpleLruReadPage(XactCtl, pageno, true, xid);
445+
TransactionIdSetStatusBit(xid, status, InvalidXLogRecPtr, slotno);
446+
XactCtl->shared->page_dirty[slotno] = true;
447+
LWLockRelease(XactSLRULock);
448+
}
449+
398450
/*
399451
* When we cannot immediately acquire XactSLRULock in exclusive mode at
400452
* commit time, add ourselves to a list of processes that need their XIDs

src/backend/access/transam/varsup.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,31 @@ GetNewTransactionId(bool isSubXact)
184184
}
185185
}
186186
}
187+
188+
/*
189+
* PGRAC: GCS-race round-3 P0-1 — xid wrap-barrier allocation gate.
190+
*
191+
* No epoch>=1 xid may be issued until this node holds proof that every
192+
* member's native-prehistory coverage latch is off (durable
193+
* NATIVE_RAW_REUSED stamp + LMON ack round, or the boot shortcut on an
194+
* already-wrapped counter). A raw 32-bit value below the native
195+
* high-water stops being an alias-free native-era identity the moment
196+
* the first epoch-1 xid exists anywhere; issuing one before the barrier
197+
* completes could feed a still-latched epoch-0 peer a false LOCAL
198+
* visibility verdict (rule 8.A). Fail-closed and retryable (53RB5,
199+
* mirroring the 53RB2 posture above): the LMON barrier is
200+
* margin-triggered ~16M xids ahead and normally completes within a
201+
* tick, so hitting this gate means the round is still in flight (or a
202+
* member cannot participate — see the barrier's LOG lines).
203+
*/
204+
if (cluster_enabled && cluster_shared_catalog
205+
&& EpochFromFullTransactionId(full_xid) > 0
206+
&& !cluster_xid_wrap_barrier_passed())
207+
ereport(ERROR,
208+
(errcode(ERRCODE_CLUSTER_XID_AUTHORITY_UNAVAILABLE),
209+
errmsg("refusing to assign a new transaction ID: xid epoch rollover barrier is not complete"),
210+
errdetail("The first epoch-1 transaction ID may not be issued until every cluster member has durably disabled native-era prehistory routing."),
211+
errhint("The barrier completes automatically within about a second; retry the transaction. If this persists, check cluster connectivity and that every member runs a barrier-capable binary.")));
187212
#endif
188213

189214
/*----------
@@ -293,6 +318,17 @@ GetNewTransactionId(bool isSubXact)
293318
xid = XidFromFullTransactionId(full_xid);
294319
}
295320
}
321+
322+
/* PGRAC: GCS-race round-3 P0-1 — re-assert the wrap-barrier gate on
323+
* the re-derived candidate (same predicate as the first derivation). */
324+
if (cluster_enabled && cluster_shared_catalog
325+
&& EpochFromFullTransactionId(full_xid) > 0
326+
&& !cluster_xid_wrap_barrier_passed())
327+
ereport(ERROR,
328+
(errcode(ERRCODE_CLUSTER_XID_AUTHORITY_UNAVAILABLE),
329+
errmsg("refusing to assign a new transaction ID: xid epoch rollover barrier is not complete"),
330+
errdetail("The first epoch-1 transaction ID may not be issued until every cluster member has durably disabled native-era prehistory routing."),
331+
errhint("The barrier completes automatically within about a second; retry the transaction. If this persists, check cluster connectivity and that every member runs a barrier-capable binary.")));
296332
#endif
297333
}
298334

src/backend/access/transam/xlog.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
#include "cluster/cluster_hw_snapshot.h" /* PGRAC: spec-5.7 D3 HW authority checkpoint snapshot */
203203
#include "cluster/cluster_xid_stripe_xlog.h" /* PGRAC: spec-6.15 D5d checkpoint re-emit */
204204
#include "cluster/cluster_xid_authority.h" /* PGRAC: spec-6.15b native-era XID authority */
205+
#include "cluster/cluster_xid_wrap_barrier.h" /* PGRAC: GCS-race round-3 P0-1 startup mirror */
205206
#include "cluster/cluster_recovery_anchor.h" /* PGRAC: spec-5.6a per-node recovery anchor */
206207
#include "cluster/cluster_lms.h" /* PGRAC: spec-5.6 GES-ready boundary for CF X */
207208
#endif
@@ -6244,6 +6245,28 @@ StartupXLOG(void)
62446245
* No-op unless this startup acquired the claim.
62456246
*/
62466247
cluster_recovery_merge_claim_release_if_held();
6248+
6249+
/*
6250+
* PGRAC (GCS-race round-2 RC-E): with redo complete and the CLOG SLRU
6251+
* trimmed, prove that local pg_xact matches the sealed native-era
6252+
* prehistory over the surviving native range, repairing holes that the
6253+
* backup-window replay legitimately re-zeroed. Only this verify may
6254+
* latch the coverage high-water that lets the visibility resolver route
6255+
* a provably-native below-floor xid to the local adopted CLOG instead
6256+
* of failing closed (53R97). Skip legs leave the latch unset -- never
6257+
* wrong, at worst degraded to today's fail-closed behaviour.
6258+
*/
6259+
cluster_xid_prehistory_verify_native_coverage();
6260+
6261+
/*
6262+
* PGRAC (GCS-race round-3 P0-1): mirror a durable NATIVE_RAW_REUSED
6263+
* stamp into shmem BEFORE backends are admitted -- a stamped authority
6264+
* disables the coverage latch outright, and an already-wrapped counter
6265+
* takes the allocation-gate boot shortcut (the first carry was gated,
6266+
* so "every latch off" is a permanent global fact). Must run after
6267+
* the verify above so a same-boot latch can never outlive the mirror.
6268+
*/
6269+
cluster_xid_wrap_barrier_startup_init();
62476270
#endif
62486271

62496272
/*
@@ -7607,6 +7630,15 @@ CreateCheckPoint(int flags)
76077630
* the refusal still lands fail-closed on every joiner (53RB5,
76087631
* authority unsealed), but the seed itself can shut down cleanly
76097632
* instead of wedging every later shutdown/boot in a FATAL loop.
7633+
*
7634+
* The seal is also a PROOF (round-2 review F2): the coverage verify
7635+
* latches a blob-IN_PROGRESS == local-IN_PROGRESS xid as "crash-aborted
7636+
* forever, never resolvable" on the strength of the seal alone. That
7637+
* holds only when the sealing shutdown can prove no in-progress bit has
7638+
* a future: a PREPARED transaction survives clean shutdown as an
7639+
* in-progress CLOG bit that COMMIT PREPARED may later flip, and any
7640+
* still-active xact makes the image non-final. Either condition skips
7641+
* the seal (WARNING, joiners fail closed) instead of publishing a lie.
76107642
*/
76117643
if (shutdown && !(flags & CHECKPOINT_END_OF_RECOVERY)
76127644
&& cluster_shared_catalog && !cluster_enabled)
@@ -7621,6 +7653,23 @@ CreateCheckPoint(int flags)
76217653
checkPoint.nextMulti, FirstMultiXactId),
76227654
errhint("Joiners will fail closed (53RB5); recreate the seed without "
76237655
"MultiXact-producing operations, or move the load in-protocol.")));
7656+
else if (GetNumberOfPreparedTransactions() > 0)
7657+
ereport(WARNING,
7658+
(errmsg("prepared transactions survive this shutdown; "
7659+
"leaving the shared XID authority unsealed"),
7660+
errdetail("A sealed native-era history must prove every in-progress "
7661+
"xid crash-aborted, but %d prepared transaction(s) may still "
7662+
"commit or abort later.",
7663+
GetNumberOfPreparedTransactions()),
7664+
errhint("Joiners will fail closed (53RB5); resolve them with "
7665+
"COMMIT PREPARED or ROLLBACK PREPARED and shut down cleanly again.")));
7666+
else if (TransactionIdPrecedes(GetOldestActiveTransactionId(),
7667+
XidFromFullTransactionId(checkPoint.nextXid)))
7668+
ereport(WARNING,
7669+
(errmsg("active transactions survive this shutdown checkpoint; "
7670+
"leaving the shared XID authority unsealed"),
7671+
errhint("Joiners will fail closed (53RB5); shut the seed down cleanly "
7672+
"with no concurrent activity to seal the native era.")));
76247673
else if (cluster_xid_prehistory_payload_bytes(native_hw) == 0)
76257674
ereport(WARNING,
76267675
(errmsg("native-era xid high-water %llu is outside the prehistory publish "

src/backend/cluster/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ OBJS = \
240240
cluster_xid_stripe.o \
241241
cluster_xid_stripe_boot.o \
242242
cluster_xid_stripe_xlog.o \
243+
cluster_xid_wrap_barrier.o \
243244
cluster_xnode_lever.o \
244245
cluster_xnode_profile.o \
245246
storage/cluster_shared_fs.o \

src/backend/cluster/cluster_catalog_bootstrap.c

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,89 @@ cluster_catalog_prepare_xid_authority(const ControlFileData *cf,
181181
errhint(
182182
"Start the seed with cluster.enabled=off, stop it cleanly, then start joiners.")));
183183

184-
if (cluster_catalog_backup_label_present())
185-
elog(LOG, "cluster shared_catalog: skipped XID prehistory adopt on backup_label boot");
186-
else {
184+
if (cluster_catalog_backup_label_present()) {
185+
uint64 own_next = 0;
186+
187+
/*
188+
* PGRAC (GCS-race round-2 RC-E supply-side fix): a backup_label boot
189+
* used to skip the adopt unconditionally, assuming a post-seed
190+
* backup whose pg_xact already carries the native bits. A PRE-seed
191+
* base backup (the RACvsRAC S3 bring-up) breaks that assumption:
192+
* the clone's pg_xact predates every native seed xid, recovery
193+
* replays only the clone's own pre-seed WAL window, and the sealed
194+
* blob is the ONLY supply of the native outcomes -- skipping left
195+
* the joiner unable to prove any native xid (155k fail-closed storm
196+
* on xid 815).
197+
*
198+
* Adoption is lineage-safe exactly here: backup_label still present
199+
* means this node has NEVER completed a boot since it was cloned
200+
* (the first recovery renames the label), so its entire local
201+
* history is a subset of the seed lineage by construction -- a
202+
* clone that ran standalone would have consumed the label and takes
203+
* the anchor + prefix-check path below.
204+
*
205+
* Within the same lineage the adopt needs NO horizon comparison --
206+
* overwriting [blob start, native_hw) is idempotent same-lineage
207+
* truth on any clone that already carries (possibly torn) native
208+
* bits, and the post-recovery verify (StartupXLOG tail) re-proves
209+
* the whole range before the resolver may route native xids locally.
210+
*
211+
* One gate stands (round-2 review F3): the clone's OWN xid epoch. A
212+
* clone taken after an xid epoch rollover reuses the pg_xact
213+
* positions below the native high-water for cluster-era xids;
214+
* adopting native bits over them would corrupt live outcomes. The
215+
* clone's own pre-adopt nextFullXid comes from the local control
216+
* file when it is still per-node, or from the pre-migration epoch
217+
* witness under cluster.controlfile_shared_authority (the local
218+
* global/pg_control is then a symlink to the SHARED authority whose
219+
* checkpoint fields belong to the last permitted writer, not to
220+
* this clone -- reading it through the symlink would compare the
221+
* seed's own high-water against itself; the B3 trap).
222+
*/
223+
if (!cluster_controlfile_shared_authority)
224+
own_next = U64FromFullTransactionId(cf->checkPointCopy.nextXid);
225+
else if (!cluster_xid_epoch_witness_read(DataDir, &own_next))
226+
ereport(FATAL,
227+
(errcode(ERRCODE_CLUSTER_XID_AUTHORITY_UNAVAILABLE),
228+
errmsg("local xid epoch witness is unavailable for the backup_label "
229+
"prehistory adopt"),
230+
errdetail("Under cluster.controlfile_shared_authority the local control "
231+
"file is the shared symlink, and no pre-migration witness "
232+
"\"%s\" passes validation.",
233+
CLUSTER_XID_EPOCH_WITNESS_REL_PATH),
234+
errhint("Re-provision this node from the seed lineage.")));
235+
236+
if (own_next >= auth.native_hw_full) {
237+
/*
238+
* Round-3 review P0-2: the pre-adopt horizon is NOT donor-local
239+
* truth. pg_basebackup follows the shared-authority symlink
240+
* (basebackup.c Dc6), so the clone's control file -- and the
241+
* witness derived from it -- carries the LAST PERMITTED WRITER's
242+
* checkpointed nextXid, which can lag the actual donor (another
243+
* node may have allocated far past it, even across an epoch
244+
* rollover). The only value this horizon can PROVE is "the
245+
* authority was last checkpointed strictly before the seal", and
246+
* that is exactly own_next < native_hw_full: no cluster-era xid
247+
* (>= stripe floor >= hw) can exist anywhere under that reading,
248+
* so the clone's lineage is a seed-lineage subset and the adopt
249+
* is idempotent truth. Anything else -- post-seal backups
250+
* (native bits already carried), epoch rollovers (pg_xact
251+
* positions reused) -- skips: the coverage verify + repair path
252+
* (or 53R97) covers those without ever overwriting live
253+
* outcomes. Same predicate as the anchor path's adopt gate.
254+
*/
255+
elog(LOG,
256+
"cluster shared_catalog: skipped XID prehistory adopt on backup_label boot; "
257+
"pre-adopt nextXid %llu is not strictly below the native high-water %llu",
258+
(unsigned long long)own_next, (unsigned long long)auth.native_hw_full);
259+
} else {
260+
cluster_xid_prehistory_adopt(DataDir, auth.native_hw_full);
261+
elog(LOG,
262+
"cluster shared_catalog: adopted XID prehistory through native high-water %llu "
263+
"on backup_label boot",
264+
(unsigned long long)auth.native_hw_full);
265+
}
266+
} else {
187267
ClusterRecoveryAnchor ra;
188268
uint64 own_next;
189269

@@ -215,8 +295,18 @@ cluster_catalog_prepare_xid_authority(const ControlFileData *cf,
215295
* and truncated away are no alibi for the surviving range, and a
216296
* missing local page inside the comparable range fails closed
217297
* (UNAVAILABLE) instead of passing as a shorter clone.
298+
*
299+
* Epoch gate (round-2 review F3): past an xid epoch rollover the
300+
* node's pg_xact positions below the native high-water belong to
301+
* cluster-era xids, so the byte compare against the native blob is
302+
* meaningless -- it would FATAL a legitimate wrap-era node. The
303+
* 32-bit oldestXid raw value cannot express the epoch, so gate on
304+
* the anchor's full nextXid instead; post-wrap the prehistory
305+
* machinery is dead anyway (the coverage latch refuses to engage
306+
* and the resolver's widen judge proves nothing native).
218307
*/
219-
if ((uint64)ra.checkPointCopy.oldestXid <= auth.native_hw_full) {
308+
if (own_next <= (uint64)PG_UINT32_MAX
309+
&& (uint64)ra.checkPointCopy.oldestXid <= auth.native_hw_full) {
220310
ClusterXidPrefixVerdict pv;
221311

222312
pv = cluster_xid_prehistory_prefix_check(DataDir, auth.native_hw_full,

src/backend/cluster/cluster_cf_storage.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <sys/stat.h>
3535
#include <unistd.h>
3636

37+
#include "access/transam.h" /* U64FromFullTransactionId (epoch witness, review F3) */
3738
#include "catalog/pg_control.h"
3839
#include "cluster/cluster_cf_authority.h"
3940
#include "cluster/cluster_cf_enqueue.h"
@@ -45,6 +46,7 @@
4546
#include "cluster/cluster_guc.h"
4647
#include "cluster/cluster_qvotec.h"
4748
#include "cluster/cluster_recovery_anchor.h"
49+
#include "cluster/cluster_xid_authority.h" /* cluster_xid_epoch_witness_write (review F3) */
4850
#include "cluster/storage/cluster_shared_fs.h"
4951
#include "miscadmin.h"
5052
#include "port/pg_crc32c.h"
@@ -716,6 +718,22 @@ cluster_cf_migrate_and_link(const char *local_pgdata)
716718
}
717719
}
718720

721+
/*
722+
* PGRAC (GCS-race round-2 review F3): persist this node's own
723+
* pre-migration nextFullXid as the local epoch witness BEFORE the
724+
* symlink flip erases the last local copy of that value. A
725+
* backup_label first boot consumes it to refuse the native-prehistory
726+
* adopt when the clone was taken after an xid epoch rollover (its
727+
* pg_xact positions below the native high-water are reused by
728+
* cluster-era xids; adopting native bits over them would corrupt live
729+
* outcomes). Written durably before the flip (same R7 ordering as
730+
* the recovery anchor); a crash in between re-runs this arm with the
731+
* local control file still real, so the rewrite is idempotent.
732+
*/
733+
if (!cluster_xid_epoch_witness_write(
734+
local_pgdata, U64FromFullTransactionId(local_cf.checkPointCopy.nextXid)))
735+
return false;
736+
719737
if (!cluster_cf_contract_persist(local_pgdata, CLUSTER_CF_CONTRACT_LOCAL_PROBED,
720738
shared_cf.system_identifier))
721739
return false; /* could not record the identity anchor -> fail-closed */

0 commit comments

Comments
 (0)