~/moonraft▊

Porting notes

41 scripts,
replayed block by block

This library is a MoonBit port of etcd-io/raft pinned at commit 26647d5. The reference tree ships 41 scripted scenarios; they are archived verbatim, replayed through the same RawNode, Changer and quorum code the library ships, and their expected output is compared byte for byte. This page records how that works and what it took to converge.

scripts 41 raft cases 558 pinned at 26647d5

The corpus

Archived as they are, embedded at build time

The reference tree's datadriven scripts — the testdata/*.txt corpus — are copied without edits into scripts/testdata/etcd/, and scripts/gen_dd_scripts.py embeds them into src/core/dd_scripts_wbtest.mbt so the replay needs no file IO and runs on every target.

GroupScriptsWhat they drive
raft/testdata/28The RawNode Ready/Advance contract, elections, replication, snapshots, membership, read-only leases
confchange/testdata/9Simple and joint configuration changes through the Changer
quorum/testdata/4Vote and commit majorities, majority and joint

src/core/datadriven_wbtest.mbt is an in-repo interpreter for the script format: it parses each file into directive / input / expected blocks and drives the command sequence through the harness that owns it.

HarnessDrivesScripts
RaftTestThe RawNode Ready/Advance contract, a message queue, and the raft logtestdata/*.txt
ConfChangeTestThe Changer — simple, enter-joint, leave-jointconfchange/testdata/*.txt
run_quorumvote_result and committed_indexquorum/testdata/*.txt

The comparison

A byte comparison, not a shape check

The harness executes every command the reference understands and renders its output in the reference's own format — a port of util.go's DescribeReady / DescribeMessage / DescribeEntry family. A script's expected block is then compared as text, so whitespace, field order and formatting all count.

confchange and quorum — 13 scripts

Compared case by case; all 13 reproduce the reference exactly, asserted by datadriven: quorum and confchange scripts reproduce upstream exactly.

raft — 28 scripts, 558 cases

Scored block by block by datadriven: raft scripts reproduce upstream block by block: all 558 cases match. The scoreboard asserts the exact count, so a change that stops one script reproducing is a failure rather than a lower score.

Convergence

What closed the last of the gap

In the order each was found and fixed, each with a test that fails before it.

The durable log, and the append thread

raft-log prints the durable log, not the unstable tail, and the append thread persists what it was handed before the acknowledgement is delivered. That is what makes the ABA-race script's interleaving visible at all: a stale append from an older term still lands, and the storage acknowledgement then reports the ignored stabilisation instead of silently doing nothing.

A local compaction is not an installed snapshot

Both move the core's snapshot index, but only an install produces Ready.snapshot — so a compacted leader no longer hands its own application a snapshot of its own log.

A committed change applies before the durability responses

The caller applies CommittedEntries and then calls Advance. That ordering is what makes the leader probe a newly added peer with the pre-advance commit index.

The leader's own progress comes from the tracker

Not synthesised from the log tail, so a proposal does not count as replicated before its self-acknowledgement is durable.

A forwarded proposal keeps its unset 0/0

Only appendEntry stamps a term and an index on it, and a response from a peer the node no longer tracks is refused with raft: cannot step as peer not found.

The leave-joint entry is broadcast at once

The automatic leave-joint entry goes out in the same turn it is appended, matching what proposing a configuration change does.

The scripts

All 41, by group

raft/testdata — 28
  • async_storage_writes
  • async_storage_writes_append_aba_race
  • campaign
  • campaign_learner_must_vote
  • checkquorum
  • confchange_disable_validation
  • confchange_v1_add_single
  • confchange_v1_remove_leader
  • confchange_v1_remove_leader_stepdown
  • confchange_v2_add_double_auto
  • confchange_v2_add_double_implicit
  • confchange_v2_add_single_auto
  • confchange_v2_add_single_explicit
  • confchange_v2_replace_leader
  • confchange_v2_replace_leader_stepdown
  • forget_leader
  • forget_leader_prevote_checkquorum
  • forget_leader_read_only_lease_based
  • heartbeat_resp_recovers_from_probing
  • lagging_commit
  • prevote
  • prevote_checkquorum
  • probe_and_replicate
  • replicate_pause
  • single_node
  • slow_follower_after_compaction
  • snapshot_succeed_via_app_resp
  • snapshot_succeed_via_app_resp_behind
confchange/testdata — 9
  • joint_autoleave
  • joint_idempotency
  • joint_learners_next
  • joint_safety
  • simple_idempotency
  • simple_promote_demote
  • simple_safety
  • update
  • zero
quorum/testdata — 4
  • joint_commit
  • joint_vote
  • majority_commit
  • majority_vote

The other direction

Scripts replay behaviour; the ledger audits tests

The corpus above checks what the library does. The ledger checks something else: every Test* function in the reference tree, matched against the MoonBit test that carries it.