Quickstart
From zero to a
replicating cluster
Five steps, each one runnable on its own. Every snippet is copied from a program that compiles, and every line of output below is what that program actually prints - the simulator is seeded, so the same code always prints the same transcript.
-
01
Add the package
moonraft lives on mooncakes.io under
moonbitstack; the source is moonbitstack/moonraft.# in your MoonBit project moon add moonbitstack/moonraft
-
02
First cluster: elect a leader
A
Clusteris the batteries-included path: it wires the consensus core to a deterministic, single-seed network so you can drive a whole cluster from one thread. Bring up five nodes and let them elect a leader.// Bring up five nodes and let them elect a leader. let cluster = @raft.Cluster::new(["n1", "n2", "n3", "n4", "n5"], seed=7) let leader = cluster.run_until_leader(300).unwrap() println("elected \{leader} in term \{cluster.node(leader).term()}")
Output - verbatim
elected n3 in term 1
-
03
Replicate and read
Propose a command and wait until every node has committed it.
committed_agreeschecks that all five logs share the same prefix up to the commit index - the one guarantee a replicated log exists to provide.// Propose a command; wait until every node has committed it. let ids = ["a", "b", "c"] let cluster = @raft.Cluster::new(ids, seed=11) let _ = cluster.run_until_leader(200) let _ = cluster.propose(b"set x = 1") let _ = cluster.run_until_committed(2, 300) println("all nodes committed through index 2: \{cluster.committed_agrees()}") for id in ids { println(" \{id}: commit_index = \{cluster.node(id).commit_index()}") }
Output - verbatim
all nodes committed through index 2: true a: commit_index = 2 b: commit_index = 2 c: commit_index = 2
-
04
Survive a leader crash
Crash the leader and let a survivor take over. The two invariants that decide correctness under failure still hold: at most one leader per term, and every committed prefix agreed across the cluster.
// Crash the leader; a survivor takes over, and safety must still hold. let ids = ["a", "b", "c", "d", "e"] let cluster = @raft.Cluster::new(ids, seed=5) let first = cluster.run_until_leader(200).unwrap() let _ = cluster.propose(b"cmd-1") let _ = cluster.run_until_committed(2, 200) cluster.crash(first) let second = cluster.run_until_leader(400).unwrap() println("crashed \{first}; new leader \{second}") println("one leader per term : \{cluster.one_leader_per_term()}") println("committed agree : \{cluster.committed_agrees()}")
Output - verbatim
crashed a; new leader b one leader per term : true committed agree : true
-
05
Plug in your state machine
Raft only orders bytes; meaning comes from your state machine, which
applys each committed entry in log order. Implement theStateMachinetrait, propose encoded commands, and fold every replica's committed log into a fresh store - the results must be identical.// A replicated key-value store on top of the consensus log. struct KV { data : Map[String, String] } fn KV::new() -> KV { { data: Map([]) } } fn KV::get(self : KV, key : String) -> String? { self.data.get(key) } impl @raft.StateMachine for KV with fn apply(self, cmd : Bytes) { let s = cmd.to_unchecked_string() if s.has_prefix("s") { let body = s[1:].to_owned() let mut key = ""; let mut value = ""; let mut eq = false for c in body { if !eq && c == '=' { eq = true } else if eq { value = value + "\{c}" } else { key = key + "\{c}" } } self.data[key] = value } else if s.has_prefix("d") { self.data.remove(s[1:].to_owned()) } } // Encode a command as the UTF-16LE bytes apply() reads back. fn to_wire(s : String) -> Bytes { let arr : Array[Byte] = [] for c in s { let code = c.to_int() arr.push((code & 0xff).to_byte()) arr.push(((code >> 8) & 0xff).to_byte()) } Bytes::from_array(arr) } fn kv_set(k : String, v : String) -> Bytes { to_wire("s" + k + "=" + v) } fn main { let ids = ["a", "b", "c"] let cluster = @raft.Cluster::new(ids, seed=9) let _ = cluster.run_until_leader(200) let _ = cluster.propose(kv_set("service", "moonraft")) let _ = cluster.run_until_committed(2, 300) for id in ids { let kv = KV::new() cluster.node(id).node().apply_committed(kv) println(" \{id}: service = \{kv.get("service").unwrap()}") } }
Output - verbatim
a: service = moonraft b: service = moonraft c: service = moonraft
The full program - with
kv_deleteand a self-check that every replica holds the same commands - is src/examples/04-kvstore.
Where to next
Run it for real
The Cluster simulator is the fastest way to learn the
protocol, but a deployment speaks Raft over a real transport. That is
the RawNode Ready / Advance loop: take a
batch of work (entries to persist, messages to send, committed entries
to apply), do it, then advance. Browse every public type
and method in the API reference, watch the
invariants hold in the live demo, or read the
source package by
package.
The src/examples/
directory is a guided tour - each program shows one idea through either
the deterministic Cluster simulator or the message-driven
RawNode, so you can pick the layer you care about:
- 00-helloworld — a minimal system: elect a leader
- 01-replicate — propose commands, confirm every node commits
- 02-fault-tolerance — crash, partition, drop packets; safety holds
- 03-operations — progress, leadership transfer, compaction
- 04-kvstore — a complete replicated key-value store on a StateMachine
- 05-membership — add & remove voters at runtime
- 06-learner — a non-voting learner, then promote it to a voter
- 07-checkquorum — check-quorum: a leader steps down when it loses its majority
- 08-snapshot — compact the log; a restarted node catches up
- 09-rawnode — the RawNode tick → Ready → Advance loop, by hand
- 10-storage — persist Raft state to MemoryStorage and recover it
- 11-readindex — a linearizable (ReadOnlySafe) read, confirmed by quorum
Local
Run the browser demo locally
The interactive demo builds from the same consensus core. Compile to
WebAssembly, drop the artifact next to the page, and serve
docs/ over http - an ES-module / fetch of
.wasm needs http(s), not file://.
moon build --target wasm --release src/examples/browser
cp _build/wasm/release/build/examples/browser/browser.wasm docs/moonraft.wasm
python3 -m http.server 8099 --directory docs # open http://localhost:8099/demo.html