From d2e2c1ef8a72cbe52f118147cc856efc833494ae Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 31 May 2026 06:11:34 +0000 Subject: [PATCH] =?UTF-8?q?feat(raft-library):=20task=204.1=20=E2=80=94=20?= =?UTF-8?q?sim=20data=20structures,=20raft=5Fsim=5Finit,=20dump=5Flog;=202?= =?UTF-8?q?=20tests?= 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.h | 81 ++++++++++++ src/lib/raft/raft_sim.c | 204 +++++++++++++++++++++++++++++- src/lib/testsuite/test_raft.c | 64 ++++++++++ 4 files changed, 349 insertions(+), 2 deletions(-) diff --git a/.kiro/specs/raft-library/tasks.md b/.kiro/specs/raft-library/tasks.md index 5661f20..22a5005 100644 --- a/.kiro/specs/raft-library/tasks.md +++ b/.kiro/specs/raft-library/tasks.md @@ -68,7 +68,7 @@ - [ ] 4. (P) Deterministic N-node simulator (raft_sim.c) _Depends: 3_ -- [ ] 4.1 Implement simulator data structures and initialization +- [x] 4.1 Implement simulator data structures and initialization - `raft_sim_node_t`: contains `raft_state_t state`, `raft_callbacks_t cbs`, `void *ctx` (points back to cluster), `int idx`, `int partition`, per-node event log buffer for `on_trace` capture - `raft_sim_cluster_t`: array of nodes up to `RAFT_SIM_MAX_NODES`, node count `n`, `leader_count` (incremented by each `on_become_leader` firing) - `raft_sim_init(cluster, n_nodes, election_timeout_ms)`: initializes `n` nodes with indices 0..n-1, priority=idx, wires `on_send_vote_request` to buffer the RPC for routing, wires `on_become_leader` to increment `leader_count` diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index 4b15469..11bc6a2 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -192,4 +192,85 @@ int raft_check_and_start_election(raft_state_t *s); /* Fills snap with current state. Caller must hold lock. */ void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap); +/* ---------- deterministic simulator (test builds only) ---------- */ + +#ifdef XCE_TESTING + +#include + +#define RAFT_SIM_MAX_NODES 8 + +/* Buffered RPC entry: one pending vote-request to route */ +typedef struct { + int src; + int dst; + uint64_t term; +} raft_sim_rpc_t; + +/* One simulated node */ +typedef struct { + raft_state_t state; + raft_callbacks_t cbs; /* wired to sim routing functions */ + void *ctx; /* points back to raft_sim_cluster_t */ + int idx; + int partition; /* 1 = isolated from other nodes */ + /* per-node trace event log for test assertions */ + char trace_buf[4096]; + int trace_len; +} raft_sim_node_t; + +/* N-node simulation cluster */ +typedef struct { + raft_sim_node_t nodes[RAFT_SIM_MAX_NODES]; + int n; + int leader_count; /* incremented by each on_become_leader */ + /* buffered RPC queue */ + raft_sim_rpc_t pending[64]; + int pending_count; +} raft_sim_cluster_t; + +/* + * raft_sim_init: initialize an n-node simulation cluster. + * Each node gets index 0..n-1, priority=idx, election_timeout_ms as given. + * Wires on_send_vote_request to buffer RPCs into c->pending. + * Wires on_become_leader to increment c->leader_count. + * All other required callbacks get no-op implementations. + * on_trace appends to the node's trace_buf. + * Returns 0 on success. + */ +int raft_sim_init(raft_sim_cluster_t *c, int n_nodes, + uint32_t election_timeout_ms); + +/* + * raft_sim_dump_log: print trace logs for all nodes to fp. + * If RAFT_SIM_VERBOSE env var is set, always prints. + */ +void raft_sim_dump_log(raft_sim_cluster_t *c, FILE *fp); + +/* + * raft_sim_deliver_pending: route all buffered RPCs synchronously. + * For each buffered (src, dst, term), calls raft_on_vote_request on dst + * then routes the reply back to src via raft_on_vote_reply. + * Skips delivery if dst has partition=1. + */ +void raft_sim_deliver_pending(raft_sim_cluster_t *c); + +/* + * raft_sim_start_all_elections: call raft_start_election on each node 0..n-1. + */ +void raft_sim_start_all_elections(raft_sim_cluster_t *c); + +/* + * raft_sim_fire_timeout: call raft_on_election_timeout on node at node_idx. + */ +void raft_sim_fire_timeout(raft_sim_cluster_t *c, int node_idx); + +/* + * raft_sim_partition: set/clear partition flag for node_idx. + * Subsequent raft_sim_deliver_pending drops RPCs to/from that node. + */ +void raft_sim_partition(raft_sim_cluster_t *c, int node_idx, int isolated); + +#endif /* XCE_TESTING */ + #endif /* RAFT_H */ diff --git a/src/lib/raft/raft_sim.c b/src/lib/raft/raft_sim.c index d09184f..911a3b4 100644 --- a/src/lib/raft/raft_sim.c +++ b/src/lib/raft/raft_sim.c @@ -1,5 +1,207 @@ #ifdef XCE_TESTING -/* raft simulation helpers — populated in task 2 */ +#include +#include + +#include "raft.h" + +/* ---------- no-op callbacks ---------- */ + +static void sim_noop_become_follower(void *ctx, uint64_t term, int leader_idx) +{ + (void)ctx; (void)term; (void)leader_idx; +} + +static int sim_noop_persist(void *ctx, uint64_t term, int voted_for) +{ + (void)ctx; (void)term; (void)voted_for; + return 0; +} + +static void sim_noop_schedule_timeout(void *ctx, uint32_t delay_ms) +{ + (void)ctx; (void)delay_ms; +} + +static void sim_noop_cancel_timeout(void *ctx) +{ + (void)ctx; +} + +static void sim_noop_start_election_after_delay(void *ctx, uint32_t delay_ms) +{ + (void)ctx; (void)delay_ms; +} + +/* ---------- wired callbacks ---------- */ + +/* + * sim_send_vote_request: buffer a pending RPC into the cluster queue. + * + * ctx is raft_sim_node_t * (the source node). + * The node's ctx field points to raft_sim_cluster_t *. + */ +static int sim_send_vote_request(void *ctx, int peer_idx, uint64_t term) +{ + raft_sim_node_t *node = (raft_sim_node_t *)ctx; + raft_sim_cluster_t *c = (raft_sim_cluster_t *)node->ctx; + + if (c->pending_count >= 64) + return -1; /* queue full — drop */ + + raft_sim_rpc_t *rpc = &c->pending[c->pending_count++]; + rpc->src = node->idx; + rpc->dst = peer_idx; + rpc->term = term; + return 0; +} + +/* + * sim_become_leader: increment cluster-level leader_count. + * + * ctx is raft_sim_node_t *. + */ +static void sim_become_leader(void *ctx, uint64_t term) +{ + raft_sim_node_t *node = (raft_sim_node_t *)ctx; + raft_sim_cluster_t *c = (raft_sim_cluster_t *)node->ctx; + (void)term; + c->leader_count++; +} + +/* + * sim_on_trace: append the event string to the node's trace_buf. + * + * ctx is raft_sim_node_t *. + */ +static void sim_on_trace(void *ctx, const char *event, const char *detail) +{ + raft_sim_node_t *node = (raft_sim_node_t *)ctx; + (void)detail; + + if (!event) + return; + + int remaining = (int)sizeof(node->trace_buf) - node->trace_len - 1; + if (remaining <= 0) + return; + + int written = snprintf(node->trace_buf + node->trace_len, (size_t)remaining, + "%s\n", event); + if (written > 0 && written < remaining) + node->trace_len += written; + else if (written >= remaining) + node->trace_len += remaining - 1; /* fill without null — snprintf already null-terminated */ +} + +/* ---------- public API ---------- */ + +int raft_sim_init(raft_sim_cluster_t *c, int n_nodes, uint32_t election_timeout_ms) +{ + memset(c, 0, sizeof(*c)); + + if (n_nodes < 1 || n_nodes > RAFT_SIM_MAX_NODES) + return -1; + + c->n = n_nodes; + c->leader_count = 0; + c->pending_count = 0; + + for (int i = 0; i < n_nodes; i++) { + raft_sim_node_t *node = &c->nodes[i]; + + /* Zero the node struct */ + memset(node, 0, sizeof(*node)); + + node->idx = i; + node->partition = 0; + node->ctx = c; /* points back to the cluster */ + + /* Wire callbacks */ + node->cbs.on_send_vote_request = sim_send_vote_request; + node->cbs.on_become_leader = sim_become_leader; + node->cbs.on_become_follower = sim_noop_become_follower; + node->cbs.on_persist = sim_noop_persist; + node->cbs.on_schedule_timeout = sim_noop_schedule_timeout; + node->cbs.on_cancel_timeout = sim_noop_cancel_timeout; + node->cbs.on_start_election_after_delay = sim_noop_start_election_after_delay; + node->cbs.on_vote_eligible = NULL; /* optional — treat all as eligible */ + node->cbs.on_trace = sim_on_trace; + + /* Initialize raft state — pass node as ctx so callbacks can find both + * node->idx (source) and node->ctx (cluster). */ + raft_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.n_peers = n_nodes; + cfg.self_idx = i; + cfg.my_priority = i; /* priority = index: node 0 has highest priority */ + cfg.election_timeout_ms = election_timeout_ms; + cfg.initial_term = 0; + cfg.initial_voted_for = -1; + cfg.cbs = &node->cbs; + cfg.ctx = node; /* node* as ctx — callbacks cast back to raft_sim_node_t* */ + + int rc = raft_init(&node->state, &cfg); + if (rc != 0) + return rc; + } + + return 0; +} + +void raft_sim_dump_log(raft_sim_cluster_t *c, FILE *fp) +{ + for (int i = 0; i < c->n; i++) { + raft_sim_node_t *node = &c->nodes[i]; + if (node->trace_len > 0) { + fprintf(fp, "[node%d trace]\n%.*s\n", + i, node->trace_len, node->trace_buf); + } + } +} + +void raft_sim_deliver_pending(raft_sim_cluster_t *c) +{ + /* Snapshot current count — callbacks may add more entries */ + int count = c->pending_count; + c->pending_count = 0; + + for (int i = 0; i < count; i++) { + raft_sim_rpc_t *rpc = &c->pending[i]; + int src = rpc->src; + int dst = rpc->dst; + uint64_t term = rpc->term; + + /* Drop if either endpoint is partitioned */ + if (c->nodes[src].partition || c->nodes[dst].partition) + continue; + + /* Deliver vote request to dst */ + uint64_t reply_term = 0; + int granted = raft_on_vote_request(&c->nodes[dst].state, src, term, + &reply_term); + + /* Route reply back to src */ + raft_on_vote_reply(&c->nodes[src].state, dst, granted, reply_term); + } +} + +void raft_sim_start_all_elections(raft_sim_cluster_t *c) +{ + for (int i = 0; i < c->n; i++) + raft_start_election(&c->nodes[i].state); +} + +void raft_sim_fire_timeout(raft_sim_cluster_t *c, int node_idx) +{ + if (node_idx >= 0 && node_idx < c->n) + raft_on_election_timeout(&c->nodes[node_idx].state); +} + +void raft_sim_partition(raft_sim_cluster_t *c, int node_idx, int isolated) +{ + if (node_idx >= 0 && node_idx < c->n) + c->nodes[node_idx].partition = isolated; +} #endif /* XCE_TESTING */ diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index 81356a1..30ffb06 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -1484,6 +1484,64 @@ static void test_raft_snapshot_returns_current_state(void) TEST_ASSERT_EQUAL_INT(1, snap.election_decided); } +/* ---------- simulator tests (task 4.1) ---------- */ + +#ifdef XCE_TESTING + +/* + * test_sim_init_3node + * + * raft_sim_init(c, 3, 2000) returns 0. + * All 3 nodes have: role=FOLLOWER, term=0, voted_for=-1. + * leader_count=0, pending_count=0. + */ +static void test_sim_init_3node(void) +{ + raft_sim_cluster_t c; + memset(&c, 0xFF, sizeof(c)); /* canary — force all fields to non-zero */ + + int rc = raft_sim_init(&c, 3, 2000); + + TEST_ASSERT_EQUAL_INT(0, rc); + TEST_ASSERT_EQUAL_INT(3, c.n); + TEST_ASSERT_EQUAL_INT(0, c.leader_count); + TEST_ASSERT_EQUAL_INT(0, c.pending_count); + + for (int i = 0; i < 3; i++) { + TEST_ASSERT_EQUAL_INT(RAFT_FOLLOWER, c.nodes[i].state.role); + TEST_ASSERT_EQUAL_UINT64(0, c.nodes[i].state.term); + TEST_ASSERT_EQUAL_INT(-1, c.nodes[i].state.voted_for); + TEST_ASSERT_EQUAL_INT(i, c.nodes[i].idx); + TEST_ASSERT_EQUAL_INT(0, c.nodes[i].partition); + } +} + +/* + * test_sim_trace_buf + * + * After raft_sim_init, calling raft_start_election on node 0 (which has + * on_trace wired) must append at least one event to node 0's trace_buf. + */ +static void test_sim_trace_buf(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + int rc = raft_sim_init(&c, 3, 2000); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* trace_buf must start empty */ + TEST_ASSERT_EQUAL_INT(0, c.nodes[0].trace_len); + + /* Start election on node 0 — on_trace wired in raft_sim_init will write to trace_buf */ + raft_start_election(&c.nodes[0].state); + + /* At least one trace event must have been written */ + TEST_ASSERT_GREATER_THAN_INT(0, c.nodes[0].trace_len); +} + +#endif /* XCE_TESTING */ + /* ---------- test_raft entry point ---------- */ void test_raft(void) @@ -1536,4 +1594,10 @@ void test_raft(void) 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); + + /* simulator init (task 4.1) */ +#ifdef XCE_TESTING + RUN_TEST(test_sim_init_3node); + RUN_TEST(test_sim_trace_buf); +#endif }