feat(ha-term-safety): out-of-band config bytes, hash-verify-before-activate, serve-fence (task 4.3)

Wires config_committed_marker to a production consumer (config_activate_committed):
verifies pushed bytes' sha256 against the committed marker before activation, fences
(loud, non-serving, stay-alive) on mismatch, and activates the latest committed marker
on recovery. Enforced via ha_dispatch_gate + promotion gate.

Fixes bug-981: a 2nd+ config upgrade could permanently fence a standby on a
push-before-commit ordering. Adds an off-iomgr delaymgr retry scheduled from the
commit-advance hook (atomic read + non-blocking enqueue only on the ACK thread) that
re-activates and clears the stale fence once the new marker commits and bytes verify.
This commit is contained in:
Rongsong Shen 2026-06-22 11:47:15 +00:00
parent c3201a2b1e
commit be4c1f0721
7 changed files with 1074 additions and 12 deletions

View file

@ -237,6 +237,23 @@ typedef struct {
* caught-up or stopped member never
* FATALs. NULL at init / when no fence
* timer is pending. */
/* ── New field added by ha-term-safety spec (Task 4.3) ──────── */
_Atomic int config_serve_fence; /* Config byte-delivery serve-fence
* (R7.9): set when the latest committed
* WAL_CONFIG_MARKER references config
* bytes whose sha256 does NOT match the
* locally-held bytes (or the bytes are
* absent locally). While set, the member
* MUST NOT serve under the new
* configuration; the bytes travel
* out-of-band via the existing config-push
* transport, so the member STAYS ALIVE and
* config_activate_committed KEEPS RETRYING
* until the matching bytes arrive and
* hash-verify, at which point the fence is
* cleared and the config is activated.
* NEVER self-terminates (loud non-serving
* fence only). 0 at init (ha_init memset). */
} ha_state_t;
/* ── Public API ────────────────────────────────────────────── */
@ -528,6 +545,17 @@ int ha_is_primary(const ha_state_t *ha);
*/
int ha_dispatch_gate(ha_state_t *ha, int opcode);
/*
* ha_config_promotion_fenced: 1 when this member is config serve-fenced and
* therefore NON-PROMOTABLE — its latest committed WAL_CONFIG_MARKER references
* config bytes it does not hold/match, so it must not become authoritative
* CTRL_PRIMARY (ha-term-safety task 4.3, R7.9). 0 otherwise. The pre-promotion
* gate in raft_cb_become_leader consults this; a fenced member stays ALIVE and
* non-promotable until the correct bytes arrive out-of-band and re-verify clears
* the fence (controller-never-self-terminate). Pure atomic read (no I/O).
*/
int ha_config_promotion_fenced(const ha_state_t *ha);
/*
* ha_apply_wal_catchup_entry: apply a single WAL catch-up entry to local
* persistent storage and advance ha->applied_lsn on success.