forked from lei.chen/myxce
feat(raft-library): task 3.5 — election timeout, leader-lost, check-and-start; 6 tests
This commit is contained in:
parent
f81054930f
commit
85a7e325a8
3 changed files with 262 additions and 6 deletions
|
|
@ -59,7 +59,7 @@
|
|||
- All-replied-no-quorum: if `replies_received == n_peers - 1` and `votes_granted < quorum`, set `role=RAFT_FOLLOWER`, `election_decided=1`, fire `on_become_follower(ctx, term, -1)`, `on_trace(became_follower)`, return `RAFT_BECAME_FOLLOWER`
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||
|
||||
- [ ] 3.5 Implement raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election
|
||||
- [x] 3.5 Implement raft_on_election_timeout, raft_on_leader_lost, raft_check_and_start_election
|
||||
- `raft_on_election_timeout`: if `election_decided`, fire `on_cancel_timeout`, `on_trace(election_timeout_noop)`, return; otherwise set `role=RAFT_FOLLOWER`, `election_decided=1`, fire `on_become_follower(ctx, term, -1)`, `on_cancel_timeout`, `on_trace(election_timeout)`
|
||||
- `raft_on_leader_lost`: clear `voted_for=-1`, set `role=RAFT_FOLLOWER`, fire `on_start_election_after_delay(ctx, my_priority * election_timeout_ms)`, fire `on_trace(leader_lost)`
|
||||
- `raft_check_and_start_election`: if `voted_for != -1` AND `voted_for != self_idx` (another node won during the delay), fire `on_trace(jitter_skip)`, return `RAFT_SKIP_ELECTION`; otherwise call `raft_start_election(s)` internally, return `RAFT_ELECTION_STARTED`
|
||||
|
|
|
|||
|
|
@ -224,16 +224,41 @@ int raft_on_vote_reply(raft_state_t *s, int from_idx, int granted, uint64_t term
|
|||
|
||||
void raft_on_election_timeout(raft_state_t *s)
|
||||
{
|
||||
(void)s;
|
||||
if (s->election_decided) {
|
||||
s->cbs->on_cancel_timeout(s->ctx);
|
||||
if (s->cbs->on_trace)
|
||||
s->cbs->on_trace(s->ctx, "election_timeout_noop", NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
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, "election_timeout", NULL);
|
||||
}
|
||||
|
||||
void raft_on_leader_lost(raft_state_t *s)
|
||||
{
|
||||
(void)s;
|
||||
s->voted_for = -1;
|
||||
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)
|
||||
s->cbs->on_trace(s->ctx, "leader_lost", NULL);
|
||||
}
|
||||
|
||||
int raft_check_and_start_election(raft_state_t *s)
|
||||
{
|
||||
(void)s;
|
||||
return 0;
|
||||
if (s->voted_for != -1 && s->voted_for != s->self_idx) {
|
||||
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 */
|
||||
int rc = raft_start_election(s);
|
||||
(void)rc;
|
||||
return RAFT_ELECTION_STARTED;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ typedef struct {
|
|||
|
||||
/* on_cancel_timeout mock */
|
||||
int cancel_timeout_count;
|
||||
|
||||
/* on_start_election_after_delay mock (task 3.5) */
|
||||
int start_election_after_delay_count;
|
||||
uint32_t last_start_election_delay;
|
||||
} mock_cbs_state_t;
|
||||
|
||||
static mock_cbs_state_t g_mock;
|
||||
|
|
@ -142,6 +146,13 @@ static void mock_cancel_timeout(void *ctx)
|
|||
g_mock.cancel_timeout_count++;
|
||||
}
|
||||
|
||||
static void mock_start_election_after_delay(void *ctx, uint32_t delay_ms)
|
||||
{
|
||||
(void)ctx;
|
||||
g_mock.start_election_after_delay_count++;
|
||||
g_mock.last_start_election_delay = delay_ms;
|
||||
}
|
||||
|
||||
static int mock_persist(void *ctx, uint64_t term, int voted_for)
|
||||
{
|
||||
(void)ctx;
|
||||
|
|
@ -187,7 +198,7 @@ static raft_callbacks_t make_mock_cbs(void)
|
|||
cbs.on_persist = mock_persist;
|
||||
cbs.on_schedule_timeout = mock_schedule_timeout;
|
||||
cbs.on_cancel_timeout = mock_cancel_timeout;
|
||||
cbs.on_start_election_after_delay = stub_start_election_after_delay;
|
||||
cbs.on_start_election_after_delay = mock_start_election_after_delay;
|
||||
/* on_vote_eligible and on_trace left NULL — tests that need them set explicitly */
|
||||
return cbs;
|
||||
}
|
||||
|
|
@ -1225,6 +1236,218 @@ static void test_vote_reply_pending_waiting(void)
|
|||
TEST_ASSERT_EQUAL_INT(0, g_mock.cancel_timeout_count);
|
||||
}
|
||||
|
||||
/* ---------- raft_on_election_timeout tests (task 3.5) ---------- */
|
||||
|
||||
/*
|
||||
* test_election_timeout_undecided
|
||||
*
|
||||
* Node is CANDIDATE, election_decided=0.
|
||||
* raft_on_election_timeout must:
|
||||
* - set role=FOLLOWER
|
||||
* - set election_decided=1
|
||||
* - fire on_become_follower once
|
||||
* - fire on_cancel_timeout once
|
||||
* - trace "election_timeout"
|
||||
*/
|
||||
static void test_election_timeout_undecided(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.election_decided = 0;
|
||||
|
||||
int become_follower_before = g_mock.become_follower_count;
|
||||
|
||||
raft_on_election_timeout(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.election_decided);
|
||||
|
||||
/* on_become_follower fired once (with current term, leader_idx=-1) */
|
||||
TEST_ASSERT_EQUAL_INT(become_follower_before + 1, g_mock.become_follower_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, g_mock.last_become_follower_term);
|
||||
TEST_ASSERT_EQUAL_INT(-1, g_mock.last_become_follower_leader_idx);
|
||||
|
||||
/* on_cancel_timeout fired once */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* trace fired with "election_timeout" */
|
||||
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("election_timeout", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_election_timeout_already_decided
|
||||
*
|
||||
* election_decided=1 already set (election outcome already fired).
|
||||
* raft_on_election_timeout must:
|
||||
* - fire on_cancel_timeout once (stale timer cleanup)
|
||||
* - NOT fire an additional on_become_follower
|
||||
* - trace "election_timeout_noop"
|
||||
*/
|
||||
static void test_election_timeout_already_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.election_decided = 1; /* already decided */
|
||||
|
||||
/* Snapshot become_follower_count before the call */
|
||||
int become_follower_before = g_mock.become_follower_count;
|
||||
|
||||
raft_on_election_timeout(&s);
|
||||
|
||||
/* on_cancel_timeout must fire once (stale timer cleanup) */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.cancel_timeout_count);
|
||||
|
||||
/* on_become_follower must NOT have fired an additional time */
|
||||
TEST_ASSERT_EQUAL_INT(become_follower_before, g_mock.become_follower_count);
|
||||
|
||||
/* trace fired with "election_timeout_noop" */
|
||||
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("election_timeout_noop", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/* ---------- raft_on_leader_lost tests (task 3.5) ---------- */
|
||||
|
||||
/*
|
||||
* test_leader_lost_clears_voted_for
|
||||
*
|
||||
* Node had voted_for=1. raft_on_leader_lost must:
|
||||
* - set voted_for=-1
|
||||
* - 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)
|
||||
{
|
||||
raft_state_t s;
|
||||
raft_callbacks_t cbs = make_mock_cbs();
|
||||
|
||||
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 = 2;
|
||||
cfg.initial_voted_for = -1;
|
||||
cfg.cbs = &cbs;
|
||||
TEST_ASSERT_EQUAL_INT(0, raft_init(&s, &cfg));
|
||||
|
||||
s.voted_for = 1;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
raft_on_leader_lost(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-1, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
/* on_start_election_after_delay fired once with delay = priority * timeout */
|
||||
TEST_ASSERT_EQUAL_INT(1, g_mock.start_election_after_delay_count);
|
||||
TEST_ASSERT_EQUAL_UINT32(2 * 500, g_mock.last_start_election_delay);
|
||||
|
||||
/* trace fired with "leader_lost" */
|
||||
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("leader_lost", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/* ---------- 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).
|
||||
* Must return RAFT_SKIP_ELECTION, role stays FOLLOWER, no election started.
|
||||
* Trace "jitter_skip" fires.
|
||||
*/
|
||||
static void test_check_and_start_voted_for_other(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.voted_for = 1; /* voted for peer 1, not self (0) */
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
int rc = raft_check_and_start_election(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_SKIP_ELECTION, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
/* No election RPCs must have fired */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.send_vote_request_count);
|
||||
|
||||
/* trace fired with "jitter_skip" */
|
||||
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.trace_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("jitter_skip", g_mock.last_trace_event);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_check_and_start_not_voted
|
||||
*
|
||||
* voted_for==-1 (no vote cast yet).
|
||||
* Must call raft_start_election internally and return RAFT_ELECTION_STARTED.
|
||||
* role becomes CANDIDATE (3-node cluster).
|
||||
*/
|
||||
static void test_check_and_start_not_voted(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.voted_for = -1;
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
int rc = raft_check_and_start_election(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_CANDIDATE, s.role);
|
||||
|
||||
/* raft_start_election must have fired vote requests */
|
||||
TEST_ASSERT_GREATER_THAN_INT(0, g_mock.send_vote_request_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_check_and_start_voted_for_self
|
||||
*
|
||||
* voted_for==self_idx (we previously voted for ourselves — this means
|
||||
* we already started an election, so start again to re-enter candidacy).
|
||||
* Must return RAFT_ELECTION_STARTED, not RAFT_SKIP_ELECTION.
|
||||
*/
|
||||
static void test_check_and_start_voted_for_self(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.voted_for = 0; /* voted for self (self_idx=0) */
|
||||
s.role = RAFT_FOLLOWER;
|
||||
|
||||
int rc = raft_check_and_start_election(&s);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_ELECTION_STARTED, rc);
|
||||
}
|
||||
|
||||
/* ---------- raft_snapshot tests ---------- */
|
||||
|
||||
static void test_raft_snapshot_returns_current_state(void)
|
||||
|
|
@ -1305,4 +1528,12 @@ void test_raft(void)
|
|||
RUN_TEST(test_vote_req_on_vote_eligible_null);
|
||||
RUN_TEST(test_vote_req_persist_fails);
|
||||
RUN_TEST(test_vote_req_reply_term_always_set);
|
||||
|
||||
/* 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_check_and_start_voted_for_other);
|
||||
RUN_TEST(test_check_and_start_not_voted);
|
||||
RUN_TEST(test_check_and_start_voted_for_self);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue