From e10cf9a6fc4b854eecb4a54c0d90eafa48a5ac6c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Fri, 5 Jun 2026 07:03:37 +0000 Subject: [PATCH] fix(ha-repl-hardening): n_peers count and single-node safe_lsn (task 6.5) - Fix ha_start_election_async: ha->n_peers = n_other (other peers), not candidates_count (total including self). The standby_converged[] array is indexed by peer index, so the bound must be n_other. - Fix ha_compute_safe_committed_lsn: return self_lsn for n_peers==0 (prevents WAL truncation on single-node re-election). - Integration test: 3-node cluster reaches CTRL_PRIMARY via majority convergence within ~20s; no degraded-mode WARN in logs. --- .kiro/specs/ha-repl-hardening/tasks.md | 2 +- src/controller/ha.c | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.kiro/specs/ha-repl-hardening/tasks.md b/.kiro/specs/ha-repl-hardening/tasks.md index 990fa4ad..c6f16871 100644 --- a/.kiro/specs/ha-repl-hardening/tasks.md +++ b/.kiro/specs/ha-repl-hardening/tasks.md @@ -233,7 +233,7 @@ - All tests pass under ASAN - _Requirements: 4.1, 4.2, 4.3, 4.4_ -- [ ] 6.5 Integration test: 3-node startup via majority convergence +- [x] 6.5 Integration test: 3-node startup via majority convergence - Requires task 5.3 (`committing_timeout_ms=60000`) deployed so timeout does not fire during test - Deploy via `/xce-deploy-ha`; all 3 nodes start within 15s - Verify primary enters `CTRL_PRIMARY` via majority-convergence INFO log (not committing timeout WARN) diff --git a/src/controller/ha.c b/src/controller/ha.c index 0b133f6d..161d05f1 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -3041,23 +3041,29 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c, raft_init(&ha->raft, &rcfg); } - /* Step 1: store peer count for ha_repl_ack_received quorum calculation */ - ha->n_peers = n_peers; - - /* Step 2: transition to CTRL_ELECTING before firing RPCs. + /* Step 1: transition to CTRL_ELECTING before firing RPCs. * raft_start_election fires on_become_leader / on_send_vote_request below; * the role must already be ELECTING so ha_dispatch_gate() admits vote RPCs. */ atomic_store_explicit(&ha->role, (int32_t)CTRL_ELECTING, memory_order_release); INFO("HA FSM: → CTRL_ELECTING (async, n_peers=%d)", n_peers); - /* Step 3: pre-allocate ha_elect_ctx_t so raft_cb_send_vote_request can + /* Step 2: pre-allocate ha_elect_ctx_t so raft_cb_send_vote_request can * read my_tag/my_lsn from it when building the capnp VoteRequest payload. * Vote counting is now owned by the raft library; this ctx is only a * metadata carrier for tag/lsn. For multi-node elections it is kept alive * until all per-peer vote_ep_ctx_t endpoints finish (iomgr lifetime). * For single-node, raft_start_election fires on_become_leader immediately - * so no ctx reads occur after return. */ + * so no ctx reads occur after return. + * + * n_other = other-node count (self excluded). standby_converged[] is + * indexed by votes_idx (relative, 0 .. n_other-1), so ha->n_peers must + * store n_other, NOT the total candidates_count. Both + * ha_check_majority_convergence and ha_repl_ack_received read ha->n_peers + * as "number of OTHER nodes". */ int n_other = (n_peers > 1) ? n_peers - 1 : 0; + + /* Store other-peer count for quorum calculations */ + ha->n_peers = n_other; ha_elect_ctx_t *ctx = calloc(1, sizeof(*ctx) + (size_t)n_other * sizeof(ctx->votes[0])); if (!ctx) {