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.
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.
| Group | Scripts | What they drive |
|---|---|---|
raft/testdata/ | 28 | The RawNode Ready/Advance contract, elections, replication, snapshots, membership, read-only leases |
confchange/testdata/ | 9 | Simple and joint configuration changes through the Changer |
quorum/testdata/ | 4 | Vote 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.
| Harness | Drives | Scripts |
|---|---|---|
RaftTest | The RawNode Ready/Advance contract, a message queue, and the raft log | testdata/*.txt |
ConfChangeTest | The Changer — simple, enter-joint, leave-joint | confchange/testdata/*.txt |
run_quorum | vote_result and committed_index | quorum/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
- 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
- joint_autoleave
- joint_idempotency
- joint_learners_next
- joint_safety
- simple_idempotency
- simple_promote_demote
- simple_safety
- update
- zero
- 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.