Skip to content

Commit 32edefe

Browse files
author
SqlRush
committed
feat(cluster): crash-rejoin re-declare barrier (Shape A) + cold-GRD fail-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.
1 parent 07c709a commit 32edefe

14 files changed

Lines changed: 414 additions & 43 deletions

src/backend/cluster/cluster_debug.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,10 @@ dump_grd_recovery(ReturnSetInfo *rsinfo)
13711371
fmt_int64((int64)c.join_block_views_rebuilt));
13721372
emit_row(rsinfo, "grd_recovery", "join_block_recovering_failclosed",
13731373
fmt_int64((int64)c.join_block_recovering_failclosed));
1374+
/* Shape A (crash-rejoin re-declare barrier): off-path crash-rejoin fence-arm
1375+
* events (standalone counter, not part of the snapshot struct). */
1376+
emit_row(rsinfo, "grd_recovery", "offpath_crash_rejoin_fenced",
1377+
fmt_int64((int64)cluster_grd_offpath_crash_rejoin_fenced_count()));
13741378
}
13751379

13761380
/*

src/backend/cluster/cluster_gcs_block.c

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,9 +1275,43 @@ cluster_gcs_block_fallback_verify_refresh(BufferDesc *buf, BufferTag tag, SCN ex
12751275
GcsLostWriteVerdict verdict;
12761276
bool refreshed = false; /* S3 forensics — storage re-read happened */
12771277

1278-
if (buf == NULL || !SCN_VALID(expected_scn))
1278+
if (buf == NULL)
12791279
return;
12801280

1281+
/*
1282+
* fix 2 (crash-rejoin re-declare barrier, defense in depth): an InvalidScn
1283+
* master watermark normally SKIPs (not SCN-tracked / old-binary master /
1284+
* holder re-ack). But if THIS self-home block is under the off-path crash-
1285+
* rejoin fence, the local GRD watermark was wiped by the restart, so an
1286+
* Invalid watermark can mask a stale home block — fail-closed instead of
1287+
* SKIP, except for a genuine extension block (never cross-node written).
1288+
* This is a second line behind the phase-gate boot barrier, which already
1289+
* fences the self-home block before the acquire reaches here.
1290+
*/
1291+
{
1292+
bool self_fenced
1293+
= (!cluster_online_join && cluster_gcs_lookup_master_static(tag) == cluster_node_id
1294+
&& cluster_conf_node_count() > 1 && !cluster_grd_offpath_boot_decided());
1295+
ClusterColdGrdVerdict cv = cluster_gcs_cold_grd_watermark_verdict(
1296+
SCN_VALID(expected_scn), self_fenced,
1297+
self_fenced && cluster_bufmgr_block_is_extension_for_gcs(tag));
1298+
1299+
if (cv == CLUSTER_COLD_GRD_SKIP)
1300+
return;
1301+
if (cv == CLUSTER_COLD_GRD_FAIL_CLOSED) {
1302+
pg_atomic_fetch_add_u64(&ClusterGcsBlock->fallback_scn_failclosed_count, 1);
1303+
ereport(ERROR,
1304+
(errcode(ERRCODE_CLUSTER_GCS_BLOCK_RESOURCE_RECOVERING),
1305+
errmsg("crash-rejoin: cannot prove home block ownership after restart "
1306+
"(cold GRD watermark) for tag spc=%u db=%u rel=%u block=%u",
1307+
tag.spcOid, tag.dbOid, tag.relNumber, tag.blockNum),
1308+
errhint("The block resource is recovering after an unclean restart; retry the "
1309+
"transaction, or enable cluster.online_join for an online re-declare "
1310+
"rejoin.")));
1311+
}
1312+
/* CLUSTER_COLD_GRD_PROVE: expected_scn valid — run the normal verdict. */
1313+
}
1314+
12811315
page_scn = cluster_bufmgr_read_block_scn_for_gcs(buf);
12821316
verdict = gcs_block_lost_write_verdict(expected_scn, page_scn);
12831317
if (verdict == GCS_LOST_WRITE_PASS) {
@@ -1466,8 +1500,8 @@ cluster_gcs_block_phase_for_tag(BufferTag tag)
14661500
* Skipped for online_join=on (its admission + join fence govern) and for
14671501
* a single declared node (no peer can hold a conflicting copy).
14681502
*/
1469-
if (!cluster_online_join && static_master == cluster_node_id
1470-
&& cluster_conf_node_count() > 1 && !cluster_grd_offpath_boot_decided()) {
1503+
if (!cluster_online_join && static_master == cluster_node_id && cluster_conf_node_count() > 1
1504+
&& !cluster_grd_offpath_boot_decided()) {
14711505
cluster_grd_inc_join_block_failclosed();
14721506
return GCS_BLOCK_RECOVERING;
14731507
}

src/backend/cluster/cluster_grd.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ cluster_grd_shmem_init(void)
770770
pg_atomic_init_u64(&cluster_grd_state->join_shards_remastered_count, 0);
771771
pg_atomic_init_u64(&cluster_grd_state->join_block_views_rebuilt_count, 0);
772772
pg_atomic_init_u64(&cluster_grd_state->join_block_recovering_failclosed_count, 0);
773+
pg_atomic_init_u64(&cluster_grd_state->offpath_crash_rejoin_fenced_count, 0);
773774
}
774775

775776
/* spec-2.15 v0.4 P1.1: entry HTAB allocation gated on GUC. GUC=0
@@ -1831,6 +1832,23 @@ cluster_grd_set_offpath_boot_decided(void)
18311832
pg_atomic_write_u32(&cluster_grd_state->offpath_boot_decided, 1);
18321833
}
18331834

1835+
/* Shape A observability: count an off-path crash-rejoin fence-arm (LMON single
1836+
* writer; read for dump_grd + t/404). */
1837+
void
1838+
cluster_grd_inc_offpath_crash_rejoin_fenced(void)
1839+
{
1840+
if (cluster_grd_state != NULL)
1841+
pg_atomic_fetch_add_u64(&cluster_grd_state->offpath_crash_rejoin_fenced_count, 1);
1842+
}
1843+
1844+
uint64
1845+
cluster_grd_offpath_crash_rejoin_fenced_count(void)
1846+
{
1847+
if (cluster_grd_state == NULL)
1848+
return 0;
1849+
return pg_atomic_read_u64(&cluster_grd_state->offpath_crash_rejoin_fenced_count);
1850+
}
1851+
18341852
/* spec-4.6 D5 — bulk counter snapshot for the dump path. */
18351853
void
18361854
cluster_grd_recovery_counters_snapshot(ClusterGrdRecoveryCounters *out)

src/backend/cluster/cluster_qvotec.c

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,34 @@ typedef struct ClusterQvotecShmem {
140140
pg_atomic_uint32 poll_cycle_count;
141141
pg_atomic_uint32 torn_write_detect_count;
142142
pg_atomic_uint32 _pad;
143-
uint8 _reserved[64];
143+
/*
144+
* Merge-order reservation (守门 07-15): the convert-queue lane claims
145+
* offset 64..71 for its self_incarnation (pg_atomic_uint64, commit
146+
* ee536b5bb7, StaticAssert-pinned). Queue merges first; this lane rebases
147+
* after and drops this placeholder so self_incarnation occupies 64..71 and
148+
* prior_unclean_death stays at 72. Keeping the byte layout identical now
149+
* makes that rebase a no-op on the wire/shmem image.
150+
*/
151+
uint8 _reserved_queue_self_incarnation[8]; /* offset 64..71 */
152+
/*
153+
* Crash-rejoin re-declare barrier (Shape A) — set ONCE at qvotec startup
154+
* (before the READY publish), read-only thereafter: 1 iff this node's
155+
* prior-incarnation self-slot on the voting disk still had the ALIVE flag
156+
* set (a clean shutdown clears it via qvotec_clear_self_alive_on_clean_
157+
* shutdown; a crash / immediate stop does NOT), i.e. this boot follows an
158+
* UNCLEAN death. The off-path rejoin tick fences self-home blocks +
159+
* closes the write gate on this, so a crash-rejoined node never cold-
160+
* serves stale ownership even when it restarts faster than the survivor's
161+
* dead-deadband (the epoch signal is INITIAL on both sides in that race).
162+
*/
163+
pg_atomic_uint32 prior_unclean_death; /* offset 72..75 */
164+
uint8 _reserved[52];
144165
} ClusterQvotecShmem;
145166

146167
StaticAssertDecl(sizeof(ClusterQvotecShmem) == 128,
147168
"ClusterQvotecShmem must be exactly 128 bytes (2 cache lines)");
169+
StaticAssertDecl(offsetof(ClusterQvotecShmem, prior_unclean_death) == 72,
170+
"prior_unclean_death must sit at offset 72 (queue lane owns 64..71)");
148171

149172

150173
static ClusterQvotecShmem *QvotecShmem = NULL;
@@ -267,6 +290,9 @@ cluster_qvotec_shmem_init(void)
267290
pg_atomic_init_u32(&QvotecShmem->poll_cycle_count, 0);
268291
pg_atomic_init_u32(&QvotecShmem->torn_write_detect_count, 0);
269292
pg_atomic_init_u32(&QvotecShmem->_pad, 0);
293+
memset(QvotecShmem->_reserved_queue_self_incarnation, 0,
294+
sizeof(QvotecShmem->_reserved_queue_self_incarnation));
295+
pg_atomic_init_u32(&QvotecShmem->prior_unclean_death, 0);
270296
memset(QvotecShmem->_reserved, 0, sizeof(QvotecShmem->_reserved));
271297
}
272298
}
@@ -372,6 +398,23 @@ cluster_qvotec_get_disks_total_count(void)
372398
return (int)pg_atomic_read_u32(&QvotecShmem->disks_total_count);
373399
}
374400

401+
/*
402+
* cluster_qvotec_prior_unclean_death -- crash-rejoin re-declare barrier
403+
* (Shape A). True iff this node's prior-incarnation self-slot on the voting
404+
* disk still carried the ALIVE flag at startup (an unclean death: a crash /
405+
* immediate stop that skipped the clean-shutdown ALIVE blank). Latched once
406+
* before the READY publish; stable for the incarnation. False when qvotec is
407+
* absent (no voting disks) so a diskless / single-node deployment is never
408+
* fenced by this signal.
409+
*/
410+
bool
411+
cluster_qvotec_prior_unclean_death(void)
412+
{
413+
if (QvotecShmem == NULL)
414+
return false;
415+
return pg_atomic_read_u32(&QvotecShmem->prior_unclean_death) != 0;
416+
}
417+
375418
uint64
376419
cluster_qvotec_get_current_epoch_at_boot(void)
377420
{
@@ -1749,7 +1792,7 @@ ClusterQvotecMain(void)
17491792
bool ghost_fresh = false;
17501793
int d;
17511794

1752-
for (d = 0; d < qvotec_n_disks && !ghost_fresh; d++) {
1795+
for (d = 0; d < qvotec_n_disks; d++) {
17531796
ClusterVotingSlot probe;
17541797
ClusterVotingDiskIoState rrc;
17551798

@@ -1759,14 +1802,29 @@ ClusterQvotecMain(void)
17591802
if (probe.generation == 0)
17601803
continue; /* never written */
17611804
if (!(probe.flags & CLUSTER_VOTING_SLOT_FLAG_ALIVE))
1762-
continue; /* prior shutdown cleared ALIVE — ok */
1805+
continue; /* prior shutdown cleared ALIVE — clean death, ok */
17631806
if (probe.incarnation == qvotec_self_incarnation)
17641807
continue; /* same incarnation — impossible but defensive */
1808+
1809+
/*
1810+
* Crash-rejoin re-declare barrier (Shape A) — a prior-incarnation
1811+
* self-slot that still carries ALIVE means the previous postmaster
1812+
* of THIS node died WITHOUT running the clean-shutdown blank
1813+
* (qvotec_clear_self_alive_on_clean_shutdown), i.e. an UNCLEAN
1814+
* death. Latch it REGARDLESS of freshness: a stale ALIVE ghost is
1815+
* still proof we crashed (we just crashed longer ago), and the
1816+
* fence must engage on a fast rejoin where the survivor has not yet
1817+
* advanced its epoch (the epoch signal is INITIAL on both sides).
1818+
* Single writer, before the READY publish; read-only afterwards.
1819+
*/
1820+
if (QvotecShmem != NULL)
1821+
pg_atomic_write_u32(&QvotecShmem->prior_unclean_death, 1);
1822+
17651823
if (probe.heartbeat_ts_us == 0)
17661824
continue;
17671825
if (now_us > probe.heartbeat_ts_us
17681826
&& (now_us - probe.heartbeat_ts_us) > heartbeat_timeout_us)
1769-
continue; /* already stale */
1827+
continue; /* already stale — no fast-restart Q6 sleep needed */
17701828
ghost_fresh = true;
17711829
}
17721830

src/backend/cluster/cluster_reconfig.c

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,32 +2191,64 @@ cluster_reconfig_offpath_rejoin_tick(void)
21912191
return;
21922192
}
21932193

2194-
if (cluster_reconfig_cluster_already_running()) {
2194+
/*
2195+
* REJOIN when EITHER signal fires:
2196+
* already_running -- a declared peer is observed past INITIAL (the
2197+
* survivor already reconfigured; slow rejoin).
2198+
* prior_unclean_death -- this node's prior-incarnation voting-disk
2199+
* self-slot still carried ALIVE (an unclean
2200+
* death). This is the ONLY signal that fires on
2201+
* a FAST rejoin, where the node restarts inside
2202+
* the survivor's dead-deadband so BOTH sides are
2203+
* still at epoch INITIAL and the epoch signal is
2204+
* blind (守门裁决 07-15: ALIVE bit, not epoch).
2205+
* The clean-shutdown blank clears ALIVE (keeps epoch), so a genuine clean
2206+
* co-boot never trips prior_unclean_death.
2207+
*/
2208+
if (cluster_reconfig_cluster_already_running() || cluster_qvotec_prior_unclean_death()) {
21952209
uint8 self_set[CLUSTER_RECONFIG_DEAD_BITMAP_BYTES] = { 0 };
21962210

2211+
/*
2212+
* Arm the epoch-keyed join fence too (belt: it engages on a SLOW
2213+
* rejoin where this node adopted a peer epoch > INITIAL). On a FAST
2214+
* rejoin this node is still at CLUSTER_EPOCH_INITIAL, so the monotonic-
2215+
* max fence epoch cannot rise above 0 and the join fence is a no-op —
2216+
* the READ fence is therefore carried by the boot barrier below, which
2217+
* is epoch-independent: we deliberately DO NOT set offpath_boot_decided,
2218+
* so the phase gate keeps self-home blocks RECOVERING for the whole
2219+
* incarnation (fail-closed until a clean restart / online_join).
2220+
*/
21972221
self_set[cluster_node_id >> 3] = (uint8)(1u << (cluster_node_id & 7));
21982222
cluster_grd_arm_join_pcm_fence(self_set); /* fence FIRST (8.A) */
21992223

22002224
LWLockAcquire(&ReconfigShmem->lock, LW_EXCLUSIVE);
22012225
ReconfigShmem->self_join_admitted = 0; /* then close the write gate */
22022226
LWLockRelease(&ReconfigShmem->lock);
22032227

2204-
cluster_grd_set_offpath_boot_decided();
2228+
/* NB: offpath_boot_decided stays 0 -> the boot barrier persists as the
2229+
* read fence for this incarnation. offpath_decided_local latches the
2230+
* tick so it does not re-arm / re-log every cycle. */
2231+
cluster_grd_inc_offpath_crash_rejoin_fenced();
22052232
offpath_decided_local = true;
22062233

22072234
ereport(LOG,
2208-
(errmsg("cluster membership: node %d crash-rejoined a running cluster with "
2209-
"cluster.online_join=off — home blocks fenced and writes closed "
2210-
"(53R60) to avoid serving stale ownership",
2211-
cluster_node_id),
2212-
errhint("Enable cluster.online_join for an online re-declare rejoin, or "
2213-
"cold-restart the cluster. Reads of peer-mastered blocks and "
2214-
"non-home work are unaffected.")));
2235+
(errmsg("cluster membership: node %d crash-rejoin detected (cluster.online_join="
2236+
"off%s) — home blocks fenced and writes closed (53R60) to avoid serving "
2237+
"stale ownership",
2238+
cluster_node_id,
2239+
cluster_qvotec_prior_unclean_death() ? ", prior unclean shutdown" : ""),
2240+
errhint("Enable cluster.online_join for an online re-declare rejoin (admission "
2241+
"self-heal is spec-5.22 follow-up), or cold-restart the cluster after a "
2242+
"full clean shutdown. Reads of peer-mastered blocks and non-home work "
2243+
"are unaffected.")));
22152244
return;
22162245
}
22172246

22182247
if (cluster_reconfig_bootstrap_quorum_at_initial()) {
2219-
/* Cold bootstrap: fresh cluster at INITIAL, no stale home blocks. */
2248+
/* Clean cold bootstrap: fresh cluster co-booting at INITIAL, ALIVE was
2249+
* cleared on the prior clean shutdown (or never written) — no stale
2250+
* home blocks. The !prior_unclean_death predicate is already proven by
2251+
* the REJOIN arm above (守门裁决 07-15 point 5②). */
22202252
cluster_grd_set_offpath_boot_decided();
22212253
offpath_decided_local = true;
22222254
return;

src/backend/storage/buffer/bufmgr.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8149,6 +8149,29 @@ cluster_bufmgr_read_block_scn_for_gcs(BufferDesc *buf)
81498149
return scn;
81508150
}
81518151

8152+
/*
8153+
* cluster_bufmgr_block_is_extension_for_gcs -- fix 2 (crash-rejoin cold-GRD
8154+
* watermark) extension-block whitelist input.
8155+
*
8156+
* True iff the tag's block number is at or beyond the relation's current
8157+
* durable size, i.e. a freshly-extended block that has never been written to
8158+
* shared storage (its InvalidScn watermark is correct, so the cold-GRD gate
8159+
* must SKIP it, not fail-closed). A pre-existing block (blockNum < nblocks)
8160+
* returns false, so an Invalid watermark on it under a self-fence is treated
8161+
* as a wiped/cold GRD watermark and fails closed. Invalidates the cached
8162+
* size first so a concurrent extension is not missed (cheap: one lseek).
8163+
*/
8164+
bool
8165+
cluster_bufmgr_block_is_extension_for_gcs(BufferTag tag)
8166+
{
8167+
SMgrRelation reln = smgropen(BufTagGetRelFileLocator(&tag), InvalidBackendId);
8168+
ForkNumber fork = BufTagGetForkNum(&tag);
8169+
8170+
/* Drop any cached size so a concurrent extension is not missed. */
8171+
smgrrelease(reln);
8172+
return tag.blockNum >= smgrnblocks(reln, fork);
8173+
}
8174+
81528175
bool
81538176
cluster_bufmgr_refresh_block_from_storage_for_gcs(BufferDesc *buf, SCN *out_page_scn)
81548177
{

src/include/cluster/cluster_gcs_block.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,51 @@ gcs_block_lost_write_verdict(SCN expected_scn, SCN shipped_scn)
11181118
return GCS_LOST_WRITE_PASS;
11191119
}
11201120

1121+
/*
1122+
* fix 2 (crash-rejoin re-declare barrier, defense in depth) — cold-GRD
1123+
* watermark verdict.
1124+
*
1125+
* The storage-fallback / local-master freshness gate normally SKIPs when the
1126+
* master pi_watermark_scn is InvalidScn (an old-binary master, a holder
1127+
* re-ack whose requester copy is authoritative, or a block that is simply not
1128+
* SCN-tracked). But a crash-rejoined node's LOCAL GRD watermark was WIPED by
1129+
* the restart, so within an active self-fence an InvalidScn watermark can mask
1130+
* a stale home block whose peer holds a newer version — a SKIP there is a
1131+
* silent fail-OPEN. This is a second line behind the phase-gate boot barrier,
1132+
* which already fences self-home blocks RECOVERING before the acquire reaches
1133+
* the freshness gate; it exists so any future path that reaches the freshness
1134+
* gate with a wiped watermark still fails closed.
1135+
*
1136+
* Pure truth table (header-only, unit-testable — no shmem, no I/O):
1137+
* expected_scn_valid -> PROVE (run the normal verdict)
1138+
* !valid, no self-fence -> SKIP (legit never-tracked / re-ack)
1139+
* !valid, self-fence, extension block -> SKIP (genuine new block, never
1140+
* cross-node written -> Invalid
1141+
* is correct; the storage refresh
1142+
* would read past EOF otherwise)
1143+
* !valid, self-fence, NOT an extension -> FAIL_CLOSED (wiped/cold GRD watermark on a
1144+
* pre-existing block — ambiguous,
1145+
* must not serve, Rule 8.A)
1146+
*/
1147+
typedef enum ClusterColdGrdVerdict {
1148+
CLUSTER_COLD_GRD_PROVE, /* watermark valid: run the lost-write verdict */
1149+
CLUSTER_COLD_GRD_SKIP, /* Invalid watermark, provably safe to keep local */
1150+
CLUSTER_COLD_GRD_FAIL_CLOSED, /* Invalid watermark under a self-fence: refuse */
1151+
} ClusterColdGrdVerdict;
1152+
1153+
static inline ClusterColdGrdVerdict
1154+
cluster_gcs_cold_grd_watermark_verdict(bool expected_scn_valid, bool self_fence_active,
1155+
bool is_extension_block)
1156+
{
1157+
if (expected_scn_valid)
1158+
return CLUSTER_COLD_GRD_PROVE;
1159+
if (!self_fence_active)
1160+
return CLUSTER_COLD_GRD_SKIP;
1161+
if (is_extension_block)
1162+
return CLUSTER_COLD_GRD_SKIP;
1163+
return CLUSTER_COLD_GRD_FAIL_CLOSED;
1164+
}
1165+
11211166
/* PGRAC: spec-5.2 D2 — read-image intent flag carried in reserved_0[0].
11221167
*
11231168
* When the master forwards an N→S read request to a node that holds the
@@ -1913,6 +1958,8 @@ extern ClusterBufmgrGcsDropResult cluster_bufmgr_invalidate_block_for_gcs(Buffer
19131958
* local copy → PASS keep / stale → refresh + re-verdict → 53R93). */
19141959
extern SCN cluster_bufmgr_read_block_scn_for_gcs(BufferDesc *buf);
19151960
extern bool cluster_bufmgr_refresh_block_from_storage_for_gcs(BufferDesc *buf, SCN *out_page_scn);
1961+
/* fix 2 (crash-rejoin cold-GRD watermark) extension-block whitelist input. */
1962+
extern bool cluster_bufmgr_block_is_extension_for_gcs(BufferTag tag);
19161963
extern void cluster_gcs_block_fallback_verify_refresh(BufferDesc *buf, BufferTag tag,
19171964
SCN expected_scn);
19181965
/* PGRAC: spec-5.2 D11 (writer-transfer-revoke) — by-tag local buffer drop

src/include/cluster/cluster_grd.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ typedef struct ClusterGrdShared {
419419
pg_atomic_uint64 join_shards_remastered_count; /* GRD shards moved to joiner */
420420
pg_atomic_uint64 join_block_views_rebuilt_count; /* joiner-home fences lifted */
421421
pg_atomic_uint64 join_block_recovering_failclosed_count; /* 53R9L denied (both gates) */
422+
pg_atomic_uint64 offpath_crash_rejoin_fenced_count; /* Shape A: off-path crash-rejoin
423+
* fence-arm events (LMON) */
422424
} ClusterGrdShared;
423425

424426
/* spec-2.17 D28b — extern atomic generation alloc helper(InitProcess hook). */
@@ -685,6 +687,8 @@ extern void cluster_grd_inc_join_block_failclosed(void);
685687
/* Shape A (crash-rejoin re-declare barrier) — off-path boot barrier flag. */
686688
extern bool cluster_grd_offpath_boot_decided(void);
687689
extern void cluster_grd_set_offpath_boot_decided(void);
690+
extern void cluster_grd_inc_offpath_crash_rejoin_fenced(void);
691+
extern uint64 cluster_grd_offpath_crash_rejoin_fenced_count(void);
688692

689693
/* spec-4.6 D5 — bulk snapshot of the 13 grd_recovery counters for the
690694
* pg_cluster_state dump (category 'grd_recovery'; one t/249 leg each). */

0 commit comments

Comments
 (0)