fix(controller,lib): decide each PreVote round once and promote only with this election's votes (bug-1915)

A late PreVote grant after WON re-ran the WON path and started a second
real election that replaced elect_ctx, so the term-2 leader promoted no
vote endpoints. Rounds now carry an id and endpoints a round/kind; stale
or wrong-kind replies, timeouts and errors are dropped; raft ignores
replies with no round in progress. raft_cb_become_leader promotes only
with its own stamped election record (own term, majority), else steps
down and re-arms. raft_on_leader_lost keeps voted_for (one vote per
term) and the delayed election starts only as a follower in the lost
term. The failover stagger hands the PreVote start to a vtask so the
delaymgr stays trigger-only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-04 09:14:38 +00:00
parent acf78f9a94
commit 88a8bf8a47
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
7 changed files with 2463 additions and 201 deletions

File diff suppressed because it is too large Load diff

View file

@ -125,6 +125,14 @@ typedef struct {
* for the full rationale (a pure per-endpoint count would allow a
* spurious free before the round is superseded). */
_Atomic int refcount;
/* bug-1915 follow-up: which round this carrier belongs to, checked by
* raft_cb_become_leader before it trusts votes[]. election_term is the
* term ha_start_election_async stamped (raft.term + 1, under raft_lock,
* immediately before raft_start_election); 0 = not stamped (synthetic
* test carriers). is_pre_vote = 1 for a PreVote round carrier
* (ha_start_prevote_async), which never holds real votes. */
uint64_t election_term;
int is_pre_vote;
ha_vote_entry_t votes[]; /* C99 flexible array: n_peers-1 entries */
} ha_elect_ctx_t;
@ -245,6 +253,13 @@ typedef struct {
* -> real election, within one call chain) — that nesting must not
* self-deadlock, while a genuinely different thread still fully blocks. */
pthread_mutex_t raft_lock;
/* bug-1915 follow-up: PreVote round id. Incremented under raft_lock by
* ha_start_prevote_async for every round it starts and captured by that
* round's PreVote endpoints and round timeout, so a reply, endpoint error
* or timeout from an older round is dropped instead of being tallied in
* the current one (PreVote replies carry no round id on the wire).
* Read and written only under raft_lock. */
uint64_t pre_vote_round;
/* ── New fields added by raft-library spec (Task 6.2) ──────── */
cluster_t *raft_c; /* cluster_t* set by ha_start_election_async; used by raft callbacks */
void *raft_election_timeout_ctx; /* election_timeout_ctx_t* for cancel (task 6.2) */

File diff suppressed because it is too large Load diff

View file

@ -737,6 +737,13 @@ static void test_post_connect_vote_failure_routes_through_production_tally(void)
extern int pre_vote_ep_err_handler_for_test(endpoint_t *ep);
/* bug-1915 follow-up: a production PreVote endpoint is stamped with its kind
* and the round it belongs to (raft_cb_send_pre_vote_request); an endpoint
* built with vote_ep_ctx_build_for_test is stamped the same way here, for the
* round currently in progress. */
extern void vote_ep_ctx_mark_pre_vote_for_test(endpoint_t *ep, uint64_t round);
extern uint64_t ha_pre_vote_round_for_test(ha_state_t *ha);
extern void pre_vote_reply_dispatch_for_test(ha_state_t *ha, cluster_t *c,
int peer_idx, int granted);
@ -797,6 +804,7 @@ static void test_post_connect_prevote_failure_routes_through_production_tally(vo
* registered err_handler (pre_vote_ep_err_handler) on this same ep. */
vote_ep_ctx_build_for_test(&ep, &ha, ctx, /*peer_idx=*/1,
4ULL, 100ULL, 50ULL, "ctrl-a");
vote_ep_ctx_mark_pre_vote_for_test(&ep, ha_pre_vote_round_for_test(&ha));
int rc = pre_vote_ep_err_handler_for_test(&ep);
TEST_ASSERT_EQUAL_INT(0, rc);
@ -850,6 +858,7 @@ static void test_prevote_round_reaches_quorum_decision_via_fast_tally(void)
make_endpoint(&ep1);
vote_ep_ctx_build_for_test(&ep1, &ha, ctx, /*peer_idx=*/1,
4ULL, 100ULL, 50ULL, "ctrl-a");
vote_ep_ctx_mark_pre_vote_for_test(&ep1, ha_pre_vote_round_for_test(&ha));
TEST_ASSERT_EQUAL_INT(0, pre_vote_ep_err_handler_for_test(&ep1));
TEST_ASSERT_EQUAL_INT_MESSAGE(1, ha.raft.pre_vote_in_progress,