feat(raft-library): task 3.4 — raft_on_vote_reply quorum tracking; 6 tests + review fixes
This commit is contained in:
parent
e0bcb35554
commit
f81054930f
3 changed files with 328 additions and 7 deletions
|
|
@ -163,11 +163,63 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
|
|||
|
||||
int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term)
|
||||
{
|
||||
(void)s;
|
||||
(void)from_idx;
|
||||
(void)granted;
|
||||
(void)term;
|
||||
return 0;
|
||||
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_in", NULL);
|
||||
|
||||
/* Step 1: term upgrade — higher term means we lost the election */
|
||||
if (term > s->term) {
|
||||
s->term = term;
|
||||
s->role = RAFT_FOLLOWER;
|
||||
s->election_decided = 1;
|
||||
s->cbs->on_become_follower(s->ctx, s->term, -1);
|
||||
s->cbs->on_cancel_timeout(s->ctx);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "became_follower", NULL);
|
||||
return RAFT_BECAME_FOLLOWER;
|
||||
}
|
||||
|
||||
/* Step 2: no-op guard — election already decided */
|
||||
if (s->election_decided) {
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "vote_reply_noop", NULL);
|
||||
return RAFT_PENDING;
|
||||
}
|
||||
|
||||
/* Step 3: count this reply */
|
||||
s->replies_received++;
|
||||
|
||||
/* Step 4: accumulate grant */
|
||||
if (granted)
|
||||
s->votes_granted++;
|
||||
|
||||
/* Step 5: quorum check */
|
||||
int quorum = (s->n_peers / 2) + 1;
|
||||
if (s->votes_granted >= quorum) {
|
||||
s->role = RAFT_LEADER;
|
||||
s->election_decided = 1;
|
||||
s->cbs->on_become_leader(s->ctx, s->term);
|
||||
s->cbs->on_cancel_timeout(s->ctx);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "quorum_reached", NULL);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "became_leader", NULL);
|
||||
return RAFT_BECAME_LEADER;
|
||||
}
|
||||
|
||||
/* Step 6: all-replied-no-quorum */
|
||||
if (s->replies_received == s->n_peers - 1 && s->votes_granted < quorum) {
|
||||
s->role = RAFT_FOLLOWER;
|
||||
s->election_decided = 1;
|
||||
s->cbs->on_become_follower(s->ctx, s->term, -1);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "became_follower", NULL);
|
||||
return RAFT_BECAME_FOLLOWER;
|
||||
}
|
||||
|
||||
/* Step 7: still waiting */
|
||||
return RAFT_PENDING;
|
||||
}
|
||||
|
||||
void raft_on_election_timeout(raft_state_t *s)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,14 @@ typedef struct {
|
|||
/* on_trace mock */
|
||||
int trace_call_count;
|
||||
char last_trace_event[64];
|
||||
|
||||
/* on_become_follower mock */
|
||||
int become_follower_count;
|
||||
uint64_t last_become_follower_term;
|
||||
int last_become_follower_leader_idx;
|
||||
|
||||
/* on_cancel_timeout mock */
|
||||
int cancel_timeout_count;
|
||||
} mock_cbs_state_t;
|
||||
|
||||
static mock_cbs_state_t g_mock;
|
||||
|
|
@ -120,6 +128,20 @@ static void mock_become_leader(void *ctx, uint64_t term)
|
|||
g_mock.last_become_leader_term = term;
|
||||
}
|
||||
|
||||
static void mock_become_follower(void *ctx, uint64_t term, int leader_idx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.become_follower_count++;
|
||||
g_mock.last_become_follower_term = term;
|
||||
g_mock.last_become_follower_leader_idx = leader_idx;
|
||||
}
|
||||
|
||||
static void mock_cancel_timeout(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.cancel_timeout_count++;
|
||||
}
|
||||
|
||||
static int mock_persist(void *ctx, uint64_t term, int voted_for)
|
||||
{
|
||||
(void)ctx;
|
||||
|
|
@ -161,10 +183,10 @@ static raft_callbacks_t make_mock_cbs(void)
|
|||
memset(&cbs, 0, sizeof(cbs));
|
||||
cbs.on_send_vote_request = mock_send_vote_request;
|
||||
cbs.on_become_leader = mock_become_leader;
|
||||
cbs.on_become_follower = stub_become_follower; /* plain stub */
|
||||
cbs.on_become_follower = mock_become_follower;
|
||||
cbs.on_persist = mock_persist;
|
||||
cbs.on_schedule_timeout = mock_schedule_timeout;
|
||||
cbs.on_cancel_timeout = stub_cancel_timeout;
|
||||
cbs.on_cancel_timeout = mock_cancel_timeout;
|
||||
cbs.on_start_election_after_delay = stub_start_election_after_delay;
|
||||
/* on_vote_eligible and on_trace left NULL — tests that need them set explicitly */
|
||||
return cbs;
|
||||
|
|
@ -964,6 +986,245 @@ static void test_vote_req_reply_term_always_set(void)
|
|||
TEST_ASSERT_EQUAL_UINT64(s.term, reply_term); /* must equal 5 */
|
||||
}
|
||||
|
||||
/* ---------- raft_on_vote_reply tests (task 3.4) ---------- */
|
||||
|
||||
/*
|
||||
* test_vote_reply_higher_term_steps_down
|
||||
*
|
||||
* Node is CANDIDATE at term=2. Receives reply with term=5 (higher).
|
||||
* Expected: RAFT_BECAME_FOLLOWER, role=FOLLOWER, term=5, election_decided=1,
|
||||
* on_become_follower fired with (5, -1), on_cancel_timeout fired once.
|
||||
*/
|
||||
static void test_vote_reply_higher_term_steps_down(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));
|
||||
/* Manually put node into CANDIDATE state at term=2 */
|
||||
s.role = RAFT_CANDIDATE;
|
||||
s.term = 2;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 0;
|
||||
|
||||
int rc = raft_on_vote_reply(&s, 1, 0, 5);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower must fire once with (term=5, leader_idx=-1) */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, g_mock.last_become_follower_term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
||||
|
||||
/* on_cancel_timeout must fire once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* on_become_leader must NOT fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_vote_reply_noop_after_decided
|
||||
*
|
||||
* election_decided=1 already set. Call raft_on_vote_reply.
|
||||
* Expected: RAFT_PENDING, no callbacks fire.
|
||||
*/
|
||||
static void test_vote_reply_noop_after_decided(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.role = RAFT_CANDIDATE;
|
||||
s.term = 2;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 1; /* already decided */
|
||||
|
||||
int rc = raft_on_vote_reply(&s, 1, 1, 2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc);
|
||||
|
||||
/* No callbacks must fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_vote_reply_quorum_3node_1grant
|
||||
*
|
||||
* 3-node cluster (quorum=2). Node is CANDIDATE with votes_granted=1 (self-vote).
|
||||
* One peer grant arrives → votes_granted=2 >= quorum=2.
|
||||
* Expected: RAFT_BECAME_LEADER, role=LEADER, election_decided=1,
|
||||
* on_become_leader fired, on_cancel_timeout fired.
|
||||
*/
|
||||
static void test_vote_reply_quorum_3node_1grant(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));
|
||||
/* Simulate state after raft_start_election: CANDIDATE, self-vote counted */
|
||||
s.role = RAFT_CANDIDATE;
|
||||
s.term = 1;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 0;
|
||||
|
||||
/* One peer grants */
|
||||
int rc = raft_on_vote_reply(&s, 1, 1, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.votes_granted);
|
||||
|
||||
/* on_become_leader must fire once with the current term */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_leader_term);
|
||||
|
||||
/* on_cancel_timeout must fire once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* on_become_follower must NOT fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_vote_reply_no_quorum_all_replied_3node
|
||||
*
|
||||
* 3-node cluster. Node starts election (votes_granted=1, replies_received=0).
|
||||
* Both peers deny. After second deny: replies_received=2 == n_peers-1=2,
|
||||
* votes_granted=1 < quorum=2 → RAFT_BECAME_FOLLOWER.
|
||||
*/
|
||||
static void test_vote_reply_no_quorum_all_replied_3node(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.role = RAFT_CANDIDATE;
|
||||
s.term = 1;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 0;
|
||||
|
||||
/* First deny: replies_received=1, not yet all replied */
|
||||
int rc1 = raft_on_vote_reply(&s, 1, 0, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.replies_received);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
|
||||
/* Second deny: replies_received=2 == n_peers-1=2 → step down */
|
||||
int rc2 = raft_on_vote_reply(&s, 2, 0, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_FOLLOWER, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower must fire once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, g_mock.last_become_follower_term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
||||
|
||||
/* on_become_leader must NOT fire */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
|
||||
/* on_cancel_timeout must NOT fire for all-replied-no-quorum (only term-upgrade and quorum paths cancel) */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_vote_reply_quorum_5node
|
||||
*
|
||||
* 5-node cluster (quorum=3). Node starts election (votes_granted=1).
|
||||
* Two grants arrive → votes_granted=3 = quorum=3 → RAFT_BECAME_LEADER.
|
||||
*/
|
||||
static void test_vote_reply_quorum_5node(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, 5, 0));
|
||||
s.role = RAFT_CANDIDATE;
|
||||
s.term = 1;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 0;
|
||||
|
||||
/* First grant: votes_granted=2, quorum=3 → still pending */
|
||||
int rc1 = raft_on_vote_reply(&s, 1, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.votes_granted);
|
||||
|
||||
/* Second grant: votes_granted=3 = quorum → leader */
|
||||
int rc2 = raft_on_vote_reply(&s, 2, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_BECAME_LEADER, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
TEST_ASSERT_EQUAL_INT(3, s.votes_granted);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_vote_reply_pending_waiting
|
||||
*
|
||||
* 3-node cluster. Node starts election (votes_granted=1, replies_received=0).
|
||||
* One deny arrives: replies_received=1, n_peers-1=2 → not all replied yet.
|
||||
* Expected: RAFT_PENDING, no role change, no callbacks.
|
||||
*/
|
||||
static void test_vote_reply_pending_waiting(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.role = RAFT_CANDIDATE;
|
||||
s.term = 1;
|
||||
s.votes_granted = 1;
|
||||
s.replies_received = 0;
|
||||
s.election_decided = 0;
|
||||
|
||||
/* One deny: replies_received=1, n_peers-1=2 → still waiting */
|
||||
int rc = raft_on_vote_reply(&s, 1, 0, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PENDING, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.election_decided);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.replies_received);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.votes_granted); /* unchanged */
|
||||
|
||||
/* No role-change callbacks */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
||||
}
|
||||
|
||||
/* ---------- raft_snapshot tests ---------- */
|
||||
|
||||
static void test_raft_snapshot_returns_current_state(void)
|
||||
|
|
@ -1025,6 +1286,14 @@ void test_raft(void)
|
|||
RUN_TEST(test_start_election_already_candidate);
|
||||
RUN_TEST(test_start_election_already_leader);
|
||||
|
||||
/* raft_on_vote_reply (task 3.4) */
|
||||
RUN_TEST(test_vote_reply_higher_term_steps_down);
|
||||
RUN_TEST(test_vote_reply_noop_after_decided);
|
||||
RUN_TEST(test_vote_reply_quorum_3node_1grant);
|
||||
RUN_TEST(test_vote_reply_no_quorum_all_replied_3node);
|
||||
RUN_TEST(test_vote_reply_quorum_5node);
|
||||
RUN_TEST(test_vote_reply_pending_waiting);
|
||||
|
||||
/* raft_on_vote_request (task 3.3) */
|
||||
RUN_TEST(test_vote_req_stale_term);
|
||||
RUN_TEST(test_vote_req_higher_term_updates_state);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue