feat(raft-sim-conformance): bag push/deliver routes vote+AE RPCs through library handlers (task 2.1)

This commit is contained in:
Rongsong Shen 2026-06-24 12:10:11 +00:00
parent 631854303c
commit ae4495104a
4 changed files with 249 additions and 2 deletions

View file

@ -4542,6 +4542,108 @@ static void test_sim_conformance_init_append_readback(void)
TEST_ASSERT_EQUAL_UINT64(g1.prng, g2.prng);
}
/*
* test_sim_conformance_bag_deliver_routes_rpcs (raft-sim-conformance task 2.1; Req 2.2, 2.4)
*
* Observable completion for raft_sim_bag_push / raft_sim_bag_deliver(i):
* - A pushed VOTE_REQ delivered by index invokes raft_on_vote_request on dst and
* pushes a VOTE_RSP back onto the bag (src/dst swapped, granted=result). The
* request is consumed (swap-with-last removal); the reply is the surviving member.
* - A pushed AE_REQ delivered by index invokes raft_on_append_entries_request on
* dst (replicating the single entry) and pushes an AE_RSP back (src/dst swapped,
* success=result). The dst's in-memory log grows (handler ran).
* - A request to a PARTITIONED dst is NOT delivered: no reply is pushed, the dst
* state is unchanged, and the request stays in the bag (mirrors a dropped edge).
*/
static void test_sim_conformance_bag_deliver_routes_rpcs(void)
{
raft_sim_cluster_t c;
memset(&c, 0, sizeof(c));
/* 3-node conformance cluster; force node 0 to LEADER so it owns a real log
* tail and per-peer progress for a genuine AppendEntries. */
TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&c, 3, 2000, 0xBA61ULL));
raft_start_election(&c.nodes[0].state); /* sends vote reqs to the bag */
/* Drain the election vote traffic via direct delivery so node 0 wins, leaving
* the bag empty for the targeted assertions below. */
for (int g = 0; g < 64 && c.bag_count > 0; g++)
raft_sim_bag_deliver(&c, 0); /* always deliver head */
TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role);
/* ---- VOTE_REQ routing ---- */
c.bag_count = 0;
raft_sim_msg_t vreq;
memset(&vreq, 0, sizeof(vreq));
vreq.kind = RAFT_SIM_MSG_VOTE_REQ;
vreq.src = 1; /* candidate */
vreq.dst = 2; /* voter */
vreq.term = c.nodes[1].state.term + 5; /* fresh higher term -> grant */
vreq.last_log_term = 0; /* empty-log candidate vs empty voter */
vreq.last_log_index = 0;
TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&c, &vreq));
TEST_ASSERT_EQUAL_INT(1, c.bag_count);
TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_deliver(&c, 0));
/* Request consumed, exactly one VOTE_RSP now in the bag, src/dst swapped. */
TEST_ASSERT_EQUAL_INT(1, c.bag_count);
TEST_ASSERT_EQUAL_INT(RAFT_SIM_MSG_VOTE_RSP, c.bag[0].kind);
TEST_ASSERT_EQUAL_INT(2, c.bag[0].src); /* voter replies */
TEST_ASSERT_EQUAL_INT(1, c.bag[0].dst); /* back to candidate */
TEST_ASSERT_EQUAL_INT(1, c.bag[0].granted);
/* Handler ran on dst: node 2 adopted the higher term and recorded the vote. */
TEST_ASSERT_EQUAL_UINT64(vreq.term, c.nodes[2].state.term);
TEST_ASSERT_EQUAL_INT(1, c.nodes[2].state.voted_for);
/* ---- AE_REQ routing ---- */
c.bag_count = 0;
/* Leader appends one client value, then sends AppendEntries to peer 1 — the
* conformance send callback pushes a real AE_REQ onto the bag. */
uint64_t value = 0xABCDEF12u;
uint64_t out_index = 0;
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_log_append(&c.nodes[0].state, &value, sizeof(value), &out_index));
TEST_ASSERT_EQUAL_UINT64(1, out_index);
TEST_ASSERT_EQUAL_INT(RAFT_OK,
raft_send_append_entries(&c.nodes[0].state, 1, /*leader_commit=*/0));
TEST_ASSERT_EQUAL_INT(1, c.bag_count);
TEST_ASSERT_EQUAL_INT(RAFT_SIM_MSG_AE_REQ, c.bag[0].kind);
TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_deliver(&c, 0));
/* AE request consumed, one AE_RSP remains, src/dst swapped, success. */
TEST_ASSERT_EQUAL_INT(1, c.bag_count);
TEST_ASSERT_EQUAL_INT(RAFT_SIM_MSG_AE_RSP, c.bag[0].kind);
TEST_ASSERT_EQUAL_INT(1, c.bag[0].src); /* follower replies */
TEST_ASSERT_EQUAL_INT(0, c.bag[0].dst); /* back to leader */
TEST_ASSERT_EQUAL_INT(1, c.bag[0].success);
/* Handler ran on dst: node 1 replicated the single entry. */
TEST_ASSERT_EQUAL_INT(1, c.nodes[1].log_count);
TEST_ASSERT_EQUAL_UINT64(c.nodes[0].log[0].term, c.nodes[1].log[0].term);
/* ---- partitioned dst is NOT delivered ---- */
c.bag_count = 0;
raft_sim_partition(&c, 2, 1); /* isolate the voter */
int term2_before = (int)c.nodes[2].state.term;
int votedfor2_before = c.nodes[2].state.voted_for;
raft_sim_msg_t vreq2;
memset(&vreq2, 0, sizeof(vreq2));
vreq2.kind = RAFT_SIM_MSG_VOTE_REQ;
vreq2.src = 1;
vreq2.dst = 2; /* partitioned */
vreq2.term = c.nodes[2].state.term + 9;
TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&c, &vreq2));
TEST_ASSERT_EQUAL_INT(1, c.bag_count);
/* Delivery to a partitioned endpoint is a no-op: distinct non-zero status,
* the request stays in the bag, no reply pushed, dst state unchanged. */
TEST_ASSERT_NOT_EQUAL(0, raft_sim_bag_deliver(&c, 0));
TEST_ASSERT_EQUAL_INT(1, c.bag_count); /* request not consumed */
TEST_ASSERT_EQUAL_INT(RAFT_SIM_MSG_VOTE_REQ, c.bag[0].kind); /* still the request */
TEST_ASSERT_EQUAL_INT(term2_before, (int)c.nodes[2].state.term);
TEST_ASSERT_EQUAL_INT(votedfor2_before, c.nodes[2].state.voted_for);
}
/* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */
/*
@ -7108,6 +7210,9 @@ void test_raft(void)
/* conformance init + in-memory adapter callbacks (task 1.2) */
RUN_TEST(test_sim_conformance_init_append_readback);
/* conformance bag push/deliver routing (task 2.1) */
RUN_TEST(test_sim_conformance_bag_deliver_routes_rpcs);
/* Group A: basic election correctness (task 5.1) */
RUN_TEST(test_proto_A1_single_node_self_elects);
RUN_TEST(test_proto_A2_3node_one_leader);