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:
parent
acf78f9a94
commit
88a8bf8a47
7 changed files with 2463 additions and 201 deletions
1126
src/controller/ha.c
1126
src/controller/ha.c
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ int raft_init(raft_state_t *s, const raft_config_t *cfg)
|
|||
s->election_decided = 0;
|
||||
s->election_timeout_ms = cfg->election_timeout_ms ? cfg->election_timeout_ms : 2000;
|
||||
s->my_priority = cfg->my_priority;
|
||||
/* No leader lost yet: the restored term (raft_check_and_start_election is
|
||||
* only meaningful after raft_on_leader_lost, which overwrites this). */
|
||||
s->leader_lost_term = cfg->initial_term;
|
||||
/* Log length restored from the adapter's recovered WAL length; the library
|
||||
* never reads storage (same discipline as initial_term/voted_for). 0 on
|
||||
* first boot. (raft-log-replication Req 1.1; design "log_len restored at
|
||||
|
|
@ -374,6 +377,17 @@ int raft_on_pre_vote_reply(raft_state_t *s, int peer_idx, int granted)
|
|||
* controller (task 4.1) acts on. There is no term in a pre-vote reply, so
|
||||
* no term adoption is even possible here. */
|
||||
|
||||
/* No-op guard — the round is already decided, or none was started
|
||||
* (mirrors raft_on_vote_reply's election_decided check). WON and LOST
|
||||
* both clear pre_vote_in_progress, and pre_votes_granted never decreases,
|
||||
* so tallying a peer that had not replied yet would read a WON round as
|
||||
* WON again on its late GRANT or DENY (bug-1915). */
|
||||
if (!s->pre_vote_in_progress) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "pre_vote_reply_noop", NULL);
|
||||
return RAFT_PRE_VOTE_PENDING;
|
||||
}
|
||||
|
||||
/* Bounds guard: ignore replies from an out-of-range peer (mirrors the
|
||||
* per-peer dedup array bound). A self reply (peer_idx == self_idx) is
|
||||
* never expected — self is granted by raft_begin_pre_vote — and would also
|
||||
|
|
@ -537,8 +551,12 @@ void raft_on_election_timeout(raft_state_t *s)
|
|||
|
||||
void raft_on_leader_lost(raft_state_t *s)
|
||||
{
|
||||
s->voted_for = -1;
|
||||
s->role = RAFT_FOLLOWER;
|
||||
/* bug-1915 final review: do NOT clear voted_for. The term does not change
|
||||
* here, so a vote already cast in it still binds — clearing it let a
|
||||
* delayed RequestVote(term) from ANOTHER candidate be granted (a second
|
||||
* vote in one term). The delayed-election guard keys on the term instead. */
|
||||
s->leader_lost_term = s->term;
|
||||
s->role = RAFT_FOLLOWER;
|
||||
s->cbs->on_start_election_after_delay(s->ctx,
|
||||
(uint32_t)s->my_priority * s->election_timeout_ms);
|
||||
if (s->cbs->on_trace)
|
||||
|
|
@ -547,13 +565,18 @@ void raft_on_leader_lost(raft_state_t *s)
|
|||
|
||||
int raft_check_and_start_election(raft_state_t *s)
|
||||
{
|
||||
if (s->voted_for != -1 && s->voted_for != s->self_idx) {
|
||||
/* Skip unless still a FOLLOWER in the leader-lost term: a term change
|
||||
* during the delay (a grant to another candidate's higher term, a new
|
||||
* leader) or a role change means another node moved the cluster on.
|
||||
* voted_for is no longer a usable signal — raft_on_leader_lost keeps the
|
||||
* vote cast in this term (often for the lost leader itself). */
|
||||
if (s->term != s->leader_lost_term || s->role != RAFT_FOLLOWER) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "jitter_skip", NULL);
|
||||
return RAFT_SKIP_ELECTION;
|
||||
}
|
||||
|
||||
/* Precondition: raft_on_leader_lost sets role=FOLLOWER, so -EALREADY is unreachable here */
|
||||
/* Precondition: role == FOLLOWER (checked above), so -EALREADY is unreachable here */
|
||||
int rc = raft_start_election(s);
|
||||
(void)rc;
|
||||
return RAFT_ELECTION_STARTED;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ typedef struct {
|
|||
int election_decided; /* 1 once on_become_leader/follower fired */
|
||||
uint32_t election_timeout_ms;
|
||||
int my_priority; /* priority index for jitter (0 = highest) */
|
||||
/* Term in which raft_on_leader_lost last ran (volatile, never persisted).
|
||||
* raft_check_and_start_election starts the delayed election only while the
|
||||
* node is still a FOLLOWER in this term: a term change during the delay
|
||||
* means another candidate (or a new leader) moved the cluster on.
|
||||
* raft_on_leader_lost no longer clears voted_for (bug-1915 final review). */
|
||||
uint64_t leader_lost_term;
|
||||
/* ---- PreVote round tally (Req 1.2) ----
|
||||
* Self-contained pre-vote bookkeeping. A pre-vote round mutates NONE of
|
||||
* the persistent Raft state above (term, voted_for, role); these fields
|
||||
|
|
@ -284,14 +290,20 @@ void raft_on_election_timeout(raft_state_t *s);
|
|||
|
||||
/*
|
||||
* raft_on_leader_lost: primary control channel dropped (RDHUP).
|
||||
* Clears voted_for, transitions to FOLLOWER, fires on_start_election_after_delay.
|
||||
* Records leader_lost_term = term, transitions to FOLLOWER, fires
|
||||
* on_start_election_after_delay. Does NOT clear voted_for: the term is
|
||||
* unchanged, so the vote cast in it still binds (one vote per term — clearing
|
||||
* it let a delayed RequestVote of the same term from another candidate be
|
||||
* granted; bug-1915 final review).
|
||||
*/
|
||||
void raft_on_leader_lost(raft_state_t *s);
|
||||
|
||||
/*
|
||||
* raft_check_and_start_election: call after the delay from raft_on_leader_lost.
|
||||
* Starts election only if we have not already voted for a non-self candidate
|
||||
* in the new term (i.e., another node won during the jitter window).
|
||||
* Starts the election only if the node is still a FOLLOWER in the term in which
|
||||
* the leader was lost (term == leader_lost_term). Any term change during the
|
||||
* delay (a vote granted to another candidate's higher term, a new leader's
|
||||
* heartbeat) or a role change means the cluster moved on: skip ("jitter_skip").
|
||||
* Returns RAFT_SKIP_ELECTION or RAFT_ELECTION_STARTED.
|
||||
*/
|
||||
int raft_check_and_start_election(raft_state_t *s);
|
||||
|
|
@ -337,7 +349,10 @@ int raft_on_pre_vote_request(raft_state_t *s, int from_idx, uint64_t cand_term,
|
|||
* pre_vote_replies), mirroring raft_on_vote_reply but WITHOUT firing
|
||||
* on_become_leader/on_become_follower or making any role/term change.
|
||||
* Returns RAFT_PRE_VOTE_WON at quorum, RAFT_PRE_VOTE_LOST when all peers
|
||||
* replied without quorum, else RAFT_PRE_VOTE_PENDING.
|
||||
* replied without quorum, else RAFT_PRE_VOTE_PENDING. A reply while no round
|
||||
* is in progress (already decided WON/LOST, or never started) is a no-op that
|
||||
* returns RAFT_PRE_VOTE_PENDING and leaves the tally untouched, so a round is
|
||||
* decided at most once.
|
||||
*/
|
||||
int raft_on_pre_vote_reply(raft_state_t *s, int peer_idx, int granted);
|
||||
|
||||
|
|
|
|||
|
|
@ -3133,15 +3133,18 @@ static void test_election_timeout_already_decided(void)
|
|||
/* ---------- raft_on_leader_lost tests (task 3.5) ---------- */
|
||||
|
||||
/*
|
||||
* test_leader_lost_clears_voted_for
|
||||
* test_leader_lost_keeps_voted_for (was test_leader_lost_clears_voted_for)
|
||||
*
|
||||
* Node had voted_for=1. raft_on_leader_lost must:
|
||||
* - set voted_for=-1
|
||||
* Node had voted_for=1 in its current term. raft_on_leader_lost must:
|
||||
* - KEEP voted_for (bug-1915 final review: clearing it without a term change
|
||||
* let a delayed RequestVote of the SAME term from another candidate be
|
||||
* granted — a second vote in one term, an Election Safety violation)
|
||||
* - record leader_lost_term = term (raft_check_and_start_election's guard)
|
||||
* - set role=FOLLOWER
|
||||
* - fire on_start_election_after_delay with delay = my_priority * election_timeout_ms
|
||||
* - trace "leader_lost"
|
||||
*/
|
||||
static void test_leader_lost_clears_voted_for(void)
|
||||
static void test_leader_lost_keeps_voted_for(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
|
@ -3159,12 +3162,18 @@ static void test_leader_lost_clears_voted_for(void)
|
|||
cfg.cbs = &cbs;
|
||||
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
||||
|
||||
s.voted_for = 1;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
s.term = 5;
|
||||
s.voted_for = 1;
|
||||
s.voted_for_term = 5;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
raft_on_leader_lost(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, s.voted_for,
|
||||
"leader-lost must not clear the vote cast in the current term");
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.leader_lost_term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
/* on_start_election_after_delay fired once with delay = priority * timeout */
|
||||
|
|
@ -3176,12 +3185,59 @@ static void test_leader_lost_clears_voted_for(void)
|
|||
TEST_ASSERT_EQUAL_STRING("leader_lost", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_leader_lost_same_term_vote_from_other_denied (bug-1915 final review)
|
||||
*
|
||||
* Node voted for candidate 1 in term 5, then lost its leader. A delayed
|
||||
* RequestVote(term 5) from ANOTHER candidate (2) must be DENIED: at most one
|
||||
* vote per node per term, whatever happened to the leader in between.
|
||||
*/
|
||||
static void test_leader_lost_same_term_vote_from_other_denied(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
uint64_t reply_term = 0;
|
||||
|
||||
cbs.on_trace = mock_trace;
|
||||
reset_mock();
|
||||
|
||||
raft_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.n_peers = 3;
|
||||
cfg.self_idx = 0;
|
||||
cfg.election_timeout_ms = 500;
|
||||
cfg.my_priority = 1;
|
||||
cfg.initial_term = 5;
|
||||
cfg.initial_voted_for = 1; /* voted for candidate 1 in term 5 */
|
||||
cfg.cbs = &cbs;
|
||||
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
|
||||
|
||||
raft_on_leader_lost(&s);
|
||||
|
||||
int persist_before = g_mock.persist_count;
|
||||
int rc = raft_on_vote_request(&s, 2, 5, 0, 0, &reply_term);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_DENY, rc,
|
||||
"a second vote in term 5 (for candidate 2) must be denied after "
|
||||
"leader-lost");
|
||||
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(persist_before, g_mock.persist_count,
|
||||
"a denied vote must not persist");
|
||||
|
||||
/* The same candidate's retry is still an idempotent re-grant. */
|
||||
rc = raft_on_vote_request(&s, 1, 5, 0, 0, &reply_term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
|
||||
}
|
||||
|
||||
/* ---------- raft_check_and_start_election tests (task 3.5) ---------- */
|
||||
|
||||
/*
|
||||
* test_check_and_start_voted_for_other
|
||||
*
|
||||
* voted_for=1, self_idx=0 (voted for a different candidate).
|
||||
* Leader lost in term 3; during the delay this node granted another candidate
|
||||
* (peer 1) a vote in its higher term 4 (term moved on, voted_for=1).
|
||||
* Must return RAFT_SKIP_ELECTION, role stays FOLLOWER, no election started.
|
||||
* Trace "jitter_skip" fires.
|
||||
*/
|
||||
|
|
@ -3189,13 +3245,18 @@ static void test_check_and_start_voted_for_other(void)
|
|||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
uint64_t reply_term = 0;
|
||||
|
||||
cbs.on_trace = mock_trace;
|
||||
reset_mock();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
s.voted_for = 1; /* voted for peer 1, not self (0) */
|
||||
s.role = RAFT_FOLLOWER;
|
||||
s.term = 3;
|
||||
raft_on_leader_lost(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_GRANT,
|
||||
raft_on_vote_request(&s, 1, 4, 0, 0, &reply_term));
|
||||
TEST_ASSERT_EQUAL_INT(1, s.voted_for); /* voted for peer 1, not self (0) */
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
int rc = raft_check_and_start_election(&s);
|
||||
|
||||
|
|
@ -3210,6 +3271,54 @@ static void test_check_and_start_voted_for_other(void)
|
|||
TEST_ASSERT_EQUAL_STRING("jitter_skip", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_check_and_start_after_leader_lost_same_term_starts (bug-1915 final review)
|
||||
*
|
||||
* The vote raft_on_leader_lost now keeps (here: for the lost leader, peer 1,
|
||||
* in term 5) must not block the delayed election: the term has not moved, so
|
||||
* the node starts its election in term 6.
|
||||
*/
|
||||
static void test_check_and_start_after_leader_lost_same_term_starts(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
s.term = 5;
|
||||
s.voted_for = 1;
|
||||
s.voted_for_term = 5;
|
||||
raft_on_leader_lost(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED,
|
||||
raft_check_and_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(6, s.term);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_check_and_start_skips_when_not_follower (bug-1915 final review)
|
||||
*
|
||||
* Leader lost in term 5, then the node left FOLLOWER (e.g. another trigger
|
||||
* already started its election) — the delayed start must skip.
|
||||
*/
|
||||
static void test_check_and_start_skips_when_not_follower(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
cbs.on_trace = mock_trace;
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_node(&s, &cbs, 3, 0));
|
||||
s.term = 5;
|
||||
raft_on_leader_lost(&s);
|
||||
s.role = RAFT_CANDIDATE; /* same term, but no longer a follower */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, raft_check_and_start_election(&s));
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
TEST_ASSERT_EQUAL_STRING("jitter_skip", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_check_and_start_not_voted
|
||||
*
|
||||
|
|
@ -6461,11 +6570,11 @@ static void test_proto_F1_leader_lost_priority_delay(void)
|
|||
/*
|
||||
* test_proto_F2_voted_during_delay_skip
|
||||
*
|
||||
* Node calls raft_on_leader_lost (voted_for=-1).
|
||||
* Node calls raft_on_leader_lost (voted_for stays -1).
|
||||
* During the jitter delay, a vote request arrives from node 0 at term=1.
|
||||
* Node grants the vote (voted_for=0).
|
||||
* Then raft_check_and_start_election is called.
|
||||
* Since voted_for==0 != self_idx==1, must return RAFT_SKIP_ELECTION.
|
||||
* Since the term moved past leader_lost_term, must return RAFT_SKIP_ELECTION.
|
||||
*/
|
||||
static void test_proto_F2_voted_during_delay_skip(void)
|
||||
{
|
||||
|
|
@ -6484,7 +6593,7 @@ static void test_proto_F2_voted_during_delay_skip(void)
|
|||
cfg.cbs = &cbs;
|
||||
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
||||
|
||||
/* Leader lost: clears voted_for to -1, schedules delayed election */
|
||||
/* Leader lost: records leader_lost_term (voted_for kept), schedules delayed election */
|
||||
raft_on_leader_lost(&s);
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
|
||||
|
|
@ -6493,7 +6602,7 @@ static void test_proto_F2_voted_during_delay_skip(void)
|
|||
TEST_ASSERT_EQUAL_INT(RAFT_GRANT, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.voted_for);
|
||||
|
||||
/* Timer fires: check-and-start sees voted_for=0 != self_idx=1 → skip */
|
||||
/* Timer fires: check-and-start sees term 1 != leader_lost_term 0 → skip */
|
||||
int check_rc = raft_check_and_start_election(&s);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, check_rc);
|
||||
|
||||
|
|
@ -6524,7 +6633,7 @@ static void test_proto_F3_no_vote_during_delay_starts_election(void)
|
|||
cfg.cbs = &cbs;
|
||||
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
||||
|
||||
/* Leader lost: voted_for cleared to -1 */
|
||||
/* Leader lost (voted_for kept; it was -1) */
|
||||
raft_on_leader_lost(&s);
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
|
||||
|
|
@ -6539,11 +6648,11 @@ static void test_proto_F3_no_vote_during_delay_starts_election(void)
|
|||
* test_proto_F4_simultaneous_rdhup_one_leader
|
||||
*
|
||||
* Nodes 1 and 2 both receive RDHUP (leader/node 0 lost).
|
||||
* Both call raft_on_leader_lost → voted_for=-1 for both.
|
||||
* Both call raft_on_leader_lost (voted_for kept; -1 for both here).
|
||||
* Node 1 (lower idx, lower priority) calls check_and_start first → ELECTION_STARTED.
|
||||
* Node 1 sends vote requests; deliver them to node 2.
|
||||
* Node 2 grants node 1 (voted_for=1).
|
||||
* Node 2 then calls check_and_start → RAFT_SKIP_ELECTION (voted_for=1 != self_idx=2).
|
||||
* Node 2 then calls check_and_start → RAFT_SKIP_ELECTION (term moved past leader_lost_term).
|
||||
* Result: leader_count==1.
|
||||
*/
|
||||
static void test_proto_F4_simultaneous_rdhup_one_leader(void)
|
||||
|
|
@ -6574,7 +6683,7 @@ static void test_proto_F4_simultaneous_rdhup_one_leader(void)
|
|||
/* Node 2 received node 1's vote request and granted it */
|
||||
TEST_ASSERT_EQUAL_INT(1, c.nodes[2].state.voted_for);
|
||||
|
||||
/* Node 2's timer fires: check_and_start sees voted_for=1 != self_idx=2 → skip */
|
||||
/* Node 2's timer fires: its term moved (granted node 1's term) → skip */
|
||||
int rc2 = raft_check_and_start_election(&c.nodes[2].state);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, rc2);
|
||||
|
||||
|
|
@ -7538,6 +7647,63 @@ static void test_pre_vote_reply_5node_won_at_three(void)
|
|||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* bug-1915 follow-up (defence in depth): a PreVote round is decided exactly
|
||||
* once. After WON (or LOST) cleared pre_vote_in_progress, a reply from a peer
|
||||
* that has not been counted yet must be a no-op — PENDING, tally untouched —
|
||||
* mirroring raft_on_vote_reply's election_decided guard. Before the fix the
|
||||
* late reply was tallied and, since pre_votes_granted never decreases, a late
|
||||
* GRANT or DENY to a WON 3-node round read as WON a second time.
|
||||
*/
|
||||
static void test_pre_vote_reply_late_grant_after_won_is_noop(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, raft_on_pre_vote_reply(&s, 1, 1));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_PRE_VOTE_PENDING,
|
||||
raft_on_pre_vote_reply(&s, 2, 1 /* late grant */),
|
||||
"a reply to an already-decided PreVote round must not decide it again");
|
||||
TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* not tallied */
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_replies[2]); /* not marked */
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress);
|
||||
}
|
||||
|
||||
static void test_pre_vote_reply_late_deny_after_won_is_noop(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, arm_pre_vote_round(&s, &cbs, 3, 0, 5));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, raft_on_pre_vote_reply(&s, 1, 1));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(RAFT_PRE_VOTE_PENDING,
|
||||
raft_on_pre_vote_reply(&s, 2, 0 /* late deny */),
|
||||
"a late DENY must not re-read the old grant count as WON");
|
||||
TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_replies[2]);
|
||||
}
|
||||
|
||||
/* No round ever started (fresh state): a stray reply is ignored, not tallied. */
|
||||
static void test_pre_vote_reply_without_round_is_noop(void)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
reset_mock();
|
||||
TEST_ASSERT_EQUAL_INT(0, init_vote_node(&s, &cbs, 3, 0, 5));
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING,
|
||||
raft_on_pre_vote_reply(&s, 1, 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_replies[1]);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress);
|
||||
}
|
||||
|
||||
/* ====================================================================== *
|
||||
* Task 6.1 — Raft safety-invariant assertions for the PreVote API
|
||||
*
|
||||
|
|
@ -9210,8 +9376,11 @@ void test_raft(void)
|
|||
/* raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election (task 3.5) */
|
||||
RUN_TEST(test_election_timeout_undecided);
|
||||
RUN_TEST(test_election_timeout_already_decided);
|
||||
RUN_TEST(test_leader_lost_clears_voted_for);
|
||||
RUN_TEST(test_leader_lost_keeps_voted_for);
|
||||
RUN_TEST(test_leader_lost_same_term_vote_from_other_denied);
|
||||
RUN_TEST(test_check_and_start_voted_for_other);
|
||||
RUN_TEST(test_check_and_start_after_leader_lost_same_term_starts);
|
||||
RUN_TEST(test_check_and_start_skips_when_not_follower);
|
||||
RUN_TEST(test_check_and_start_not_voted);
|
||||
RUN_TEST(test_check_and_start_voted_for_self);
|
||||
|
||||
|
|
@ -9240,6 +9409,9 @@ void test_raft(void)
|
|||
RUN_TEST(test_pre_vote_reply_lost_purity_state_unchanged);
|
||||
RUN_TEST(test_pre_vote_reply_out_of_range_peer_ignored);
|
||||
RUN_TEST(test_pre_vote_reply_5node_won_at_three);
|
||||
RUN_TEST(test_pre_vote_reply_late_grant_after_won_is_noop);
|
||||
RUN_TEST(test_pre_vote_reply_late_deny_after_won_is_noop);
|
||||
RUN_TEST(test_pre_vote_reply_without_round_is_noop);
|
||||
|
||||
/* Raft safety-invariant assertions for the PreVote API (task 6.1, RN-1) */
|
||||
RUN_TEST(test_invariant_prevote_sequence_pure_no_persist);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue