feat(ha-prevote): implement pure PreVote reply tally (task 2.3)
raft_on_pre_vote_reply: per-peer-deduped tally returning WON at quorum ((n_peers/2)+1), LOST when all non-self peers reply without quorum, else PENDING; bounds-guards peer_idx. Never fires become-leader/follower, never mutates term/voted_for/role, never persists. 8 RED-first tests (3- and 5-node, dedup, out-of-range, purity). Raft PreVote pure core complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6db912b2a9
commit
9fac681510
3 changed files with 305 additions and 1 deletions
|
|
@ -3327,6 +3327,233 @@ static void test_begin_pre_vote_null_send_callback_suppressed(void)
|
|||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
}
|
||||
|
||||
/* ---------- raft_on_pre_vote_reply tally (task 2.3) ---------- */
|
||||
|
||||
/*
|
||||
* Helper: arm a pre-vote round on an n-node cluster at the given term.
|
||||
* Mirrors what raft_begin_pre_vote sets up (self-grant, in-progress), so the
|
||||
* tally tests can feed replies without depending on the send transport.
|
||||
*/
|
||||
static int arm_pre_vote_round(raft_state_t *s, raft_callbacks_t *cbs,
|
||||
int n_peers, int self_idx, uint64_t term)
|
||||
{
|
||||
int rc = init_vote_node(s, cbs, n_peers, self_idx, term);
|
||||
if (rc != 0)
|
||||
return rc;
|
||||
/* Suppress sends; we drive replies directly. */
|
||||
raft_begin_pre_vote(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_won_at_quorum
|
||||
*
|
||||
* 3-node cluster (quorum=2), self-grant already counted. One peer grants →
|
||||
* the round is WON. Tally lives on the raft state; in-progress is cleared.
|
||||
*/
|
||||
static void test_pre_vote_reply_won_at_quorum(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));
|
||||
/* begin sent to peers; clear send counter so it does not confuse asserts. */
|
||||
|
||||
int rc = raft_on_pre_vote_reply(&s, 1, 1 /* granted */);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* self + peer 1 */
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress); /* round closed */
|
||||
|
||||
/* CRITICAL: a win must NEVER fire leadership callbacks. */
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_lost_when_all_reply_no_quorum
|
||||
*
|
||||
* 3-node cluster. Both peers deny. After all non-self peers reply without
|
||||
* quorum, the round is LOST.
|
||||
*/
|
||||
static void test_pre_vote_reply_lost_when_all_reply_no_quorum(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));
|
||||
|
||||
int rc1 = raft_on_pre_vote_reply(&s, 1, 0 /* deny */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc1); /* one peer still out */
|
||||
|
||||
int rc2 = raft_on_pre_vote_reply(&s, 2, 0 /* deny */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_LOST, rc2); /* all replied, no quorum */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only */
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_pending_when_undecided
|
||||
*
|
||||
* 3-node cluster. One peer denies, the other has not replied yet → PENDING
|
||||
* (quorum still reachable).
|
||||
*/
|
||||
static void test_pre_vote_reply_pending_when_undecided(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));
|
||||
|
||||
int rc = raft_on_pre_vote_reply(&s, 1, 0 /* deny */);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only */
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress); /* still open */
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_duplicate_ignored
|
||||
*
|
||||
* A duplicate reply from the same peer must not double-count. A grant from
|
||||
* peer 1, then a second grant from peer 1, leaves the tally at 2 (self + one
|
||||
* grant). In a 5-node cluster (quorum=3) that keeps the round PENDING.
|
||||
*/
|
||||
static void test_pre_vote_reply_duplicate_ignored(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, 5, 0, 5));
|
||||
|
||||
int rc1 = raft_on_pre_vote_reply(&s, 1, 1 /* grant */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc1);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* self + peer 1 */
|
||||
|
||||
/* Duplicate grant from peer 1 — must be ignored. */
|
||||
int rc2 = raft_on_pre_vote_reply(&s, 1, 1 /* grant again */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc2);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.pre_votes_granted); /* unchanged: no double-count */
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_won_purity_state_unchanged
|
||||
*
|
||||
* On WON: term, voted_for, voted_for_term, role are all unchanged and no
|
||||
* persist / leadership callback fires.
|
||||
*/
|
||||
static void test_pre_vote_reply_won_purity_state_unchanged(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));
|
||||
/* A real vote cast earlier in this term must survive the tally. */
|
||||
s.voted_for = 2;
|
||||
s.voted_for_term = 5;
|
||||
|
||||
int rc = raft_on_pre_vote_reply(&s, 1, 1 /* grant */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(2, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_UINT64(5, s.voted_for_term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_lost_purity_state_unchanged
|
||||
*
|
||||
* On LOST: term / voted_for / role unchanged; no leadership callback.
|
||||
*/
|
||||
static void test_pre_vote_reply_lost_purity_state_unchanged(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, 7));
|
||||
s.voted_for = 1;
|
||||
s.voted_for_term = 7;
|
||||
|
||||
raft_on_pre_vote_reply(&s, 1, 0 /* deny */);
|
||||
int rc = raft_on_pre_vote_reply(&s, 2, 0 /* deny */);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_LOST, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(7, s.term);
|
||||
TEST_ASSERT_EQUAL_INT(1, s.voted_for);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, s.voted_for_term);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, s.role);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.persist_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_out_of_range_peer_ignored
|
||||
*
|
||||
* A peer_idx outside [0, n_peers) (or >= RAFT_MAX_PEERS) is ignored: no tally
|
||||
* change, round stays in progress, returns PENDING.
|
||||
*/
|
||||
static void test_pre_vote_reply_out_of_range_peer_ignored(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));
|
||||
|
||||
int rc_neg = raft_on_pre_vote_reply(&s, -1, 1 /* grant */);
|
||||
int rc_over = raft_on_pre_vote_reply(&s, 3, 1 /* grant */); /* == n_peers */
|
||||
int rc_max = raft_on_pre_vote_reply(&s, RAFT_MAX_PEERS, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_neg);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_over);
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING, rc_max);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_votes_granted); /* self only, no spurious count */
|
||||
TEST_ASSERT_EQUAL_INT(1, s.pre_vote_in_progress);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_pre_vote_reply_5node_won_at_three
|
||||
*
|
||||
* 5-node cluster (quorum=3): self + two granting peers → WON.
|
||||
*/
|
||||
static void test_pre_vote_reply_5node_won_at_three(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, 5, 0, 5));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_PENDING,
|
||||
raft_on_pre_vote_reply(&s, 1, 1));
|
||||
TEST_ASSERT_EQUAL_INT(RAFT_PRE_VOTE_WON,
|
||||
raft_on_pre_vote_reply(&s, 2, 1));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(3, s.pre_votes_granted);
|
||||
TEST_ASSERT_EQUAL_INT(0, s.pre_vote_in_progress);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_leader_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_mock.become_follower_count);
|
||||
}
|
||||
|
||||
/* ---------- test_raft entry point ---------- */
|
||||
|
||||
void test_raft(void)
|
||||
|
|
@ -3397,6 +3624,16 @@ void test_raft(void)
|
|||
RUN_TEST(test_begin_pre_vote_single_node_wins);
|
||||
RUN_TEST(test_begin_pre_vote_null_send_callback_suppressed);
|
||||
|
||||
/* raft_on_pre_vote_reply tally (task 2.3) */
|
||||
RUN_TEST(test_pre_vote_reply_won_at_quorum);
|
||||
RUN_TEST(test_pre_vote_reply_lost_when_all_reply_no_quorum);
|
||||
RUN_TEST(test_pre_vote_reply_pending_when_undecided);
|
||||
RUN_TEST(test_pre_vote_reply_duplicate_ignored);
|
||||
RUN_TEST(test_pre_vote_reply_won_purity_state_unchanged);
|
||||
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);
|
||||
|
||||
/* simulator init (task 4.1) */
|
||||
#ifdef XCE_TESTING
|
||||
RUN_TEST(test_sim_init_3node);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue