feat(ha-repl-hardening): majority-convergence → CTRL_PRIMARY transition (task 5.2)

- Add ha_check_majority_convergence() in ha.c: counts standby_converged[],
  promotes CTRL_PRIMARY_COMMITTING → CTRL_PRIMARY when all standbys converge
- Threshold: n_peers (all standbys) per spec test cases; single-node satisfies immediately
- Called from repl_ack_decode_and_dispatch() before replication dispatch
- Logs INFO "majority converged (N/M standbys) → CTRL_PRIMARY" (Req 7.4)
- Three unit tests: 1/2 converged → stays COMMITTING; 2/2 → PRIMARY; single-node → immediate
- Fix ha.h comment: clarify double-promotion prevention via vtask context, not CAS
This commit is contained in:
Rongsong Shen 2026-06-05 04:42:53 +00:00
parent d13010e4e0
commit 534bfd68fe
4 changed files with 218 additions and 1 deletions

View file

@ -1220,6 +1220,61 @@ void ha_process_standby_state(ha_state_t *ha, int peer_idx,
}
}
/*
* ha_check_majority_convergence — evaluate whether a majority of standbys have
* converged and promote to CTRL_PRIMARY if so (Task 5.2).
*
* Counts the number of converged standbys from ha->standby_converged[].
* The majority threshold is ha->n_peers (all standbys must converge).
* For single-node clusters (n_peers=0), the threshold is 0, satisfied immediately.
*
* Only acts when role == CTRL_PRIMARY_COMMITTING. Called from the single-threaded
* vtaskmgr/iomgr context so no additional lock is needed; the role check is
* sufficient to prevent double-promotion.
*
* Logs INFO "majority converged (N/M standbys) → CTRL_PRIMARY" (Req 7.4, 2.4).
* Calls ha_promote_to_primary(c) which increments term and registers hooks.
* When c == NULL (unit tests with zeroed cluster_t), calls ha_do_promote(ha)
* directly so role transitions correctly without a live cluster.
*
* Requirements: 2.3, 2.4, 6.3, 6.5 (ha-repl-hardening spec)
*/
void ha_check_majority_convergence(ha_state_t *ha, cluster_t *c)
{
if (!ha) return;
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role,
memory_order_acquire);
if (role != CTRL_PRIMARY_COMMITTING)
return;
/* Count converged standbys */
int n_peers = ha->n_peers;
int n_converged = 0;
for (int i = 0; i < n_peers && i < MAX_HA_CANDIDATES; i++) {
if (ha->standby_converged[i])
n_converged++;
}
/* Majority threshold: all standbys must converge (n_peers).
* Single-node (n_peers=0): threshold=0 — satisfied immediately. */
if (n_converged < n_peers)
return;
INFO("HA: majority converged (%d/%d standbys) → CTRL_PRIMARY",
n_converged, n_peers);
if (c) {
/* Full promotion: increments term, sets role=PRIMARY, registers hooks,
* arms periodic heartbeat to standbys. */
ha_promote_to_primary(c);
} else {
/* Test mode (c == NULL): directly set role so assertions work without
* a live cluster_t. ha_do_promote increments term and sets PRIMARY. */
ha_do_promote(ha);
}
}
/*
* ha_dispatch_repl_action_for_standby — execute the decision-loop action for
* one standby on the current heartbeat cycle (Task 3.3).
@ -2470,6 +2525,12 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
sr.config_tag, sr.applied_lsn,
committed_tag, committed_lsn);
/* Task 5.2: After updating convergence tracking, check whether a
* majority of standbys have converged and promote to CTRL_PRIMARY.
* Must run BEFORE the replication dispatch so that a fully-converged
* cluster enters PRIMARY before making further replication decisions. */
ha_check_majority_convergence(ha, c);
/* Derive oldest_wal_lsn from the WAL directory.
* The oldest retained segment's start LSN is the boundary —
* standbys below it need a snapshot (Task 3.5b). */