forked from lei.chen/myxce
feat(raft-sim-conformance): bag-only Drop/Duplicate/Reorder ops (task 2.2)
This commit is contained in:
parent
ae4495104a
commit
b450e60d13
4 changed files with 238 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <<serverVars, candidateVars, leaderVars, logVars>>: 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++)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue