fix(ha-config-bootstrap,ha-repl-hardening): config-less node must not win election
Root cause (found via ha-convergence-recovery task 8 live 3-node E2E): a controller with no local config (config_version_tag==0) could become PRIMARY and push an empty config, breaking replication. Two upstream election-safety gaps, both outside ha-convergence-recovery: 1. RDHUP re-election bypassed the no-config 'voter only' guard. The startup path (ha_start_election_async) refuses to start an election when my_config_tag==0 && n_peers>1, but the RDHUP failover path (ha_on_primary_ctrl_fd_rdhup) called raft_on_leader_lost unconditionally. Fix: mirror the guard before elect_ctx alloc / hook / raft_on_leader_lost; a config-less node stays CTRL_STANDBY (voter only) and grants the configured peer instead of starting (and winning) its own election. 2. Voter freshness gate compared the candidate only against the voter's LOCAL config, ignoring the cluster's known committed state. Fix: ha_vote_self_freshness floors the comparison at max(local, learned-committed config_tag / committed_lsn), so a voter denies any candidate whose config/cluster state is outdated vs what the cluster has committed. Strictly additive (never denies the freshest node — Leader Completeness preserves liveness). TDD: test_rdhup_noconfig_node_does_not_elect + configured-node contrast; test_ha_vote_gate learned-floor deny + liveness + backward-compat. Full controller suite green under ASAN. commit.c and ha-convergence-recovery files untouched.
This commit is contained in:
parent
513b965f98
commit
c97c7632e9
6 changed files with 439 additions and 9 deletions
|
|
@ -377,11 +377,19 @@ static int myreq_handler(vtask_t* task)
|
|||
uint64_t cand_tag = vrq ? vrq->config_tag : 0;
|
||||
uint64_t cand_lsn = vrq ? vrq->last_log_lsn : 0;
|
||||
uint64_t cand_term = vrq ? vrq->term : 0;
|
||||
uint64_t my_tag = c->myconf->config_version_tag;
|
||||
uint64_t my_lsn = (uint64_t)atomic_load_explicit(
|
||||
uint64_t local_tag = c->myconf->config_version_tag;
|
||||
uint64_t local_lsn = (uint64_t)atomic_load_explicit(
|
||||
&c->ha.applied_lsn, memory_order_acquire);
|
||||
|
||||
/* Freshness gate: deny if candidate is not at least as fresh as us.
|
||||
/* Election-safety fix #2: the freshness floor is the max of our local
|
||||
* state and our learned-committed cluster state, so we deny candidates
|
||||
* that are outdated against what the cluster has already committed
|
||||
* (not merely against our local config file / applied LSN). */
|
||||
uint64_t my_tag = 0, my_lsn = 0;
|
||||
ha_vote_self_freshness(&c->ha, local_tag, local_lsn, &my_tag, &my_lsn);
|
||||
|
||||
/* Freshness gate: deny if candidate is not at least as fresh as the
|
||||
* effective floor.
|
||||
* (Req 2.4: config_tag must be ≥ ours; if equal, log_lsn must be ≥ ours.) */
|
||||
int freshness_ok = !(cand_tag < my_tag ||
|
||||
(cand_tag == my_tag && cand_lsn < my_lsn));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue