@@ -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
146167StaticAssertDecl (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
150173static 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+
375418uint64
376419cluster_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
0 commit comments