feat(raft-library): task 4.1 — sim data structures, raft_sim_init, dump_log; 2 tests
This commit is contained in:
parent
85a7e325a8
commit
d2e2c1ef8a
4 changed files with 349 additions and 2 deletions
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
- [ ] 4. (P) Deterministic N-node simulator (raft_sim.c)
|
- [ ] 4. (P) Deterministic N-node simulator (raft_sim.c)
|
||||||
_Depends: 3_
|
_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_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_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`
|
- `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`
|
||||||
|
|
|
||||||
|
|
@ -192,4 +192,85 @@ int raft_check_and_start_election(raft_state_t *s);
|
||||||
/* Fills snap with current state. Caller must hold lock. */
|
/* Fills snap with current state. Caller must hold lock. */
|
||||||
void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap);
|
void raft_snapshot(const raft_state_t *s, raft_snapshot_t *snap);
|
||||||
|
|
||||||
|
/* ---------- deterministic simulator (test builds only) ---------- */
|
||||||
|
|
||||||
|
#ifdef XCE_TESTING
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#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 */
|
#endif /* RAFT_H */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,207 @@
|
||||||
#ifdef XCE_TESTING
|
#ifdef XCE_TESTING
|
||||||
|
|
||||||
/* raft simulation helpers — populated in task 2 */
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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 */
|
#endif /* XCE_TESTING */
|
||||||
|
|
|
||||||
|
|
@ -1484,6 +1484,64 @@ static void test_raft_snapshot_returns_current_state(void)
|
||||||
TEST_ASSERT_EQUAL_INT(1, snap.election_decided);
|
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 ---------- */
|
/* ---------- test_raft entry point ---------- */
|
||||||
|
|
||||||
void test_raft(void)
|
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_voted_for_other);
|
||||||
RUN_TEST(test_check_and_start_not_voted);
|
RUN_TEST(test_check_and_start_not_voted);
|
||||||
RUN_TEST(test_check_and_start_voted_for_self);
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue