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

@ -25,6 +25,9 @@ int validate_startup_config(const char *config_path,
/* forward declaration — implemented in config_repl.c */
int load_primary_config(cluster_t *c);
/* ha-term-safety task 4.3: hash-verified recovery-activate from the latest
* committed WAL_CONFIG_MARKER (-ENOENT falls back to load_primary_config). */
int config_activate_committed(cluster_t *c);
time_t last_print_time = 0;
unsigned long npkts = 0;
@ -193,12 +196,30 @@ int myapp_run(myconf_t *myconf)
* will receive the config via the primary's replication push post-election
* (Req 7.3). */
{
int pre_rc = load_primary_config(&c);
/* ha-term-safety task 4.3 (remediation r1, Finding B / R7.10): activate
* the config referenced by the LATEST COMMITTED WAL_CONFIG_MARKER,
* hash-VERIFIED against the locally-held bytes, instead of a blind
* load_primary_config. persist recovery (step 6 above) has already
* re-derived committed_lsn, so config_committed_marker reads the committed
* prefix here. Semantics:
* - committed marker present + bytes hash-match → activate (verified).
* - committed marker present + bytes mismatch/absent → loud serve-fence,
* member STAYS ALIVE and retries when correct bytes arrive
* out-of-band post-election (NEVER self-terminates).
* - NO committed marker (legacy / first boot) → -ENOENT falls back to
* the existing load_primary_config blind-load (no false fence). */
int pre_rc = config_activate_committed(&c);
if (pre_rc == 0) {
INFO("Pre-election config loaded: tag=%016llx",
INFO("Pre-election config activated: tag=%016llx",
(unsigned long long)c.myconf->config_version_tag);
} else if (pre_rc == -EAGAIN) {
ERROR("Pre-election config serve-FENCED: committed config marker "
"references bytes not yet present/matching locally — member "
"NON-SERVING under the new config, staying alive and awaiting "
"out-of-band config-push (R7.9)");
}
/* Ignore errors — node may have no config yet (first boot or standby) */
/* Ignore other errors — node may have no config yet (first boot/standby);
* it fetches config via the primary's push post-election (Req 7.3). */
}
/* ── Step 8: ha_start_election_async ─────────────────────────────────── */