~/moonraft▊

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.

  1. 01

    Add the package

    moonraft lives on mooncakes.io under moonbitstack; the source is moonbitstack/moonraft.

    # in your MoonBit project
    moon add moonbitstack/moonraft
  2. 02

    First cluster: elect a leader

    A Cluster is 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
  3. 03

    Replicate and read

    Propose a command and wait until every node has committed it. committed_agrees checks 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
  4. 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
  5. 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 the StateMachine trait, 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_delete and 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:

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