From 85a7e325a8530c258a2c335551222583c79ac81a Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 31 May 2026 06:02:29 +0000 Subject: [PATCH] =?UTF-8?q?feat(raft-library):=20task=203.5=20=E2=80=94=20?= =?UTF-8?q?election=20timeout,=20leader-lost,=20check-and-start;=206=20tes?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/specs/raft-library/tasks.md | 2 +- src/lib/raft/raft.c | 33 ++++- src/lib/testsuite/test_raft.c | 233 +++++++++++++++++++++++++++++- 3 files changed, 262 insertions(+), 6 deletions(-) diff --git a/.kiro/specs/raft-library/tasks.md b/.kiro/specs/raft-library/tasks.md index 5821890..5661f20 100644 --- a/.kiro/specs/raft-library/tasks.md +++ b/.kiro/specs/raft-library/tasks.md @@ -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` diff --git a/src/lib/raft/raft.c b/src/lib/raft/raft.c index 7080031..e224d77 100644 --- a/src/lib/raft/raft.c +++ b/src/lib/raft/raft.c @@ -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; } diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index ef414d5..81356a1 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -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); }