From b450e60d13523cf7ecf4709ee4850c3f2f8ab3e8 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 24 Jun 2026 12:19:15 +0000 Subject: [PATCH] feat(raft-sim-conformance): bag-only Drop/Duplicate/Reorder ops (task 2.2) --- .kiro/specs/raft-sim-conformance/tasks.md | 2 +- src/lib/raft/raft.h | 24 ++++ src/lib/raft/raft_sim.c | 50 +++++++ src/lib/testsuite/test_raft.c | 163 ++++++++++++++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/raft-sim-conformance/tasks.md b/.kiro/specs/raft-sim-conformance/tasks.md index cd0861a..2ba5bd5 100644 --- a/.kiro/specs/raft-sim-conformance/tasks.md +++ b/.kiro/specs/raft-sim-conformance/tasks.md @@ -45,7 +45,7 @@ - _Boundary: raft_sim.c, test_raft.c_ - _Depends: 1.2_ -- [ ] 2.2 RED+GREEN: Drop / Duplicate / Reorder mutate only the bag +- [x] 2.2 RED+GREEN: Drop / Duplicate / Reorder mutate only the bag - Test: `raft_sim_bag_drop`/`_duplicate` change bag multiplicity without mutating any node's term/role/log; reorder delivers in a PRNG-chosen order - Implement the three bag operations (mirroring `raft.tla:443`/`448` `UNCHANGED` of non-message vars) - Observable completion: node snapshots are identical before/after a drop or duplicate of an undelivered message diff --git a/src/lib/raft/raft.h b/src/lib/raft/raft.h index f7477ca..465e634 100644 --- a/src/lib/raft/raft.h +++ b/src/lib/raft/raft.h @@ -695,6 +695,30 @@ int raft_sim_bag_push(raft_sim_cluster_t *c, const raft_sim_msg_t *msg); */ int raft_sim_bag_deliver(raft_sim_cluster_t *c, int i); +/* + * raft_sim_bag_drop: DropMessage (raft.tla:448). Discard the bag member at index + * `i` (swap-with-last removal, bag_count -= 1) without mutating ANY node — mirrors + * the TLA+ UNCHANGED of all non-message vars. Returns 0 on a discarded member, -1 + * (fail loud, bag untouched) on an out-of-range index. + */ +int raft_sim_bag_drop(raft_sim_cluster_t *c, int i); + +/* + * raft_sim_bag_duplicate: DuplicateMessage (raft.tla:443). Re-enqueue a copy of the + * bag member at index `i` onto the tail (multiplicity += 1) without mutating ANY + * node. Honors RAFT_SIM_MAX_MSGS (fail loud on overflow). Returns -1 on an + * out-of-range index, otherwise the push status (0 on success, -1 on bag overflow). + */ +int raft_sim_bag_duplicate(raft_sim_cluster_t *c, int i); + +/* + * raft_sim_bag_pick: Reorder source. Return a PRNG-chosen in-flight bag index in + * [0, bag_count) so delivery order is seed-deterministic (replayable) rather than + * FIFO. Pure read: advances only c->prng, never any node state. Returns -1 when the + * bag is empty. The caller delivers the chosen index via raft_sim_bag_deliver. + */ +int raft_sim_bag_pick(raft_sim_cluster_t *c); + /* * raft_sim_dump_log: print trace logs for all nodes to fp. * If RAFT_SIM_VERBOSE env var is set, always prints. diff --git a/src/lib/raft/raft_sim.c b/src/lib/raft/raft_sim.c index 59cf819..163653d 100644 --- a/src/lib/raft/raft_sim.c +++ b/src/lib/raft/raft_sim.c @@ -698,6 +698,56 @@ int raft_sim_bag_deliver(raft_sim_cluster_t *c, int i) return 0; /* unreachable — enum is exhaustive */ } +/* + * raft_sim_bag_drop: DropMessage (raft.tla:448). Discard the bag member at `i` + * (swap-with-last removal, bag_count -= 1) without touching ANY node. Mirrors the + * TLA+ UNCHANGED <>: this is a + * pure bag mutation. Bounds-checked; returns -1 (fail loud, bag untouched) on an + * out-of-range index, 0 on a discarded member. + */ +int raft_sim_bag_drop(raft_sim_cluster_t *c, int i) +{ + if (i < 0 || i >= c->bag_count) + return -1; /* fail loud — no silent no-op */ + + raft_sim_bag_remove_at(c, i); /* swap-with-last; decrements bag_count */ + return 0; +} + +/* + * raft_sim_bag_duplicate: DuplicateMessage (raft.tla:443). Re-enqueue a COPY of the + * bag member at `i` onto the tail (multiplicity += 1) without touching ANY node — + * mirrors the TLA+ UNCHANGED of all non-message vars. Honors RAFT_SIM_MAX_MSGS via + * raft_sim_bag_push (fail loud on overflow). The original at `i` is left intact. + * Returns -1 on an out-of-range index, the push status otherwise (0 on success, + * -1 on bag overflow). + */ +int raft_sim_bag_duplicate(raft_sim_cluster_t *c, int i) +{ + if (i < 0 || i >= c->bag_count) + return -1; /* fail loud — no silent no-op */ + + /* Copy by value FIRST: raft_sim_bag_push may grow the array, but the source is + * already an independent local, so a tail push can never alias the source. */ + raft_sim_msg_t copy = c->bag[i]; + return raft_sim_bag_push(c, ©); +} + +/* + * raft_sim_bag_pick: Reorder source. Return a PRNG-chosen in-flight bag index in + * [0, bag_count) so the scheduler delivers in a deterministic, seed-replayable + * order rather than FIFO. Pure read of the bag: advances only the cluster PRNG, never + * any node's (term, role, votedFor, log). Returns -1 when the bag is empty (nothing + * to deliver). The subsequent raft_sim_bag_deliver does the node-side work. + */ +int raft_sim_bag_pick(raft_sim_cluster_t *c) +{ + if (c->bag_count <= 0) + return -1; /* empty bag — nothing to pick */ + + return (int)(raft_sim_prng_next(c) % (uint64_t)c->bag_count); +} + void raft_sim_start_all_elections(raft_sim_cluster_t *c) { for (int i = 0; i < c->n; i++) diff --git a/src/lib/testsuite/test_raft.c b/src/lib/testsuite/test_raft.c index ee1b851..d4caad0 100644 --- a/src/lib/testsuite/test_raft.c +++ b/src/lib/testsuite/test_raft.c @@ -4644,6 +4644,165 @@ static void test_sim_conformance_bag_deliver_routes_rpcs(void) TEST_ASSERT_EQUAL_INT(votedfor2_before, c.nodes[2].state.voted_for); } +/* ----- task 2.2: Drop / Duplicate / Reorder mutate ONLY the bag ----- */ + +/* Observable per-node snapshot for the R2.5 "node state UNCHANGED" assertion. + * Captures exactly the non-message vars raft.tla:443/448 keep UNCHANGED across + * DuplicateMessage/DropMessage: serverVars (term, role, votedFor) + logVars + * (the log entries and length). memcmp-comparable (memset to zero first). */ +typedef struct { + uint64_t term; + raft_role_t role; + int voted_for; + int log_count; + uint64_t commit_index; + raft_sim_entry_t log[RAFT_SIM_MAX_LOG]; +} sim_node_state_snapshot_t; + +static void sim_snapshot_node(const raft_sim_node_t *node, + sim_node_state_snapshot_t *out) +{ + memset(out, 0, sizeof(*out)); + out->term = node->state.term; + out->role = node->state.role; + out->voted_for = node->state.voted_for; + out->log_count = node->log_count; + out->commit_index = node->commit_index; + for (int i = 0; i < node->log_count && i < RAFT_SIM_MAX_LOG; i++) + out->log[i] = node->log[i]; +} + +/* + * test_sim_2_2_drop_duplicate_preserve_node_state (task 2.2; Req 2.5) + * + * R2.5 / raft.tla:443/448: DropMessage and DuplicateMessage change bag + * multiplicity ONLY — every node's (term, role, votedFor, log) is byte-identical + * before and after. The bag operations never touch any node: + * - raft_sim_bag_drop(c, i): bag_count -= 1, node snapshots identical. + * - raft_sim_bag_duplicate(c, j): bag_count += 1, the duplicate's content equals + * the original, node snapshots still identical. + * Bounds are fail-loud (return < 0) and leave the bag untouched. + */ +static void test_sim_2_2_drop_duplicate_preserve_node_state(void) +{ + raft_sim_cluster_t c; + memset(&c, 0, sizeof(c)); + + /* 3-node conformance cluster; give nodes some real, distinct state so the + * "UNCHANGED" assertion has teeth (non-trivial term/role/votedFor/log). */ + TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&c, 3, 2000, 0xD0D0ULL)); + raft_start_election(&c.nodes[0].state); /* node 0 -> candidate */ + for (int g = 0; g < 64 && c.bag_count > 0; g++) + raft_sim_bag_deliver(&c, 0); /* node 0 wins -> LEADER */ + TEST_ASSERT_EQUAL_INT(RAFT_LEADER, c.nodes[0].state.role); + uint64_t value = 0x1122334455667788ULL; + uint64_t out_index = 0; + TEST_ASSERT_EQUAL_INT(RAFT_OK, + raft_log_append(&c.nodes[0].state, &value, sizeof(value), &out_index)); + + /* Seed the bag directly with two UNDELIVERED messages (never routed, so no + * node was touched by them). */ + c.bag_count = 0; + raft_sim_msg_t m0; + memset(&m0, 0, sizeof(m0)); + m0.kind = RAFT_SIM_MSG_VOTE_REQ; + m0.src = 1; m0.dst = 2; m0.term = 7; + raft_sim_msg_t m1; + memset(&m1, 0, sizeof(m1)); + m1.kind = RAFT_SIM_MSG_AE_REQ; + m1.src = 0; m1.dst = 1; m1.term = 3; m1.entries_len = 1; + m1.entry_term = 3; m1.entry_value = 0xFEEDFACEULL; + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&c, &m0)); + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&c, &m1)); + TEST_ASSERT_EQUAL_INT(2, c.bag_count); + + /* Snapshot every node BEFORE the bag mutations. */ + sim_node_state_snapshot_t before[RAFT_SIM_MAX_NODES]; + for (int i = 0; i < c.n; i++) + sim_snapshot_node(&c.nodes[i], &before[i]); + + /* ---- DROP ---- discard the AE_REQ at index 1; multiplicity drops by 1. */ + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_drop(&c, 1)); + TEST_ASSERT_EQUAL_INT(1, c.bag_count); + for (int i = 0; i < c.n; i++) { + sim_node_state_snapshot_t after; + sim_snapshot_node(&c.nodes[i], &after); + TEST_ASSERT_EQUAL_MEMORY(&before[i], &after, sizeof(after)); + } + + /* ---- DUPLICATE ---- re-enqueue a copy of the surviving message (index 0); + * multiplicity rises by 1, the duplicate content equals the original. */ + raft_sim_msg_t orig = c.bag[0]; + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_duplicate(&c, 0)); + TEST_ASSERT_EQUAL_INT(2, c.bag_count); + TEST_ASSERT_EQUAL_MEMORY(&orig, &c.bag[0], sizeof(orig)); /* original intact */ + TEST_ASSERT_EQUAL_MEMORY(&orig, &c.bag[1], sizeof(orig)); /* copy at tail */ + for (int i = 0; i < c.n; i++) { + sim_node_state_snapshot_t after; + sim_snapshot_node(&c.nodes[i], &after); + TEST_ASSERT_EQUAL_MEMORY(&before[i], &after, sizeof(after)); + } + + /* ---- bounds: fail-loud, bag untouched ---- */ + int saved_count = c.bag_count; + TEST_ASSERT_TRUE(raft_sim_bag_drop(&c, -1) < 0); + TEST_ASSERT_TRUE(raft_sim_bag_drop(&c, c.bag_count) < 0); + TEST_ASSERT_TRUE(raft_sim_bag_duplicate(&c, -1) < 0); + TEST_ASSERT_TRUE(raft_sim_bag_duplicate(&c, c.bag_count) < 0); + TEST_ASSERT_EQUAL_INT(saved_count, c.bag_count); +} + +/* + * test_sim_2_2_reorder_prng_deterministic (task 2.2; Req 2.5) + * + * Reorder: raft_sim_bag_pick(c) returns a PRNG-chosen in-flight index so delivery + * order is PRNG-determined, not FIFO. Same seed => same pick sequence (replay), and + * picking is a pure read: it mutates no node's (term, role, votedFor, log). + */ +static void test_sim_2_2_reorder_prng_deterministic(void) +{ + raft_sim_cluster_t a, b; + memset(&a, 0, sizeof(a)); + memset(&b, 0, sizeof(b)); + + /* Two clusters, identical seed: pick sequences must match byte-for-byte. */ + TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&a, 3, 2000, 0xAB1EULL)); + TEST_ASSERT_EQUAL_INT(0, raft_sim_conformance_init(&b, 3, 2000, 0xAB1EULL)); + + /* Push >=2 distinct messages onto each (same content) so a pick can vary. */ + for (int k = 0; k < 4; k++) { + raft_sim_msg_t m; + memset(&m, 0, sizeof(m)); + m.kind = RAFT_SIM_MSG_VOTE_REQ; + m.src = 0; m.dst = 1; m.term = (uint64_t)(k + 1); + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&a, &m)); + TEST_ASSERT_EQUAL_INT(0, raft_sim_bag_push(&b, &m)); + } + TEST_ASSERT_EQUAL_INT(4, a.bag_count); + + /* Snapshot node state before any picks (picking must not mutate nodes). */ + sim_node_state_snapshot_t before[RAFT_SIM_MAX_NODES]; + for (int i = 0; i < a.n; i++) + sim_snapshot_node(&a.nodes[i], &before[i]); + + for (int step = 0; step < 16; step++) { + int pa = raft_sim_bag_pick(&a); + int pb = raft_sim_bag_pick(&b); + TEST_ASSERT_EQUAL_INT(pa, pb); /* deterministic replay */ + TEST_ASSERT_TRUE(pa >= 0 && pa < a.bag_count); /* names a live member */ + } + + for (int i = 0; i < a.n; i++) { + sim_node_state_snapshot_t after; + sim_snapshot_node(&a.nodes[i], &after); + TEST_ASSERT_EQUAL_MEMORY(&before[i], &after, sizeof(after)); + } + + /* Empty bag: pick returns < 0 (nothing to deliver). */ + a.bag_count = 0; + TEST_ASSERT_TRUE(raft_sim_bag_pick(&a) < 0); +} + /* ---------- Group A: Basic Election Correctness (task 5.1) ---------- */ /* @@ -7213,6 +7372,10 @@ void test_raft(void) /* conformance bag push/deliver routing (task 2.1) */ RUN_TEST(test_sim_conformance_bag_deliver_routes_rpcs); + /* bag-only Drop/Duplicate/Reorder mutate only the bag (task 2.2) */ + RUN_TEST(test_sim_2_2_drop_duplicate_preserve_node_state); + RUN_TEST(test_sim_2_2_reorder_prng_deterministic); + /* 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);