Brief
You are writing the node software for a small distributed system whose network is actively hostile: messages are delayed, duplicated, and dropped, and the test harness deliberately partitions the cluster while checking your answers for consistency. The harness is Maelstrom (Jepsen's workbench). You climb a ladder of workloads — each one a distributed-systems pattern — and the top rung is a linearizable key-value store backed by your own Raft implementation.
Instructions
1. The Maelstrom environment
Your node is any executable. Maelstrom launches N copies of it and speaks JSON to each:
- Transport — one JSON object per line on stdin (messages in) and stdout (messages out). Anything for humans goes to stderr; a stray print to stdout corrupts the protocol.
- Message shape —
{"src": "<node-id>", "dest": "<node-id>", "body": {…}}. Every body has atype; requests carry a uniquemsg_id; replies echo it back asin_reply_toand use the<type>_okreply type (errors use typeerrorwith a numericcodeandtext). - Init — the first message every node receives is
{"type": "init", "node_id": "n1", "node_ids": ["n1","n2",…]}. Store both, replyinit_ok. Node ids start withn; client ids withc. - Inter-node RPC — nodes message each other with the same envelope. There is no delivery guarantee: build your own retry (with timeout) and make handlers idempotent, because retries mean duplicates.
- Maelstrom services — the harness also exposes key-value stores as pseudo-nodes
you can RPC (
lin-kv,seq-kv) withread/write/casoperations; rung 4 uses one.
Run a workload as
maelstrom test -w <workload> --bin <your-binary> --node-count <n> --time-limit <s> [--rate <ops/s>] [--nemesis partition]
and pass when it prints "Everything looks good!".
2. The ladder — one contract per rung
- Echo — handle
{"type":"echo","echo":<any>}→ replyecho_okwith the sameechofield. This rung exists only to prove your stdin/stdout/reply plumbing. - Unique IDs — handle
{"type":"generate"}→generate_okwith anidthat is globally unique across all nodes for the whole run, without any inter-node communication. The workload runs with partitions and demands total availability — derive ids from what a node already knows (its node id + a local counter, or timestamp + node id + sequence). Ids may be any JSON value. - Broadcast — three sub-stages against the
broadcastworkload:- Handle
{"type":"topology","topology":{…}}(a suggested neighbor map — you may use it or ignore it) →topology_ok;{"type":"broadcast","message":<int>}→broadcast_ok;{"type":"read"}→read_okwithmessages: every value this node has ever seen, in any order, duplicates ignored. Gossip each new value to other nodes so every node eventually reads every value. - Survive partitions (
--nemesis partition): retry unacknowledged gossip until it lands; dedupe on receipt. Values injected during a partition must appear everywhere once it heals. - Tune for efficiency on 25 nodes: first ≤ 30 messages per operation, then ≤ 20, while keeping median propagation latency within the workload's stated bounds (below ~1 s median / 2 s max, then ~1 s / 2 s with the lower message budget) — batch, and trade topology fan-out against hops.
- Handle
- Grow-only counter — workload
g-counter, partitions on. Handle{"type":"add","delta":<non-negative int>}→add_okand{"type":"read"}→read_okwith the counter's value. The counter is eventually consistent: reads may lag while partitioned but must converge to the true sum once healed. Build it on theseq-kvservice (cas-loop per node key, sum on read) or as CRDT-style per-node counters gossiped and merged with max. - Kafka-style log — workload
kafka: replicated append-only logs.{"type":"send","key":<log>,"msg":<int>}→send_okwith the assigned integeroffset; offsets within one key are monotonically increasing and never reused.{"type":"poll","offsets":{<key>:<from>}}→poll_okwithmsgs: for each key,[[offset, msg], …]from the requested offset on, in order, no committed gaps.{"type":"commit_offsets","offsets":{…}}→commit_offsets_ok;{"type":"list_committed_offsets","keys":[…]}→ the last committed offset per key. Start on the linearizablelin-kvservice, then multi-node with sane performance. - Totally-available transactions — workload
txn-rw-register. Handle{"type":"txn","txn":[["r",<key>,null],["w",<key>,<value>],…]}→txn_okwith the same list, reads filled in with the value seen (or null). The system must stay totally available under partition — never block, never refuse — which caps you at weak isolation: first pass Read Uncommitted (no dirty writes: the checker rejects two transactions' writes interleaving per key), then Read Committed (never expose a value from an uncommitted or aborted transaction — replicate whole transactions atomically).
3. The Raft assignment
Rung 7: make workload lin-kv pass — a linearizable key-value store
(read / write / cas operations from clients, where cas compares the current
value with from and sets to only on match, erroring on mismatch or missing key) —
with partitions enabled, by implementing Raft yourself. Requirements, not the paper:
- State — every node persists (in memory here)
currentTerm,votedFor, and alogof{term, command}entries (1-indexed); volatilecommitIndexandlastApplied; leaders track per-followernextIndexandmatchIndex. - Leader election — nodes start as followers. A follower that hears nothing from a valid leader within a randomized election timeout (spread the range wide enough that split votes are rare; hundreds of ms against Maelstrom's latencies) increments its term, votes for itself, and requests votes from all peers. A vote is granted at most once per term, and only to a candidate whose log is at least as up-to-date (higher last term wins; equal terms → longer log wins). A majority of votes makes a leader; a message bearing a higher term makes anyone step down to follower and adopt that term.
- Log replication — clients' operations go to the leader (non-leaders reply with a
"not the leader" error or proxy). The leader appends to its own log and sends
append-entries RPCs carrying the previous entry's index and term; a follower rejects
the RPC if its log disagrees at that position, and the leader backs
nextIndexdown and retries until logs match, then overwrites the follower's conflicting suffix. Heartbeats are empty append-entries on a timer well under the election timeout. - Commit — the leader advances
commitIndexto the highest index replicated on a majority whose entry is from the leader's current term; committed entries are applied to the KV state machine in log order, exactly once, and only then answered to the client. - Safety under partition — at most one leader per term; a deposed leader's
unreplicated entries are discarded, never applied; committed entries survive any
minority partition and leader change; two nodes never apply different commands at the
same log index.
cascorrectness is the observable proof: it only ever succeeds against the true committed value.
Constraints
- Any language; only a JSON library is assumed. Do not use a Raft/consensus library — the consensus code is the exercise. Maelstrom's provided language demos may be read for plumbing, not copied for logic.
- Never write non-protocol output to stdout.
- Handlers must tolerate duplicate delivery at every rung from 3 up.
- Rungs are completed in order; each must pass before the next starts.
Acceptance
One check per exercise milestone, all via the Maelstrom CLI:
- Echo —
maelstrom test -w echo --bin <node> --node-count 1 --time-limit 10passes. - Unique IDs —
-w unique-ids --node-count 3 --rate 1000 --time-limit 30 --availability total --nemesis partitionpasses: zero duplicate ids under partition. - Broadcast —
-w broadcast --node-count 5 --time-limit 20 --rate 10passes; then the same with--nemesis partition; then--node-count 25 --rate 100with reportedmsgs-per-op≤ 30, and finally ≤ 20, latencies within the stated bounds. - Grow-only counter —
-w g-counter --node-count 3 --rate 100 --time-limit 20 --nemesis partitionpasses: final reads equal the sum of all acknowledged deltas. - Kafka-style log —
-w kafka --node-count 2 --concurrency 2n --time-limit 20 --rate 1000passes: no lost writes, per-key order preserved, committed offsets honored. - Transactions —
-w txn-rw-register --node-count 2 --concurrency 2n --rate 1000 --time-limit 20 --availability total --nemesis partitionpasses, first with--consistency-models read-uncommitted, thenread-committed. - Raft —
-w lin-kv --node-count 3 --concurrency 2n --rate 100 --time-limit 60 --nemesis partitionpasses: Maelstrom's linearizability checker finds no violation; kills/partitions during the run produce re-elections, not lost acknowledged writes.
Related
- Implement Raft with Gossip Glomers — the exercise this is the subject of.
- Gossip Glomers — Fly.io's challenge series this statement is adapted from.
- Maelstrom protocol & workload docs — the authoritative message and workload contracts restated above.
- The Raft paper — the protocol §3 states as requirements.