README

Lfan-ke/raft-moonbit/core does not have a README file

#
Logger

pub trait Logger {
fn log(Self, LogLevel, String) -> Unit
}

A pluggable sink for the consensus core's diagnostics (etcd's raft.Logger). Where etcd's Go interface exposes twelve printf-style methods (a Debug/ Debugf pair per level), a MoonBit caller formats the message with string interpolation at the call site, so a single log(level, message) carries every level and the LogLevel enum supplies the level distinction. Keeping it a trait decouples the core from any particular logging backend, and the no-op default (NopLogger) keeps a server that wants no logging allocation-free.

#
StateMachine

pub(open) trait StateMachine {
fn apply(Self, Bytes) -> Unit
}

A replicated state machine: it consumes committed commands in log order. Implementors decide what each command means. Keeping this pluggable lets the same consensus core drive a key-value store, a config registry or anything else.

#
Tracer

pub trait Tracer {
fn on_event(Self, TraceEvent) -> Unit
}

A pluggable observer of the core's state transitions (etcd's TraceLogger). A single dispatch method mirrors etcd's one-event-at-a-time trace stream; wrappers below name the individual events. The default NopTracer discards everything, so an untraced server pays nothing.

#
Transport

Pluggable transport for delivering Raft RPCs to a peer, addressed by node id. A real cluster implements this over TCP, HTTP or gRPC; an in-process cluster can call the handlers directly. Keeping it a trait decouples the consensus core from any particular networking stack.

#
ConfigError

pub suberror ConfigError {
EmptyId
HeartbeatTickNotPositive
ElectionTickNotGreater
MaxInflightNotPositive
MaxInflightBytesTooSmall
LeaseBasedNeedsCheckQuorum
} derive(Eq)

Why a configuration was rejected — one variant per etcd Config.validate branch that applies to this port. Raised by Config::validate / RaftNode::from_config (etcd returns an error from the same branches).

#
Cluster

pub struct Cluster {
nodes : Map[String, RaftNode]
ids : Array[String]
inflight : Array[InFlight]
now : Int
part : Map[String, Int]
down : Map[String, Bool]
drop_permil : Int
max_delay : Int
next_group : Int
rng : UInt64
}

A deterministic, discrete-time cluster simulator. It owns a set of RaftNodes and a network that can be told to drop, delay, reorder and partition traffic, then advanced tick by tick. Because every random choice comes from a single seeded PRNG, a whole run — elections, replication, failures and all — replays identically, which is what makes it useful for finding consensus bugs and pinning them down (the deterministic-simulation approach the task recommends).

#
Cluster::all_committed

fn Cluster::all_committed(self : Cluster, index : UInt64) -> Bool

Whether every running node has committed up through index.

#
Cluster::committed_agrees

fn Cluster::committed_agrees(self : Cluster) -> Bool

State Machine Safety (Raft §5.4.3): if any two servers have committed an entry at a given index, it is the same entry. Checked over the common committed prefix by comparing the term at every index — a divergence there would mean two different commands were committed at the same slot.

#
Cluster::compact_leader

fn Cluster::compact_leader(self : Cluster, upto : UInt64, data : Bytes) -> Bool

Compact the current leader's log up to upto, standing the discarded prefix in for a snapshot with payload data. A lagging follower that later needs an entry from the discarded prefix will be caught up by InstallSnapshot. Returns whether a leader performed the compaction.

#
Cluster::crash

fn Cluster::crash(self : Cluster, id : String) -> Unit

Stop a node: it no longer ticks and all traffic to or from it is dropped, modelling a crash. Its state is retained, so restart brings it back as it would return after reloading from stable storage.

#
Cluster::enable_check_quorum

fn Cluster::enable_check_quorum(self : Cluster) -> Unit

Enable check-quorum (and lease reads) on every server.

#
Cluster::heal

fn Cluster::heal(self : Cluster) -> Unit

Heal all partitions: every node shares one network again.

#
Cluster::invariants_hold

fn Cluster::invariants_hold(self : Cluster) -> Bool

Whether every stated invariant holds right now. A scenario asserts this after each interesting step.

#
Cluster::is_down

fn Cluster::is_down(self : Cluster, id : String) -> Bool

Whether id is currently stopped.

#
Cluster::isolate

fn Cluster::isolate(self : Cluster, id : String) -> Unit

Cut one node off from every other node.

#
Cluster::leader

fn Cluster::leader(self : Cluster) -> String?

The id of a current leader, if exactly the usual single one is running.

#
Cluster::leaders

fn Cluster::leaders(self : Cluster) -> Array[String]

The ids of every server that currently believes it is leader.

#
Cluster::logs_consistent

fn Cluster::logs_consistent(self : Cluster) -> Bool

Log Matching (Raft §5.3): wherever two running logs both hold an entry at some index with the same term, every preceding entry matches too. Checked pairwise against the first running node as a reference over the indices both physically retain (past any snapshot baseline).

#
Cluster::new

fn Cluster::new(ids : Array[String], seed? : UInt64) -> Cluster

Build a cluster of the given server ids. Every node knows every other as a peer and starts in one network partition (fully connected). seed fixes the network's PRNG; per-node election jitter is seeded from each id so a run is fully reproducible.

#
Cluster::node

fn Cluster::node(self : Cluster, id : String) -> RaftNode

The server with the given id.

#
Cluster::one_leader_per_term

fn Cluster::one_leader_per_term(self : Cluster) -> Bool

Election Safety (Raft §5.2): no two servers ever believe they are leader in the same term. Different terms are fine — that is normal succession. This is the invariant a partition-and-heal scenario must never break.

#
Cluster::partition

fn Cluster::partition(self : Cluster, group_a : Array[String], group_b : Array[String]) -> Unit

Split the cluster so that the two id groups cannot exchange messages. Nodes inside a group still reach each other; nodes not listed keep their group.

#
Cluster::propose

fn Cluster::propose(self : Cluster, command : Bytes) -> Bool

Propose a command on the current leader, if there is one. Returns whether a leader accepted it.

#
Cluster::propose_conf

fn Cluster::propose_conf(self : Cluster, change :
ConfChange
) -> Bool

Propose a configuration change on the current leader. Returns whether a leader accepted it.

#
Cluster::propose_on

fn Cluster::propose_on(self : Cluster, id : String, command : Bytes) -> Bool

Propose a command on a specific server. Useful when several nodes believe they lead — for example a partitioned old leader alongside a fresh one — and the test wants the proposal to go to a chosen side. Returns whether that node accepted it as leader.

#
Cluster::restart

fn Cluster::restart(self : Cluster, id : String) -> Unit

Bring a stopped node back.

#
Cluster::run

fn Cluster::run(self : Cluster, ticks : Int) -> Unit

Advance the cluster by ticks ticks.

#
Cluster::run_until_committed

fn Cluster::run_until_committed(self : Cluster, index : UInt64, max_ticks : Int) -> Bool

Tick until every running node has committed at least index, or max_ticks elapse. Returns whether the target was reached.

#
Cluster::run_until_leader

fn Cluster::run_until_leader(self : Cluster, max_ticks : Int) -> String?

Tick until a leader emerges or max_ticks elapse; returns the leader id.

#
Cluster::same_committed_commands

fn Cluster::same_committed_commands(self : Cluster) -> Bool

The strongest agreement check: every live node holds byte-for-byte the same command at every committed index past the highest snapshot baseline. Where committed_agrees only compares terms, this compares the actual replicated commands, so a run that commits distinct values proves they land in the same order everywhere — a stand-in for linearizability of the committed prefix.

#
Cluster::set_delay

fn Cluster::set_delay(self : Cluster, max_delay : Int) -> Unit

Set the maximum extra delivery delay, in ticks. Any value above zero also reorders traffic, since messages sent together can arrive apart.

#
Cluster::set_drop

fn Cluster::set_drop(self : Cluster, permil : Int) -> Unit

Set the per-message drop probability, in parts per thousand.

#
Cluster::tick

fn Cluster::tick(self : Cluster) -> Unit

Advance the whole cluster by one tick: every running node ticks (which may start elections or emit heartbeats), then all due messages are delivered.

#
Cluster::transfer_leadership

fn Cluster::transfer_leadership(self : Cluster, target : String) -> Bool

Ask the current leader to transfer leadership to target. Returns whether a leader started the transfer.

#
ConfDriver

pub struct ConfDriver {
config :
Membership

applied : UInt64
}

A configuration driver: it walks a node's committed log and folds every committed ConfChange entry into a Membership, tracking how far it has applied so it never applies a change twice. This is the "configuration state machine" that runs beside the application state machine.

#
ConfDriver::config

The configuration this driver has folded up to.

#
ConfDriver::drive

fn ConfDriver::drive(self : ConfDriver, node : Node) -> Int

Fold every committed-but-unapplied ConfChange entry of node into the configuration, in log order, advancing the applied marker. Normal entries are skipped. Returns how many changes were applied in this pass.

#
ConfDriver::new

fn ConfDriver::new(members : Array[String]) -> ConfDriver

Create a configuration driver starting from an initial voter set.

#
Config

pub struct Config {
id : String
peers : Array[String]
election_tick : Int
heartbeat_tick : Int
max_msg_bytes : UInt64
max_uncommitted_size : UInt64
max_inflight : Int
max_inflight_bytes : UInt64
check_quorum : Bool
pre_vote : Bool
read_only_option : ReadOnlyOption
step_down_on_removal : Bool
disable_proposal_forwarding : Bool
disable_conf_change_validation : Bool
applied : UInt64
logger : &Logger
tracer : &Tracer
seed : UInt64
}

The parameters to start a server, collected into one explicit value (etcd's raft.Config). It gathers what RaftNode::new otherwise takes as a dozen loose optional arguments, so a caller can build, inspect and validate a configuration before constructing the node. RaftNode::new remains as a convenience constructor for callers that just want defaults; from_config is the validated path.

#
Config::new

fn Config::new(id : String, peers : Array[String], election_tick? : Int, heartbeat_tick? : Int, max_msg_bytes? : UInt64, max_uncommitted_size? : UInt64, max_inflight? : Int, max_inflight_bytes? : UInt64, check_quorum? : Bool, pre_vote? : Bool, read_only_option? : ReadOnlyOption, step_down_on_removal? : Bool, disable_proposal_forwarding? : Bool, disable_conf_change_validation? : Bool, applied? : UInt64, logger? : &Logger, tracer? : &Tracer, seed? : UInt64) -> Config

Build a Config for server id with the other voters peers, taking etcd's defaults for everything unset. The result is not validated; call validate or go through RaftNode::from_config.

#
Config::validate

fn Config::validate(self : Config) -> Unit raise ConfigError

Reject an unusable configuration, mirroring etcd's Config.validate branch for branch. The etcd checks that do not apply to this port (a nil Storage, a local-message-target id, MaxCommittedSizePerReady which lives on the async-storage path) are noted in GAP_core.md rather than enforced here.

#
DemoReport

pub struct DemoReport {
seed : UInt64
node_count : Int
first_leader : String?
proposal_accepted : Bool
committed : Bool
second_leader : String?
one_leader_per_term : Bool
committed_agrees : Bool
invariants_hold : Bool
}

The outcome of the introductory demonstration cmd/example runs: a leader is elected, one command is replicated, the leader is crashed, and a survivor takes over. Kept as data rather than printed inline so the very run a reader watches is the run a test asserts against.

#
FullStatus

pub(all) struct FullStatus {
basic : RaftStatus
progress : Array[ProgressStatus]
config :
ConfState

}

The full status of the server (etcd's Status): the basic status, a snapshot of every tracked follower's replication progress — including the leader's own entry, fully caught up — and the current configuration as a ConfState.

#
InFlight

type InFlight

One message in flight across the simulated network: the tick it is due to be delivered, a monotonic sequence number for deterministic tie-breaking, and the message itself.

#
LogLevel

pub enum LogLevel {
Debug
Info
Warning
Error
Fatal
Panic
} derive(Eq)

A severity level, ordered from most to least verbose. Mirrors the five levels etcd's Logger exposes; Panic additionally aborts.

#
Node

pub struct Node {
id : String
role : Role
current_term : UInt64
voted_for : String?
log : Array[
Entry
]
commit_index : UInt64
last_applied : UInt64
snapshot_index : UInt64
snapshot_term : UInt64
}

A single Raft node: its identity, current role, and the state it holds.

current_term, voted_for and log make up the state a real deployment must persist to stable storage before replying to any RPC, so the node recovers correctly after a crash. commit_index and last_applied are volatile: they are safe to lose and are rebuilt after a restart.

#
Node::advance_commit

fn Node::advance_commit(self : Node, index : UInt64) -> Unit

Advance the commit index once a higher index is known to be committed. Never moves backwards.

#
Node::append

fn Node::append(self : Node, term : UInt64, command : Bytes) ->
Entry

Append a command to the log under term, returning the newly stored entry. The new entry takes the next sequential index.

#
Node::append_conf

fn Node::append_conf(self : Node, term : UInt64, command : Bytes) ->
Entry

Append a configuration-change command to the log under term. Membership changes travel through the log like any other entry so that every server adopts them at the same point in the sequence (Raft §6).

#
Node::apply_committed

fn Node::apply_committed(self : Node, sm : &StateMachine) -> Unit

Apply every committed-but-unapplied entry to sm in index order and advance last_applied (Raft §5.3). This is how a committed log turns into application state; calling it again once caught up does nothing.

#
Node::apply_hard_state

fn Node::apply_hard_state(self : Node, hs :
HardState
) -> Unit

Adopt a HardState read back from storage. The commit index never moves backwards: a stale HardState replayed after a snapshot must not un-commit entries the snapshot already covers.

#
Node::become_candidate

fn Node::become_candidate(self : Node) -> Unit

Start a new election: advance the term and vote for self.

#
Node::become_follower

fn Node::become_follower(self : Node, term : UInt64) -> Unit

Step down to follower and adopt a newly observed, higher term, clearing any vote cast in the old term.

#
Node::become_leader

fn Node::become_leader(self : Node) -> Unit

Take leadership after winning a majority of votes in the current term.

#
Node::become_pre_candidate

fn Node::become_pre_candidate(self : Node) -> Unit

Enter the pre-vote probe phase (etcd's becomePreCandidate): change the role but NOT the term or vote — a pre-candidate solicits votes under a hypothetical next term it has not adopted.

#
Node::compact

fn Node::compact(self : Node, upto : UInt64, data : Bytes, conf_state? :
ConfState
) ->
Snapshot

Compact the log by discarding every entry at or before upto, keeping that index's term as the new snapshot baseline. data is the serialized state machine those entries produced. Only committed entries past the current baseline may be compacted; otherwise the log is left untouched.

#
Node::current_term

fn Node::current_term(self : Node) -> UInt64

The node's current term.

#
Node::entries_after

fn Node::entries_after(self : Node, index : UInt64) -> Array[
Entry
]

Every entry strictly after absolute index, in order. The leader uses this to assemble the payload of an AppendEntries that repairs a lagging follower.

#
Node::entries_after_limited

fn Node::entries_after_limited(self : Node, index : UInt64, max : UInt64) -> Array[
Entry
]

At most max entries strictly after absolute index, in order. A leader caps the size of an AppendEntries with this so a badly lagging follower is caught up over several bounded messages instead of one huge one.

#
Node::entry_at

fn Node::entry_at(self : Node, index : UInt64) ->
Entry
?

Fetch the entry at absolute index, or None when it is at or before the snapshot baseline or past the end of the log.

#
Node::find_conflict_by_term

fn Node::find_conflict_by_term(self : Node, index : UInt64, term : UInt64) -> (UInt64, UInt64)

A best guess at where our log stops matching another log whose only known point is (index, term) (Raft §5.3, findConflictByTerm). Returns the greatest i <= index whose term is <= term (or is unknown because that index is compacted), together with that term. Both the follower (building a reject hint) and the leader (jumping back on that hint) use it, so a whole run of mismatched terms is skipped in one retry instead of one index at a time.

#
Node::handle_append_entries

Handle an AppendEntries RPC (Raft §5.3). This performs the log-matching consistency check, stores the entries while truncating any conflicting suffix, and advances the commit index. An empty entries acts as the leader's heartbeat.

#
Node::handle_install_snapshot

Handle an InstallSnapshot RPC (Raft §7). A stale leader is rejected. A current-or-newer leader makes this node adopt the term, step down, and — if the snapshot is newer than what it already holds — replace its log with the snapshot baseline. A snapshot that is not newer is ignored, so a delayed duplicate cannot roll the state machine backwards.

#
Node::handle_request_vote

Handle a RequestVote RPC (Raft §5.2, §5.4.1). A larger term makes this node step down first. The vote is granted only when this node has not yet voted for a different candidate in the term and the candidate's log is at least as up-to-date as its own.

#
Node::hard_state

Capture this node's HardState for durable storage.

#
Node::install_snapshot

fn Node::install_snapshot(self : Node, snapshot :
Snapshot
) -> Unit

Install a snapshot sent by the leader, discarding the whole log in favour of the snapshot baseline. Used when a follower has fallen so far behind that the leader has already compacted the entries it would otherwise need (§7).

#
Node::last_log_index

fn Node::last_log_index(self : Node) -> UInt64

The index of the last entry in the log, or the snapshot baseline when the log is empty (0 on a fresh node).

#
Node::last_log_term

fn Node::last_log_term(self : Node) -> UInt64

The term of the last entry in the log, or the snapshot baseline term when the log is empty (0 on a fresh node).

#
Node::load_from

fn Node::load_from(self : Node, storage : &
RaftStorage
) -> Unit

Rebuild a node from any RaftStorage: adopt the snapshot baseline, the HardState, and every stored entry, in that order. This is the storage-backed crash-recovery path. A snapshot the backend has not finished preparing, or a range that has been compacted out from under us, is tolerated rather than fatal, matching etcd's error contract on the read path.

#
Node::log_len

fn Node::log_len(self : Node) -> Int

The number of entries physically held in memory (those after the snapshot baseline). Useful for compaction bookkeeping and tests.

#
Node::mark_applied

fn Node::mark_applied(self : Node, index : UInt64) -> Unit

Record that the state machine has applied entries up through index.

#
Node::new

fn Node::new(id : String) -> Node

Create a fresh node that starts as a follower at term 0 with an empty log.

#
Node::persisted

Capture this node's persistent state so it can be written to a LogStore. The log is copied, so the returned value is a stable point-in-time image.

#
Node::propose_conf_change

Append a configuration change to the leader's log as a ConfChange entry under term, returning the stored entry. The change takes effect on the cluster only once the entry is committed and applied.

#
Node::recover

fn Node::recover(self : Node, state :
Persisted
) -> Unit

Reload persistent state into this node after a restart, replacing whatever it currently holds.

#
Node::recover_from

fn Node::recover_from(self : Node, wal : &
WalStore
) -> Unit

Rebuild a node from a write-ahead log, the whole restart path in one call.

#
Node::replay

fn Node::replay(self : Node, records : Array[
WalRecord
]) -> Unit

Replay a write-ahead log into this node, rebuilding its durable state in record order: snapshots set the baseline, HardState records set term, vote and commit, and entry records rebuild the log tail. This is the crash- recovery path a node runs on restart before serving any request.

#
Node::request_vote_args

Build the RequestVote arguments this node would send while standing for election in its current term.

#
Node::role

fn Node::role(self : Node) -> Role

The node's current role.

#
Node::save_into

fn Node::save_into(self : Node, storage :
MemoryStorage
) -> Unit

Write a node's durable state into a MemoryStorage: the snapshot baseline (if any), the HardState, and every in-memory log entry. Together with load_from this is the persist half of a node's restart path expressed over the storage engine rather than the write-ahead log.

#
Node::save_to

fn Node::save_to(self : Node, wal : &
WalStore
) -> Unit

Persist a node's full state to a write-ahead log as a snapshot baseline (if any), the current HardState, and every in-memory log entry. This is the checkpoint a node writes so a later replay reconstructs it.

#
Node::term_at

fn Node::term_at(self : Node, index : UInt64) -> UInt64

The term of the entry at index, or 0 when index is 0 (the empty position before the first entry) or past the end of the log. Used by the log-matching consistency check.

#
NopLogger

pub struct NopLogger {
}

The default Logger: it discards every message (etcd defaults to a real stderr logger, but a deterministic simulation and its tests want silence). A deployment that wants diagnostics supplies its own Logger.

#
NopTracer

pub struct NopTracer {
}

The default Tracer: it discards every event (matching etcd's default !with_tla build, where every trace* call is an empty function).

#
ProgressStatus

pub(all) struct ProgressStatus {
id : String
match_index : UInt64
next_index : UInt64
state :
ProgressState

paused : Bool
pending_snapshot : UInt64
is_learner : Bool
} derive(Eq)

A read-only view of one follower's replication progress, surfaced for status and monitoring (etcd's tracker.Progress as exposed through Status and WithProgress). It is a copied value, so reading it never disturbs the running protocol.

#
RaftNode

pub struct RaftNode {
core : Node
id : String
peers : Array[String]
config :
Membership

progress : Map[String,
Progress
]
votes : Map[String, Bool]
pre_vote : Bool
check_quorum : Bool
in_pre_campaign : Bool
leader_id : String?
election_elapsed : Int
heartbeat_elapsed : Int
election_timeout : Int
heartbeat_timeout : Int
randomized_election_timeout : Int
max_msg_bytes : UInt64
max_uncommitted_size : UInt64
uncommitted_size : UInt64
max_inflight : Int
max_inflight_bytes : UInt64
rng : UInt64
conf_applied : UInt64
lead_transferee : String?
read_only : ReadOnly
pending_read_index : Array[(String, Bytes)]
no_forward : Bool
pending_conf_index : UInt64
no_conf_change_validation : Bool
step_down_on_removal : Bool
pending_farewell : Array[
Message
]
logger : &Logger
tracer : &Tracer
}

A message-driven Raft server: the consensus core (Node) wrapped with everything a real deployment needs to run it — the peer set and configuration, per-follower replication progress, election and heartbeat timers with randomized timeouts, pre-vote, and a deterministic PRNG so tests can replay a run exactly. It communicates only through Messages: tick and step return the messages to send, and a transport (real or simulated) delivers them. This mirrors etcd's raft.Node/Ready split of logic from I/O.

#
RaftNode::advance_applied

fn RaftNode::advance_applied(self : RaftNode, index : UInt64) -> Unit

Advance the applied watermark to index, releasing the uncommitted-tail bytes of the entries now applied (etcd releases the quota at apply time, on MsgStorageApplyResp, not at commit time). Deferring the release to apply is what bounds the un-applied tail even for a single-voter leader — which commits its own appends instantly, so a commit-time release could never form a bounded backlog. The application (or the RawNode/Advance layer) calls this once it has applied committed entries.

#
RaftNode::become_candidate

fn RaftNode::become_candidate(self : RaftNode) -> Unit

Transition to candidate (advancing the term and voting for self), also re-randomizing the election timeout.

#
RaftNode::become_follower

fn RaftNode::become_follower(self : RaftNode, term : UInt64) -> Unit

Transition to follower in term, re-randomizing the election timeout. Every state change routes through the timer reset (Raft §5.2), which is what makes the per-node timeout fresh on each transition and keeps split votes rare.

#
RaftNode::campaign

Start a campaign. With pre-vote enabled the node first runs a pre-vote round under a hypothetical next term without touching its own term or vote, so a partitioned node cannot force real elections and inflate terms (Raft §9.6, pre-vote). Winning the pre-vote — or pre-vote being disabled — starts the real election.

#
RaftNode::commit_index

fn RaftNode::commit_index(self : RaftNode) -> UInt64

The commit index of the underlying core.

#
RaftNode::conf_state

The current membership as a ConfState, to stamp into a snapshot so a follower restoring from it rebuilds the same voter/learner sets (§7).

#
RaftNode::disable_proposal_forwarding

fn RaftNode::disable_proposal_forwarding(self : RaftNode) -> Unit

Turn off proposal forwarding (etcd's DisableProposalForwarding): a follower will drop client proposals instead of forwarding them to the leader.

#
RaftNode::election_deadline

fn RaftNode::election_deadline(self : RaftNode) -> Int

The randomized election timeout this server is currently counting toward, in ticks. Exposed for tests that check the timeout stays within its band.

#
RaftNode::enable_check_quorum

fn RaftNode::enable_check_quorum(self : RaftNode) -> Unit

Turn on check-quorum and lease reads for this server.

#
RaftNode::enable_read_only_safe

fn RaftNode::enable_read_only_safe(self : RaftNode) -> Unit

Switch this server to the linearizable ReadOnlySafe read mode: a read is confirmed by a fresh heartbeat quorum rather than the election lease.

#
RaftNode::forget_leader

fn RaftNode::forget_leader(self : RaftNode) -> Unit

Forget the currently-recognised leader (etcd's MsgForgetLeader): a follower clears its leader so it may grant (pre)votes at once instead of waiting out a check-quorum lease, without moving its term or resetting its election timer. Ignored under lease-based reads; a no-op on a candidate or leader.

#
RaftNode::from_config

fn RaftNode::from_config(config : Config) -> RaftNode raise ConfigError

Build a server from a validated Config (etcd's newRaft, which panics on an invalid config; here the error is raised so the caller can handle it). This is the explicit, checked counterpart to RaftNode::new.

#
RaftNode::full_status

fn RaftNode::full_status(self : RaftNode) -> FullStatus

The full status of this server (etcd's Status): basic status + per-follower progress + the current configuration.

#
RaftNode::has_lease

fn RaftNode::has_lease(self : RaftNode) -> Bool

Whether this leader currently holds a valid lease — a quorum has confirmed it this window — and may therefore serve reads without a fresh round trip.

#
RaftNode::id

fn RaftNode::id(self : RaftNode) -> String

This server's id.

#
RaftNode::is_leader

fn RaftNode::is_leader(self : RaftNode) -> Bool

Whether this server currently believes it is the leader.

#
RaftNode::leader

fn RaftNode::leader(self : RaftNode) -> String?

The leader this server last heard from, if any.

#
RaftNode::new

fn RaftNode::new(id : String, peers : Array[String], seed? : UInt64, election_timeout? : Int, heartbeat_timeout? : Int, max_msg_bytes? : UInt64, max_uncommitted_size? : UInt64, max_inflight? : Int, max_inflight_bytes? : UInt64, check_quorum? : Bool, pre_vote? : Bool, step_down_on_removal? : Bool, disable_conf_change_validation? : Bool, read_only_option? : ReadOnlyOption, logger? : &Logger, tracer? : &Tracer) -> RaftNode

Create a server with id id, the other voters peers, and a deterministic seed. Election timeouts randomize in [election_timeout, 2*election_timeout), the spread that keeps split votes rare (Raft §5.2). Heartbeats go out every heartbeat_timeout ticks, which must be well below the election timeout.

#
RaftNode::node

fn RaftNode::node(self : RaftNode) -> Node

The underlying consensus core, for reading log/commit state in tests.

#
RaftNode::progress_of

fn RaftNode::progress_of(self : RaftNode, id : String) -> ProgressStatus?

One member's progress view, or None if it is not tracked. A leader also answers for itself (etcd tracks the leader in its own progress map).

#
RaftNode::progress_status

fn RaftNode::progress_status(self : RaftNode) -> Array[ProgressStatus]

A snapshot of every tracked follower's progress. On a leader this includes the leader's own entry (caught up to its last index); a non-leader tracks no progress and returns an empty view.

#
RaftNode::promotable

fn RaftNode::promotable(self : RaftNode) -> Bool

Whether this node may stand for election (etcd's promotable): it must be a voter of the current configuration. A learner or a node that has been removed is not promotable. (etcd also refuses while a snapshot is being applied; this port installs snapshots synchronously, so there is no such in-progress state.)

#
RaftNode::propose

fn RaftNode::propose(self : RaftNode, command : Bytes) -> Array[
Message
]

Append a client command and replicate it. On a leader the command is appended and the resulting AppendEntries returned; on a follower it is forwarded to the leader (unless forwarding is disabled or no leader is known); on a candidate it is dropped. This routes through the same Propose message path a remote proposal takes, so local and forwarded proposals behave identically.

#
RaftNode::propose_conf

Append a configuration change to the leader's log and replicate it, so every server folds the same membership change into its configuration at the same log position once it commits (Raft §6). A no-op on a non-leader.

#
RaftNode::propose_conf_v2

Append a batch configuration change (joint consensus, Raft §4.3) and replicate it. Entering joint with a non-empty batch moves the cluster to C(old,new); once committed, auto_leave has the leader append the matching leave automatically. A no-op on a non-leader or mid-transfer.

#
RaftNode::quorum_active

fn RaftNode::quorum_active(self : RaftNode) -> Bool

Whether a quorum of the configuration has been heard from within the current liveness window — the leader itself plus every follower whose progress is flagged active. This is what confirms the leader still commands the cluster, underpinning both check-quorum and lease reads.

#
RaftNode::raw

fn RaftNode::raw(self : RaftNode) -> RawNode

View this server through the Ready/Advance contract. The two are the same server; the RawNode only batches the core's output into the cycle a real deployment drives.

#
RaftNode::raw_async

fn RaftNode::raw_async(self : RaftNode) -> RawNode

View this server under AsyncStorageWrites (etcd's Config.AsyncStorageWrites): each Ready hands its entries and committed entries to local storage threads as StorageAppend / StorageApply directives, and the caller returns the paired responses through step_append_resp / step_apply_resp once the work is durable/applied. advance becomes a no-op; the responses drive the stable/applied cursors instead.

#
RaftNode::raw_async_sized

fn RaftNode::raw_async_sized(self : RaftNode, max_committed_size : UInt64) -> RawNode

As raw_async, but also paginating each StorageApply batch to max_committed_size bytes (etcd's AsyncStorageWrites + MaxCommittedSizePerReady).

#
RaftNode::raw_with_max_committed_size

fn RaftNode::raw_with_max_committed_size(self : RaftNode, max_committed_size : UInt64) -> RawNode

View this server through the Ready/Advance contract, paginating committed entries to max_committed_size bytes per Ready.

#
RaftNode::read_index

fn RaftNode::read_index(self : RaftNode) -> UInt64?

Serve a linearizable read (Raft §6.4). A follower cannot answer, so None is returned. A leader may answer only once it has committed an entry in its own term — the no-op appended on election, once committed, guarantees its commit index reflects the latest term — and only while a quorum has confirmed its leadership this window. The returned index is the commit index the caller must wait for the state machine to reach before replying to the client, which is what makes the read see every previously-acknowledged write.

#
RaftNode::read_only_option

fn RaftNode::read_only_option(self : RaftNode) -> ReadOnlyOption

The read-confirmation mode this server is currently using.

#
RaftNode::reduce_uncommitted_size

fn RaftNode::reduce_uncommitted_size(self : RaftNode, size : UInt64) -> Unit

Release size bytes from the uncommitted tally as entries commit or apply (etcd's reduceUncommittedSize). Never goes below zero.

#
RaftNode::report_snapshot

fn RaftNode::report_snapshot(self : RaftNode, id : String, reject : Bool) -> Unit

Report the outcome of a snapshot the leader shipped (etcd's ReportSnapshot / MsgSnapStatus). A failure discards the pending snapshot; either way the follower leaves the Snapshot state and is probed, paused until its next AppendEntries response confirms where it now stands.

#
RaftNode::report_unreachable

fn RaftNode::report_unreachable(self : RaftNode, id : String) -> Unit

Report that a follower is unreachable (etcd's ReportUnreachable). A streaming follower drops back to probing so the leader stops optimistically advancing into messages that are being lost.

#
RaftNode::request_read_index

fn RaftNode::request_read_index(self : RaftNode, context : Bytes) -> Array[
Message
]

Request a linearizable read (Raft §6.4). Returns the heartbeats to broadcast (in ReadOnlySafe mode) so a quorum can confirm the leader is current; the confirmed read index is later collected with take_read_states. A follower, or a leader that has not yet committed an entry in its own term, serves nothing. In ReadOnlyLeaseBased mode (the default) a read is confirmed at once when the lease is valid.

#
RaftNode::request_transfer_leader

fn RaftNode::request_transfer_leader(self : RaftNode, target : String) -> Array[
Message
]

Request that leadership move to target (etcd's TransferLeader). On a leader this begins the handoff; on a follower the request is forwarded to the leader, so a transfer may be initiated from any server. Routes through the same TransferLeader message path a forwarded request takes.

#
RaftNode::role

fn RaftNode::role(self : RaftNode) -> Role

The current role (etcd's SoftState.RaftState). During the pre-vote probe the core genuinely occupies the PreCandidate state — a first-class Raft state, not a derived flag — having changed role without adopting the hypothetical term.

#
RaftNode::set_pre_vote

fn RaftNode::set_pre_vote(self : RaftNode, on : Bool) -> Unit

Enable or disable the pre-vote probe at runtime (etcd sets r.preVote directly). A mixed-version rolling restart flips replicas one at a time, so a cluster momentarily runs some nodes with pre-vote and some without.

#
RaftNode::set_read_only_option

fn RaftNode::set_read_only_option(self : RaftNode, option : ReadOnlyOption) -> Unit

Select how this leader confirms linearizable reads (etcd's Config.ReadOnlyOption). Safe is etcd's default; this server defaults to LeaseBased for backward compatibility with existing callers, so a deployment that wants etcd's default must ask for Safe explicitly.

#
RaftNode::soft_state

fn RaftNode::soft_state(self : RaftNode) -> SoftState

This server's soft state — its known leader and current role (etcd's raft.softState). Reading it never disturbs the protocol.

#
RaftNode::status

fn RaftNode::status(self : RaftNode) -> RaftStatus

Capture this server's current status.

#
RaftNode::step

Process one incoming message and return the messages to send in reply. This is the single entry point the transport drives; together with tick it is the whole externally-visible behaviour of a server.

The term is classified first (Raft §5.1). A stale message is never allowed to reach a handler as if it were current: acting on a stale response is exactly what let a late VoteResp be miscounted into a false majority (double leader) or a late AppendResp inflate a follower's progress.

#
RaftNode::take_read_states

fn RaftNode::take_read_states(self : RaftNode) -> Array[ReadState]

Collect the linearizable reads confirmed since the last call. Each carries the commit index the state machine must have applied before the read is answered, so it observes every previously-acknowledged write.

#
RaftNode::term

fn RaftNode::term(self : RaftNode) -> UInt64

The current term.

#
RaftNode::tick

Advance the logical clock by one tick, returning the messages to send. A leader beats every heartbeat_timeout ticks; a follower or candidate that reaches its randomized election timeout starts a new campaign.

#
RaftNode::transfer_leadership

fn RaftNode::transfer_leadership(self : RaftNode, target : String) -> Array[
Message
]

Begin transferring leadership to target (Raft §3.10). If the target is already caught up it is sent a TimeoutNow so it campaigns immediately; otherwise it is first sent the entries it lacks and the caller retries the transfer once it has caught up. A no-op on a non-leader or an unknown target.

#
RaftNode::uncommitted_size

fn RaftNode::uncommitted_size(self : RaftNode) -> UInt64

The current size of the uncommitted log tail, in payload bytes.

#
RaftNode::voter_nodes

fn RaftNode::voter_nodes(self : RaftNode) -> Array[String]

The voters of the current configuration, sorted (etcd's VoterNodes), for status and diagnostics. Sorting makes the list deterministic regardless of the order members were added.

#
RaftNode::with_progress

fn RaftNode::with_progress(self : RaftNode, visit : (String, ProgressStatus) -> Unit) -> Unit

Visit every tracked follower's progress (etcd's WithProgress). The visitor must not retain the Progress beyond the call; it is handed a copied view.

#
RaftStatus

pub(all) struct RaftStatus {
id : String
role : Role
term : UInt64
leader : String?
commit : UInt64
last_index : UInt64
applied : UInt64
} derive(Eq)

A point-in-time snapshot of a server's observable state, for monitoring and tests. It is a plain value copied out of the node, so reading it never disturbs the running protocol.

#
RaftStatus::describe

fn RaftStatus::describe(self : RaftStatus) -> String

A one-line, human-readable summary, e.g. a leader term=3 leader=a commit=7.

#
RawNode

pub struct RawNode {
raft : RaftNode
log :
RaftLog

max_committed : UInt64
msgs : Array[
Message
]
read_states : Array[ReadState]
prev_soft : SoftState
prev_hard :
HardState

async_storage : Bool
reflected_snap : UInt64
}

The goroutine-free driver of a Raft server (etcd's RawNode). The core RaftNode produces its output — messages to send, entries appended, commits — as return values of tick/step/propose; RawNode batches that output into the synchronous Ready/Advance cycle a real deployment runs: take a Ready, persist its entries and hard state, send its messages, apply its committed entries, then call advance.

It needs no language-level async: a Ready is a plain value and advance a plain call. The unstable-vs-stable split that separates entries still to be written (entries) from committed entries ready to apply (committed_entries) is kept by a real RaftLog: its storage holds what the application has persisted, its unstable tail holds what has not.

#
RawNode::advance

fn RawNode::advance(self : RawNode, rd : Ready) -> Unit

Notify the driver that the last Ready has been handled (etcd's Advance): its entries were persisted (by store) and its committed entries applied, so move the applied cursor past them. The stable watermark already moved in store. A no-op under async writes (the responses drive the cursors).

#
RawNode::apply_conf_change

fn RawNode::apply_conf_change(self : RawNode, cc :
ConfChange
) -> Array[String]

Apply a committed configuration change to the local configuration and report the resulting voter set (etcd's ApplyConfChange).

#
RawNode::basic_status

fn RawNode::basic_status(self : RawNode) -> RaftStatus

This server's basic status (etcd's BasicStatus): id, term, vote, commit, leader and role, without the per-follower progress map.

#
RawNode::campaign

fn RawNode::campaign(self : RawNode) -> Unit

Start a campaign, buffering the vote (or pre-vote) requests it emits.

#
RawNode::conf_state

The current membership as a ConfState (etcd's ApplyConfChange return / ConfState()): the voters, learners, the outgoing half while joint, the staged-demotion learners_next, and whether an auto-leave is pending.

#
RawNode::forget_leader

fn RawNode::forget_leader(self : RawNode) -> Unit

Voluntarily forget the current leader so this node can start an election without waiting out the election timeout (etcd's ForgetLeader).

#
RawNode::full_status

fn RawNode::full_status(self : RawNode) -> FullStatus

This server's full status (etcd's Status): basic status plus the per-follower progress view and the current configuration.

#
RawNode::has_ready

fn RawNode::has_ready(self : RawNode) -> Bool

Whether any work is outstanding (etcd's HasReady): a soft- or hard-state change, buffered messages, entries to persist or apply, or pending read states. Lets a driver skip building a Ready when the node is idle.

#
RawNode::is_async

fn RawNode::is_async(self : RawNode) -> Bool

Whether this driver is in AsyncStorageWrites mode.

#
RawNode::new

fn RawNode::new(raft : RaftNode) -> RawNode

Build a driver over raft (etcd's NewRawNode). The previous soft and hard states are seeded from the server as it stands, so the first Ready reports a change only if one genuinely happens afterwards. The RaftLog is recovered from the node's durable state: whatever is already in the node's log (and its snapshot baseline) is loaded into stable storage, so only entries appended afterwards land in the unstable tail and need writing.

#
RawNode::new_sized

fn RawNode::new_sized(raft : RaftNode, max_committed_size : UInt64) -> RawNode

Build a driver that paginates each Ready's committed entries to at most max_committed_size bytes (etcd's MaxCommittedSizePerReady). A burst of commits is then handed to the application over several Readys instead of one unbounded batch.

#
RawNode::next_unstable_snapshot

fn RawNode::next_unstable_snapshot(self : RawNode) ->
Snapshot
?

The snapshot available to be applied but not yet handed to a Ready, if any (etcd's raftLog.nextUnstableSnapshot). Reconciles with the core first, so a snapshot the core installed since the last poll is reflected into the mirror.

#
RawNode::node

fn RawNode::node(self : RawNode) -> RaftNode

The underlying core server, for reading protocol state in tests and drivers.

#
RawNode::propose

fn RawNode::propose(self : RawNode, data : Bytes) -> Unit

Propose a client command. On a leader it is appended and the resulting AppendEntries are buffered; on a follower it is forwarded to the known leader (etcd's MsgProp forwarding), or dropped if forwarding is disabled or no leader is known; a candidate drops it.

#
RawNode::propose_conf

Propose a configuration change, appended and replicated like any entry so every server folds it in at the same log position once committed (§6).

#
RawNode::propose_conf_v2

Propose a batch (joint) configuration change, appended and replicated like any entry so every server folds the same change in at the same log position once it commits (etcd's ProposeConfChange with a ConfChangeV2).

#
RawNode::read_index

fn RawNode::read_index(self : RawNode, rctx : Bytes) -> Unit

Request a linearizable read (etcd's ReadIndex, which steps a MsgReadIndex). Under ReadOnlySafe the read is not answered from the leader's lease but confirmed by a fresh heartbeat quorum, so the confirming heartbeats are buffered for the next Ready; the read surfaces once a quorum acknowledges the position (drained from the core in sync_log). The synchronous lease accessor RaftNode::read_index was wrong here: it returns an index without emitting the confirmation round, so under ReadOnlySafe the read was never confirmed.

#
RawNode::ready

fn RawNode::ready(self : RawNode) -> Ready

Take the outstanding work and commit to handling it (etcd's Ready): the same batch as ready_without_accept, but the messages are now drained and the soft/hard baseline advanced, so the next Ready reports only new work. The returned batch must be handled and passed back to advance.

#
RawNode::ready_without_accept

fn RawNode::ready_without_accept(self : RawNode) -> Ready

Assemble a Ready without committing to handle it (etcd's readyWithoutAccept): a pure read that leaves the buffered messages, read states, and cursors untouched, so a caller may inspect pending work and decide not to consume it.

#
RawNode::report_snapshot

fn RawNode::report_snapshot(self : RawNode, id : String, reject : Bool) -> Unit

Report the outcome of a snapshot sent to id (etcd's ReportSnapshot): reject true means the follower could not apply it, so the leader retries.

#
RawNode::report_unreachable

fn RawNode::report_unreachable(self : RawNode, id : String) -> Unit

Report that a message to id could not be delivered (etcd's ReportUnreachable): the leader stops streaming to that follower until it responds again.

#
RawNode::stabilize

Drive the node to quiescence for a caller that owns message delivery: keep taking a Ready, treat its entries as persisted and its committed entries as applied via advance, and collect every outbound message, until no work remains. Returns the messages a transport would deliver. This is the plain, synchronous read of the Ready/Advance loop that single-node drivers and the browser demo run in place of a goroutine.

#
RawNode::status

fn RawNode::status(self : RawNode) -> RaftStatus

This server's status snapshot (etcd's Status/BasicStatus).

#
RawNode::step

Feed one received message to the core, buffering the replies it produces.

#
RawNode::step_append_resp

fn RawNode::step_append_resp(self : RawNode, resp : StorageAppendResp) -> Unit

Return a StorageAppend acknowledgement: the entries have been made durable. The confirmed unstable prefix moves into stable storage and the unstable tail is truncated — but only if the response is not stale: a response whose term is below the node's current term (a later term has taken over) is ignored, and the ABA guard in async_stabilize further requires the unstable log to still hold that (index, log_term).

#
RawNode::step_apply_resp

fn RawNode::step_apply_resp(self : RawNode, resp : StorageApplyResp) -> Unit

Return a StorageApply acknowledgement: the committed entries have been applied. Advance the applied cursor and release the applied entries' quota. Committed entries are term-independent, so there is no staleness check.

#
RawNode::store

fn RawNode::store(self : RawNode, rd : Ready) -> Unit

Persist a Ready's entries to stable storage (etcd's storage.Append(rd.Entries), which the caller runs before applying committed_entries). This is the first half of handling a synchronous Ready: the caller stores, then applies committed_entries, then calls advance. A no-op under async writes, where the append is instead driven by StorageAppend + step_append_resp.

#
RawNode::tick

fn RawNode::tick(self : RawNode) -> Unit

Advance the logical clock by one tick, buffering any messages the tick emits (a leader's heartbeats, or a follower's campaign) for the next Ready.

#
RawNode::transfer_leader

fn RawNode::transfer_leader(self : RawNode, target : String) -> Unit

Begin transferring leadership to target (etcd's TransferLeader).

#
RawNode::with_progress

fn RawNode::with_progress(self : RawNode, visit : (String, ProgressStatus) -> Unit) -> Unit

Visit each follower's progress (etcd's WithProgress).

#
ReadOnly

type ReadOnly

The read-index bookkeeping a leader keeps for linearizable reads (etcd's readOnly, current design). In ReadOnlySafe mode confirmation is by an internal position counter, not the caller's context: each heartbeat carries the position confirmed + len(unconfirmed), so a single quorum acknowledgement releases every currently-unconfirmed read at once. This is what etcd switched to (see its "use an internally defined context" note) and avoids the collision where two reads sharing a context would clobber each other's ack state. In ReadOnlyLeaseBased mode the leader trusts its election lease instead.

#
ReadOnlyOption

pub enum ReadOnlyOption {
Safe
LeaseBased
} derive(Eq)

How a linearizable read is confirmed (etcd's ReadOnlyOption).

Safe confirms every read with a fresh heartbeat quorum, so it holds even under unbounded clock drift — etcd's default and recommended setting. LeaseBased trusts the leader's election lease instead, saving the round trip at the cost of depending on bounded clock drift; etcd requires check-quorum to be on when it is selected.

#
ReadState

pub(all) struct ReadState {
index : UInt64
request_ctx : Bytes
} derive(Eq)

A linearizable-read ticket (etcd's ReadState): the commit index a read must wait for the state machine to reach, tagged with the caller's opaque context so a returned index can be matched back to the request that asked for it.

#
Ready

pub(all) struct Ready {
soft_state : SoftState?
hard_state :
HardState
?
read_states : Array[ReadState]
entries : Array[
Entry
]
snapshot :
Snapshot
?
committed_entries : Array[
Entry
]
messages : Array[
Message
]
must_sync : Bool
storage_append : StorageAppend?
storage_apply : StorageApply?
}

The batch of outstanding work the application must handle for one turn of the state machine (etcd's Ready). The contract is synchronous, not a coroutine: the caller persists entries/hard_state/snapshot, sends messages, applies committed_entries, serves read_states, then calls advance.

A field carries work only when there is some: soft_state and hard_state are Some only on change (expressed as Option, not a sentinel), and the arrays are empty when idle. must_sync says whether the persist must be a durable (fsync) write or may be lazily flushed.

#
Role

pub(all) enum Role {
Follower
PreCandidate
Candidate
Leader
} derive(Eq)

The role a node plays in the Raft protocol at a given moment.

A node starts as a Follower. On election timeout it becomes a Candidate and stands for election; if it wins a majority it becomes the Leader for that term.

#
SoftState

pub(all) struct SoftState {
lead : String?
state : Role
} derive(Eq)

The volatile part of a server's state, useful for logging and monitoring but never persisted (etcd's SoftState). lead is None when no leader is known. A Ready carries a SoftState only when it has changed, which is why the leader/role transition is the trigger rather than the current value.

#
SoftState::equal

fn SoftState::equal(self : SoftState, other : SoftState) -> Bool

Whether two soft states are equal (etcd's SoftState.equal). A change in either the known leader or the role is what makes a new Ready report one.

#
StorageAppend

A directive to the local append thread: write these unstable entries (and the hard state / snapshot, if present) to stable storage, then return resp (etcd's MsgStorageAppend).

#
StorageAppendResp

pub(all) struct StorageAppendResp {
index : UInt64
log_term : UInt64
term : UInt64
} derive(Eq)

The acknowledgement a storage thread sends back once a StorageAppend has been made durable (etcd's MsgStorageAppendResp). It attests the last log (index, log_term) written and the term the raft node held when the directive was issued — the two together defeat the ABA race: a response that arrives after the term has moved on, or after the unstable log at that index was overwritten, is ignored rather than mistaken for an ack of the new log.

#
StorageApply

pub(all) struct StorageApply {
entries : Array[
Entry
]
resp : StorageApplyResp
}

A directive to the local apply thread: apply these committed entries to the state machine, then return resp (etcd's MsgStorageApply).

#
StorageApplyResp

pub(all) struct StorageApplyResp {
entries : Array[
Entry
]
}

The acknowledgement a state machine sends back once a StorageApply batch has been applied (etcd's MsgStorageApplyResp). It carries the applied entries so the raft node can advance its applied cursor and release their quota; committed entries are term-independent, so no ABA guard is needed.

#
TraceEvent

pub enum TraceEvent {
InitState(String)
StateChange(String, Role, UInt64)
Commit(String, UInt64)
Replicate(String, Array[
Entry
])
SendMessage(
Message
)
ReceiveMessage(
Message
)
ConfChange(String,
ConfState
)
}

One observable transition of the consensus core, surfaced to an external tracer. These mirror the events etcd's state_trace.go records for its TLA+ conformance harness (traceBecome*, traceCommit, traceReplicate, trace{Send,Receive}Message, traceConfChangeEvent, traceInitState). etcd guards the machinery behind a with_tla build tag so a normal build compiles the no-op variant (state_trace_nop.go); the equivalent here is that the default NopTracer erases to nothing.

#
demo_report_lines

fn demo_report_lines(report : DemoReport) -> Array[String]

Render a [DemoReport] as the exact lines cmd/example prints, stopping at whichever step the run failed to reach.

Parameters:
  • report : the outcome to render.

Returns the human-readable transcript, one entry per line.

#
demo_run

fn demo_run(ids : Array[String], seed : UInt64, elect_ticks : Int, commit_ticks : Int, reelect_ticks : Int) -> DemoReport

Run the introductory simulation and record what happened at each step.

Parameters:
  • ids : the servers to simulate.
  • seed : fixes the deterministic run.
  • elect_ticks : tick budget for the first election.
  • commit_ticks : tick budget for committing the first command.
  • reelect_ticks : tick budget for the re-election after the crash.

Returns a DemoReport naming the elected leader, whether the command committed, the successor once the leader is crashed, and the safety invariants that must survive the succession.

#
must_sync

Whether a synchronous durable write is required before replying to any RPC (etcd's MustSync). The persistent state on every server — currentTerm, votedFor, and the log — must be flushed when new entries are appended or the term or vote changed; a bare commit-index bump may be flushed lazily (§5).

#
replicate

fn replicate(leader : Node, followers : Array[Node]) -> UInt64

Drive one replication round from leader to every follower and advance the commit index once a majority (the leader included) store the last entry. Returns the resulting commit index.

#
run_election

fn run_election(candidate : Node, peers : Array[Node]) -> Bool

Run one round of leader election synchronously: candidate advances its term and asks every peer for a vote, counting its own. It becomes leader on a majority. If a peer reveals a higher term the candidate steps down at once and the round fails. Returns whether the candidate won.

#
run_single_node

fn run_single_node(id : String, commands : Array[Bytes]) -> Array[Bytes]

Run a single-node Raft group through the Ready/Advance loop over a batch of client commands, returning the commands the state machine applied, in order.

This is the smallest complete embedding of a RawNode, and it is the live documentation of the Ready contract: for each command, propose it, then drain every Ready by running the three steps a real application runs, in order — persist the entries (store), apply the committed_entries, then advance. Because persistence happens before application, committed_entries may cover the same freshly appended entries as entries in one Ready, just as in etcd. It is the non-test entry point that wires RawNode into a runnable driver, and a demo or an embedder can call it directly.