#ifdef XCE_TESTING #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; /* Capture the CANDIDATE's last-log tuple at SEND time from its own in-memory * log (the library holds no entry bytes): last_log_index == log_count, * last_log_term == term of the tail entry (0 when the log is empty). This * feeds the (lastLogTerm, lastLogIndex) freshness gate at delivery. */ rpc->last_log_index = (uint64_t)node->log_count; rpc->last_log_term = (node->log_count > 0) ? node->log[node->log_count - 1].term : 0; 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++; } /* ---------- log-replication callbacks (raft-log-replication) ---------- * * These back the four required log callbacks with the node's in-memory log. * ctx is raft_sim_node_t * (same convention as the wired election callbacks). * The library holds no entry bytes, so the simulator owns storage here: * - on_persist_entry: append/overwrite at 1-based index (term + bytes) * - on_get_entry_term: read the stored term (0 = absent) * - on_truncate_from: drop entries index..end * - on_send_append_entries: buffer one AE (sourcing the single entry from the * sender's own log at prev_log_index+1 when n_entries == 1) * No AppendEntries delivery happens here — that is raft_sim_deliver_append_entries * (task 8.2). These callbacks only do storage/term-read/truncate/buffer. */ /* * sim_persist_entry: durably append or overwrite the entry at 1-based `index`. * Sets the slot's term and copies up to RAFT_SIM_ENTRY_LEN bytes; extends * log_count when appending past the current tail. */ static int sim_persist_entry(void *ctx, uint64_t index, uint64_t term, const void *entry, size_t len) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; if (index < 1 || index > RAFT_SIM_MAX_LOG) return -1; /* out of fixed sim bounds */ raft_sim_entry_t *slot = &node->log[index - 1]; slot->term = term; size_t n = len; if (n > RAFT_SIM_ENTRY_LEN) n = RAFT_SIM_ENTRY_LEN; /* bounded copy */ if (entry && n > 0) memcpy(slot->bytes, entry, n); slot->len = n; if ((int)index > node->log_count) node->log_count = (int)index; return 0; } /* * sim_get_entry_term: return the stored term at 1-based `index`, or 0 if absent * (0-means-absent convention). */ static uint64_t sim_get_entry_term(void *ctx, uint64_t index) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; if (index < 1 || index > (uint64_t)node->log_count) return 0; return node->log[index - 1].term; } /* * sim_truncate_from: drop entries at `index`..end (clear term so they read as * absent) and shrink log_count. */ static int sim_truncate_from(void *ctx, uint64_t index) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; if (index < 1) return -1; for (uint64_t i = index; i <= (uint64_t)node->log_count; i++) { if (i <= RAFT_SIM_MAX_LOG) { raft_sim_entry_t *slot = &node->log[i - 1]; memset(slot, 0, sizeof(*slot)); /* term=0 => absent */ } } if (node->log_count >= (int)index) node->log_count = (int)index - 1; return 0; } /* * sim_send_append_entries: buffer one AppendEntries into the cluster AE queue. * For n_entries == 1 the single entry's index/term/bytes are sourced from the * sender's own in-memory log at prev_log_index + 1 (the library holds no bytes). */ static int sim_send_append_entries(void *ctx, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, uint64_t leader_commit, int n_entries) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; raft_sim_cluster_t *c = (raft_sim_cluster_t *)node->ctx; if (c->ae_pending_count >= RAFT_SIM_MAX_AE) return -1; /* queue full — drop */ raft_sim_ae_t *ae = &c->ae_pending[c->ae_pending_count++]; memset(ae, 0, sizeof(*ae)); ae->src = node->idx; ae->dst = peer_idx; ae->leader_term = node->state.term; /* capture SEND-time term (see raft.h) */ ae->prev_log_index = prev_log_index; ae->prev_log_term = prev_log_term; ae->leader_commit = leader_commit; ae->n_entries = n_entries; if (n_entries == 1) { uint64_t idx = prev_log_index + 1; ae->entry_index = idx; if (idx >= 1 && idx <= (uint64_t)node->log_count) { raft_sim_entry_t *slot = &node->log[idx - 1]; ae->entry_term = slot->term; ae->entry_len = slot->len; if (slot->len > 0) memcpy(ae->entry_bytes, slot->bytes, slot->len); } } return 0; } /* * sim_on_commit_advanced: record the library-reported commit_index and bump the * fire counter so tests can assert the strict-advance / fire-once semantics. The * library fires this ONLY on a strict commit_index advance (never on a held * index), so the count == number of strict advances. No durability here — the * sim has no commit barrier; this is the trigger mirror only. */ static void sim_on_commit_advanced(void *ctx, uint64_t commit_index) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; node->commit_index = commit_index; node->commit_advanced_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 */ } /* ---------- conformance harness adapter (raft-sim-conformance task 1.2) ---------- * * The conformance init wires a SEPARATE callback set alongside the legacy ones. * Log storage REUSES sim_persist_entry / sim_get_entry_term / sim_truncate_from * (they already back node->log[] = on_persist_entry / on_get_entry_term / * on_truncate_from). Only the commit + send callbacks differ: * - sim_conf_on_commit_advanced fills node->applied[] (the legacy variant does * not), so State Machine Safety can observe value divergence, * - sim_conf_send_vote_request / sim_conf_send_append_entries push a unified * raft_sim_msg_t onto cluster.bag[] (mirroring raft.tla `messages`) instead * of the legacy pending[] / ae_pending[] queues. * Bag DELIVERY/routing is task 2.1 — not implemented here. */ /* * raft_sim_prng_next: SplitMix64 step. Advances c->prng and returns a well-mixed * 64-bit value. Deterministic and reproducible (no libc rand()): seeded from * c->seed in raft_sim_conformance_init, this is the single source for every * later nondeterministic choice the scheduler makes (R1.2). */ uint64_t raft_sim_prng_next(raft_sim_cluster_t *c) { uint64_t z = (c->prng += 0x9E3779B97F4A7C15ULL); z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; return z ^ (z >> 31); } /* * sim_conf_decode_value: deterministic decode of a committed entry's payload into * the applied state-machine value. Interprets the first 8 bytes (little-endian, as * stored by memcpy on the same host) as a uint64_t; shorter payloads zero-extend. * This is the stable decode applied[] uses so State Machine Safety divergence (two * nodes applying different values at the same index) is observable. */ static uint64_t sim_conf_decode_value(const raft_sim_entry_t *slot) { uint64_t v = 0; size_t n = slot->len; if (n > sizeof(v)) n = sizeof(v); if (n > 0) memcpy(&v, slot->bytes, n); return v; } /* * sim_conf_on_commit_advanced: conformance commit callback. Records the * library-reported commit_index and applies each newly-committed entry's decoded * value into node->applied[] (for committed indexes prev_commit+1 .. commit_index). * applied[k-1] holds the value applied at 1-based committed index k. Also bumps the * fire counter (mirrors the legacy variant's count semantics). */ static void sim_conf_on_commit_advanced(void *ctx, uint64_t commit_index) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; uint64_t prev = node->commit_index; if (commit_index > prev) { for (uint64_t k = prev + 1; k <= commit_index; k++) { if (k >= 1 && k <= (uint64_t)node->log_count && k <= RAFT_SIM_MAX_LOG) node->applied[k - 1] = sim_conf_decode_value(&node->log[k - 1]); } } node->commit_index = commit_index; node->commit_advanced_count++; } /* * raft_sim_bag_push: append one message to the unified cluster bag. Fails loud * (returns -1) on overflow rather than silently truncating, so a bounds bug is * visible (design Error Handling: bag overflow fails loud). Returns 0 on success. */ int raft_sim_bag_push(raft_sim_cluster_t *c, const raft_sim_msg_t *msg) { if (c->bag_count >= RAFT_SIM_MAX_MSGS) return -1; /* fail loud — do not silently drop */ c->bag[c->bag_count++] = *msg; return 0; } /* * sim_conf_send_vote_request: conformance vote-request send. Builds a * RAFT_SIM_MSG_VOTE_REQ from the callback args and pushes it onto the bag. */ static int sim_conf_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; raft_sim_msg_t msg; memset(&msg, 0, sizeof(msg)); msg.kind = RAFT_SIM_MSG_VOTE_REQ; msg.src = node->idx; msg.dst = peer_idx; msg.term = term; /* Capture the CANDIDATE's last-log tuple at SEND time from its own in-memory log * (the library holds no entry bytes): last_log_index == log_count, last_log_term * == the tail entry's term (0 when empty). This feeds the (lastLogTerm, * lastLogIndex) freshness gate when raft_sim_bag_deliver routes the request — * exactly as the legacy sim_send_vote_request does for the pending[] path. */ msg.last_log_index = (uint64_t)node->log_count; msg.last_log_term = (node->log_count > 0) ? node->log[node->log_count - 1].term : 0; return raft_sim_bag_push(c, &msg); } /* * sim_conf_send_append_entries: conformance AppendEntries send. Builds a * RAFT_SIM_MSG_AE_REQ from the callback args and pushes it onto the bag. For * n_entries == 1 the single entry's (term, value) is sourced from the SENDER's own * in-memory log at prev_log_index + 1 (the library holds no entry bytes), decoded * via sim_conf_decode_value — exactly as the legacy sim_send_append_entries sources * bytes. */ static int sim_conf_send_append_entries(void *ctx, int peer_idx, uint64_t prev_log_index, uint64_t prev_log_term, uint64_t leader_commit, int n_entries) { raft_sim_node_t *node = (raft_sim_node_t *)ctx; raft_sim_cluster_t *c = (raft_sim_cluster_t *)node->ctx; raft_sim_msg_t msg; memset(&msg, 0, sizeof(msg)); msg.kind = RAFT_SIM_MSG_AE_REQ; msg.src = node->idx; msg.dst = peer_idx; msg.term = node->state.term; /* send-time leader term */ msg.prev_log_index = prev_log_index; msg.prev_log_term = prev_log_term; msg.leader_commit = leader_commit; msg.entries_len = n_entries; if (n_entries == 1) { uint64_t idx = prev_log_index + 1; if (idx >= 1 && idx <= (uint64_t)node->log_count) { raft_sim_entry_t *slot = &node->log[idx - 1]; msg.entry_term = slot->term; msg.entry_value = sim_conf_decode_value(slot); } } return raft_sim_bag_push(c, &msg); } /* ---------- 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; /* log-replication callbacks backed by the node's in-memory log */ node->cbs.on_persist_entry = sim_persist_entry; node->cbs.on_get_entry_term = sim_get_entry_term; node->cbs.on_send_append_entries = sim_send_append_entries; node->cbs.on_truncate_from = sim_truncate_from; node->cbs.on_commit_advanced = sim_on_commit_advanced; 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.initial_commit_index = 0; /* first boot — library never reads storage */ 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; } int raft_sim_conformance_init(raft_sim_cluster_t *c, int n_nodes, uint32_t election_timeout_ms, uint64_t seed) { memset(c, 0, sizeof(*c)); if (n_nodes < 1 || n_nodes > RAFT_SIM_MAX_NODES) return -1; c->n = n_nodes; c->seed = seed; c->prng = seed; /* deterministic PRNG state seeded from the run key */ for (int i = 0; i < n_nodes; i++) { raft_sim_node_t *node = &c->nodes[i]; memset(node, 0, sizeof(*node)); node->idx = i; node->partition = 0; node->ctx = c; /* points back to the cluster */ /* Conformance adapter callback set. Election/persist/timeout callbacks * reuse the existing sim implementations; the conformance variants differ * only for commit (fills applied[]) and the send paths (push to bag[]). */ node->cbs.on_send_vote_request = sim_conf_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; /* log storage reused from the legacy in-memory log callbacks */ node->cbs.on_persist_entry = sim_persist_entry; node->cbs.on_get_entry_term = sim_get_entry_term; node->cbs.on_send_append_entries = sim_conf_send_append_entries; node->cbs.on_truncate_from = sim_truncate_from; node->cbs.on_commit_advanced = sim_conf_on_commit_advanced; node->cbs.on_vote_eligible = NULL; /* all eligible */ node->cbs.on_trace = sim_on_trace; raft_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = n_nodes; cfg.self_idx = i; cfg.my_priority = i; /* priority = index */ cfg.election_timeout_ms = election_timeout_ms; cfg.initial_term = 0; cfg.initial_voted_for = -1; cfg.initial_commit_index = 0; cfg.cbs = &node->cbs; cfg.ctx = node; 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, passing the candidate's send-time last-log * tuple so dst evaluates the (lastLogTerm, lastLogIndex) freshness gate. */ uint64_t reply_term = 0; int granted = raft_on_vote_request(&c->nodes[dst].state, src, term, rpc->last_log_term, rpc->last_log_index, &reply_term); /* Route reply back to src */ raft_on_vote_reply(&c->nodes[src].state, dst, granted, reply_term); } } void raft_sim_deliver_append_entries(raft_sim_cluster_t *c) { /* Snapshot current count — handling may buffer more AEs (mirrors vote path) */ int count = c->ae_pending_count; c->ae_pending_count = 0; for (int i = 0; i < count; i++) { raft_sim_ae_t *ae = &c->ae_pending[i]; int src = ae->src; int dst = ae->dst; /* Drop if either endpoint is partitioned */ if (c->nodes[src].partition || c->nodes[dst].partition) continue; /* The single entry's bytes/term were captured into the AE at buffer * time (library holds no entry bytes); pass them through for n_entries==1. */ const void *entry = (ae->n_entries == 1) ? ae->entry_bytes : NULL; size_t entry_len = (ae->n_entries == 1) ? ae->entry_len : 0; uint64_t entry_term = (ae->n_entries == 1) ? ae->entry_term : 0; /* Deliver AppendEntries request to dst using the SEND-time leader term * captured into the AE buffer at sim_send_append_entries (NOT the live * source term). This mirrors the vote path's buffered rpc->term and keeps * reorder/churn delivery (9.3/9.4) honest about the sending epoch so the * follower's UpdateTerm/stale-drop decisions match the real protocol. */ uint64_t leader_term = ae->leader_term; raft_append_result_t result; memset(&result, 0, sizeof(result)); raft_on_append_entries_request(&c->nodes[dst].state, src, leader_term, ae->prev_log_index, ae->prev_log_term, ae->leader_commit, entry, entry_len, entry_term, ae->n_entries, &result); /* Route reply back to src */ raft_on_append_entries_reply(&c->nodes[src].state, dst, result.success, result.match_index, result.reply_term); } } /* * raft_sim_bag_remove_at: swap-with-last removal of bag member `i`. * * Bag removal model (consumed on delivery, shared with task 2.2 Drop/Duplicate/ * Reorder): the message at `i` is overwritten by the last member and bag_count is * decremented. This keeps the bag a compact dense array with no holes, so an index * chosen by the PRNG always names a live member. Delivery removes the request * FIRST, then pushes the reply, so the reply lands at a fresh tail slot and never * collides with the just-vacated index. */ static void raft_sim_bag_remove_at(raft_sim_cluster_t *c, int i) { /* Caller guarantees 0 <= i < bag_count. */ int last = c->bag_count - 1; if (i != last) c->bag[i] = c->bag[last]; c->bag_count = last; } int raft_sim_bag_deliver(raft_sim_cluster_t *c, int i) { if (i < 0 || i >= c->bag_count) return -1; /* out of range — nothing to deliver */ /* Snapshot the message by value before any removal/push reshuffles the bag. */ raft_sim_msg_t m = c->bag[i]; int src = m.src; int dst = m.dst; /* Honor partitions: a partitioned endpoint's message is not delivered, mirroring * a dropped network edge. Leave it in the bag (a later heal can deliver it). */ if (c->nodes[src].partition || c->nodes[dst].partition) return -2; /* Consume the request/response FIRST (swap-with-last), then push any reply so * indices stay sane and the reply occupies a fresh tail slot. */ raft_sim_bag_remove_at(c, i); switch (m.kind) { case RAFT_SIM_MSG_VOTE_REQ: { /* Route to dst's vote handler, passing the candidate's send-time last-log * tuple so dst evaluates the (lastLogTerm, lastLogIndex) freshness gate. */ uint64_t reply_term = 0; int granted = raft_on_vote_request(&c->nodes[dst].state, src, m.term, m.last_log_term, m.last_log_index, &reply_term); raft_sim_msg_t rsp; memset(&rsp, 0, sizeof(rsp)); rsp.kind = RAFT_SIM_MSG_VOTE_RSP; rsp.src = dst; /* voter replies */ rsp.dst = src; /* back to candidate */ rsp.term = reply_term; rsp.granted = granted; return raft_sim_bag_push(c, &rsp); } case RAFT_SIM_MSG_AE_REQ: { /* For n_entries == 1 the single entry's bytes come from the carried value * (the library holds none); re-encode the same little-endian uint64_t the * SEND callback decoded, so the receiver's sim_persist_entry / decode round- * trips it. Use the SEND-time leader term carried in the message (m.term), * NOT the live source term. */ unsigned char entry_bytes[sizeof(uint64_t)]; const void *entry = NULL; size_t entry_len = 0; uint64_t entry_term = 0; if (m.entries_len == 1) { memcpy(entry_bytes, &m.entry_value, sizeof(entry_bytes)); entry = entry_bytes; entry_len = sizeof(entry_bytes); entry_term = m.entry_term; } raft_append_result_t result; memset(&result, 0, sizeof(result)); raft_on_append_entries_request(&c->nodes[dst].state, src, m.term, m.prev_log_index, m.prev_log_term, m.leader_commit, entry, entry_len, entry_term, m.entries_len, &result); raft_sim_msg_t rsp; memset(&rsp, 0, sizeof(rsp)); rsp.kind = RAFT_SIM_MSG_AE_RSP; rsp.src = dst; /* follower replies */ rsp.dst = src; /* back to leader */ rsp.term = result.reply_term; rsp.success = result.success; rsp.match_index = result.match_index; return raft_sim_bag_push(c, &rsp); } case RAFT_SIM_MSG_VOTE_RSP: /* Responses are first-class bag members: route to the candidate's tally. */ raft_on_vote_reply(&c->nodes[dst].state, src, m.granted, m.term); return 0; case RAFT_SIM_MSG_AE_RSP: raft_on_append_entries_reply(&c->nodes[dst].state, src, m.success, m.match_index, m.term); return 0; } return 0; /* unreachable — enum is exhaustive */ } 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; } void raft_sim_advance_commit(raft_sim_cluster_t *c, int node_idx) { if (node_idx < 0 || node_idx >= c->n) return; /* Library decides whether the commit index advanced; on a strict advance the * node's on_commit_advanced (sim_on_commit_advanced) updates commit_index / * commit_advanced_count. The -EPERM off-leader return is intentionally * discarded — tests read the per-node mirror, not this status. */ (void)raft_advance_commit(&c->nodes[node_idx].state); } /* Two log entries are identical iff their term, length, and payload bytes * (over the recorded length) all match. */ static int sim_entry_identical(const raft_sim_entry_t *a, const raft_sim_entry_t *b) { if (a->term != b->term || a->len != b->len) return 0; return memcmp(a->bytes, b->bytes, a->len) == 0; } int raft_sim_check_log_matching(const raft_sim_cluster_t *c) { /* For every ordered-pair-free pair (a < b) and every index i present in * both logs: if the entries at i carry the same term, the prefixes 1..i * must be entry-identical. Pairs are symmetric, so a < b suffices. */ for (int a = 0; a < c->n; a++) { const raft_sim_node_t *na = &c->nodes[a]; for (int b = a + 1; b < c->n; b++) { const raft_sim_node_t *nb = &c->nodes[b]; int shared = na->log_count < nb->log_count ? na->log_count : nb->log_count; for (int i = 1; i <= shared; i++) { const raft_sim_entry_t *ea = &na->log[i - 1]; const raft_sim_entry_t *eb = &nb->log[i - 1]; /* Same (index, term) ⇒ prefixes through i must be identical. */ if (ea->term != eb->term) continue; for (int k = 1; k <= i; k++) { if (!sim_entry_identical(&na->log[k - 1], &nb->log[k - 1])) return 0; /* Log Matching violated */ } } } } return 1; /* holds */ } int raft_sim_restart_node(raft_sim_cluster_t *c, int node_idx) { if (node_idx < 0 || node_idx >= c->n) return -1; raft_sim_node_t *node = &c->nodes[node_idx]; /* Snapshot the DURABLE state that survives a process restart: the persisted * (term, voted_for) and the log length (the in-memory log[] models the WAL on * disk and is left untouched). Volatile consensus state is discarded by the * re-init below. */ uint64_t restored_term = node->state.term; int restored_voted_for = node->state.voted_for; uint64_t restored_log_len = (uint64_t)node->log_count; /* The committed LSN is durable too: on recovery the controller restores it * into initial_commit_index. Model that here so a restarted node retains its * committed position (never regresses). */ uint64_t restored_commit_index = node->state.commit_index; raft_config_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.n_peers = node->state.n_peers; cfg.self_idx = node->idx; cfg.my_priority = node->idx; cfg.election_timeout_ms = node->state.election_timeout_ms; cfg.initial_term = restored_term; cfg.initial_voted_for = restored_voted_for; cfg.initial_log_len = restored_log_len; cfg.initial_commit_index = restored_commit_index; cfg.cbs = &node->cbs; cfg.ctx = node; /* same ctx convention as raft_sim_init */ /* raft_init returns the node to FOLLOWER and clears votes/election/leader * progress while restoring (term, voted_for, log_len). The log[] array and * the partition flag are preserved. */ return raft_init(&node->state, &cfg); } #endif /* XCE_TESTING */