feat(ha-single-leader-role): legacy election path symmetry — direct CTRL_PRIMARY via shared sequence (task 3.1)

This commit is contained in:
Rongsong Shen 2026-06-13 10:16:14 +00:00
parent c089fb69f3
commit b7b951b9e7
4 changed files with 167 additions and 46 deletions

View file

@ -2346,16 +2346,21 @@ apply_role:
ha->phase2_done = 1;
}
/* Enter COMMITTING: block job traffic until majority ack (Req 4.1, 8.1) */
atomic_store_explicit(&ha->role, (int32_t)CTRL_PRIMARY_COMMITTING,
/* ha-single-leader-role task 3.1 (Req 1.1, 1.5): enter CTRL_PRIMARY
* DIRECTLY — symmetric with raft_cb_become_leader's rewrite. There
* is no transitional COMMITTING state and no committing-timeout
* watchdog; new-leader safety comes from the own-term WAL_NOOP +
* writable_lsn gate armed below (ha_arm_own_term_gate), not from a
* fourth role value. The legacy XCE_TESTING election path OWNS its
* term (no raft library in test mode); it historically did NOT
* increment the term in this branch, so we preserve that — no
* ha_do_promote, no new term store (research.md §4 finding 2). */
atomic_store_explicit(&ha->role, (int32_t)CTRL_PRIMARY,
memory_order_release);
INFO("HA FSM: → CTRL_PRIMARY_COMMITTING (term=%" PRIu64
INFO("HA FSM: → CTRL_PRIMARY (term=%" PRIu64
", first_boot=%d)", atomic_load_explicit(&ha->term, memory_order_acquire),
is_first_boot);
/* Reset ack counter for this committing round (Req 5.2) */
atomic_store(&ha->repl_ack_count, 0);
/* Push config to standbys with lower config_tag (Req 7.1) */
#ifdef XCE_TESTING
if (ha_push_config_hook != NULL)
@ -2374,30 +2379,25 @@ apply_role:
ha_send_ctrlhello_to_all_standbys(c);
#endif
/* Arm committing timeout watchdog (degraded-mode fallback, Req 8.4) */
/* ha-single-leader-role task 3.1: route promotion side effects
* through the SAME shared sequence raft_cb_become_leader runs so the
* test-driven (legacy) and raft-driven promotion paths cannot diverge
* (design §Legacy Path Symmetry). Guarded on a real cluster_t — the
* helpers dereference &c->hooksys / &c->persist / &c->commit, and the
* unit-test evaluation path passes c == NULL. Identical role / hook /
* heartbeat / no-watchdog / own-term-gate state results either road. */
if (c) {
/* Cancel any prior committing timeout */
committing_timeout_ctx_t *prev = (committing_timeout_ctx_t *)
atomic_exchange(&ha->committing_timeout_ctx, NULL);
if (prev) {
atomic_store(&prev->cancelled, 1);
}
committing_timeout_ctx_t *ct = calloc(1, sizeof(*ct));
if (ct) {
atomic_init(&ct->cancelled, 0);
ct->ha = ha;
ct->c = c;
atomic_store(&ha->committing_timeout_ctx, ct);
int rc = delay_task_ms(&c->delaymgr,
(int)ha->config.committing_timeout_ms,
1, committing_timeout_fn, ct);
if (rc < 0) {
WARN("ha_elect_evaluate_and_signal: delay_task_ms "
"for committing timeout failed: %d", rc);
atomic_store(&ha->committing_timeout_ctx, NULL);
free(ct);
}
}
/* Register the persistence hooks BEFORE the own-term no-op append,
* so the persistence seam is live before any racing client write.
* Idempotent via the hooks_registered guard. */
ha_register_persist_hooks(c);
/* Periodic heartbeat to standbys (idempotency guard prevents
* double-arming if already started). */
ha_start_primary_heartbeat(c);
/* Own-term WAL_NOOP + writable_lsn gate (Req 1.4, 2.1, 2.2) —
* non-blocking; append failure degrades safe (gate unset,
* promotion still completes). */
ha_arm_own_term_gate(c);
}
} else {
/* STANDBY result */