forked from lei.chen/myxce
808 lines
30 KiB
C
808 lines
30 KiB
C
#ifdef XCE_TESTING
|
|
|
|
#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;
|
|
/* 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 */
|