fix(raft): vote-reply per-peer dedup + stale-term drop (bug-1095/1096)
raft_on_vote_reply double-counted duplicated vote-responses (counter ignored from_idx) and failed to drop stale-term replies, allowing two leaders at the same term (bug-851 split-brain family). Mirror the existing pre-vote per-peer dedup pattern (new vote_replies[] set) and add DropStaleResponse (term < currentTerm). Surfaced by the raft-sim-conformance oracle (task 4.2). 47 raft / 264 overall lib tests pass.
This commit is contained in:
parent
e15bbf8190
commit
b5b7e01b92
3 changed files with 135 additions and 2 deletions
|
|
@ -70,6 +70,10 @@ int raft_init(raft_state_t *s, const raft_config_t *cfg)
|
|||
s->self_idx = cfg->self_idx;
|
||||
s->votes_granted = 0;
|
||||
s->replies_received = 0;
|
||||
/* Defensive: a node must never carry stale per-peer vote flags into its
|
||||
* first election (mirrors pre_vote_replies). raft_start_election re-clears
|
||||
* them, but zero here so a never-elected node has a clean array. */
|
||||
memset(s->vote_replies, 0, sizeof(s->vote_replies));
|
||||
s->election_decided = 0;
|
||||
s->election_timeout_ms = cfg->election_timeout_ms ? cfg->election_timeout_ms : 2000;
|
||||
s->my_priority = cfg->my_priority;
|
||||
|
|
@ -110,6 +114,13 @@ int raft_start_election(raft_state_t *s)
|
|||
s->voted_for_term = s->term;
|
||||
s->votes_granted = 1; /* self-vote */
|
||||
s->replies_received = 0;
|
||||
/* Fresh per-peer vote-reply tally for this election (mirrors
|
||||
* pre_vote_replies reset). Mark self as already-counted so a stray
|
||||
* self-addressed reply cannot double-count the self-vote that
|
||||
* votes_granted already includes. */
|
||||
memset(s->vote_replies, 0, sizeof(s->vote_replies));
|
||||
if (s->self_idx >= 0 && s->self_idx < RAFT_MAX_PEERS)
|
||||
s->vote_replies[s->self_idx] = 1;
|
||||
s->election_decided = 0;
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
|
|
@ -421,8 +432,6 @@ int raft_on_pre_vote_reply(raft_state_t *s, int peer_idx, int granted)
|
|||
|
||||
int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term)
|
||||
{
|
||||
(void)from_idx;
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_in", NULL);
|
||||
|
||||
|
|
@ -438,6 +447,15 @@ int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term
|
|||
return RAFT_BECAME_FOLLOWER;
|
||||
}
|
||||
|
||||
/* DropStaleResponse (raft.tla DropStaleResponse, mterm < currentTerm):
|
||||
* a reply from an older term is discarded, NOT counted toward this
|
||||
* election — and it is NOT a higher term, so it must not step us down. */
|
||||
if (term < s->term) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_stale_drop", NULL);
|
||||
return RAFT_PENDING;
|
||||
}
|
||||
|
||||
/* Step 2: no-op guard — election already decided */
|
||||
if (s->election_decided) {
|
||||
if (s->cbs->on_trace)
|
||||
|
|
@ -445,6 +463,24 @@ int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term
|
|||
return RAFT_PENDING;
|
||||
}
|
||||
|
||||
/* Bounds guard: ignore a reply from an out-of-range peer (mirrors the
|
||||
* pre-vote per-peer dedup bound). */
|
||||
if (from_idx < 0 || from_idx >= s->n_peers || from_idx >= RAFT_MAX_PEERS) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_out_of_range", NULL);
|
||||
return RAFT_PENDING;
|
||||
}
|
||||
|
||||
/* Per-peer idempotency (raft.tla votesGranted is a SET): a duplicate reply
|
||||
* from a peer we already counted is ignored — a legal DuplicateMessage must
|
||||
* not double-count toward quorum (Election Safety). */
|
||||
if (s->vote_replies[from_idx]) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_dup", NULL);
|
||||
return RAFT_PENDING;
|
||||
}
|
||||
s->vote_replies[from_idx] = 1;
|
||||
|
||||
/* Step 3: count this reply */
|
||||
s->replies_received++;
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ typedef struct {
|
|||
int self_idx; /* this node's index in [0, n_peers) */
|
||||
int votes_granted; /* votes collected in current election */
|
||||
int replies_received; /* total peer replies received (for all-replied-no-quorum, Req 4.4) */
|
||||
int vote_replies[RAFT_MAX_PEERS]; /* per-peer replied flag; avoids double-count (mirrors pre_vote_replies) */
|
||||
int election_decided; /* 1 once on_become_leader/follower fired */
|
||||
uint32_t election_timeout_ms;
|
||||
int my_priority; /* priority index for jitter (0 = highest) */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue