v0.10.0 — Beta: learner members
Pre-releaseraft-io v0.10.0 — Beta: learner members
Grow the cluster without holding your breath. v0.10.0 adds non-voting
learner members: a node you can add to the cluster that replicates the log and
catches up like a follower but counts toward no quorum until you promote it. So
adding a far-behind node — even one that has to pull a whole snapshot — never
shrinks your fault tolerance or stalls commits while it catches up. Everything
here is MINOR-compatible, and the on-disk encoding is byte-identical to v0.9 for
any cluster that uses no learners.
What is raft-io?
A from-scratch implementation of the Raft consensus algorithm, built as a clean,
embeddable library rather than a framework. The protocol core is a deterministic,
sans-I/O state machine: you feed a node events and it returns actions. Time,
networking, and storage are injected through trait seams, which is what makes the
core provable and reproducible from a seed. It is the consensus layer above
wal-db and the coordination substrate for Hive DB clustering.
What's new in 0.10.0
Learner (non-voting) members
Adding a fresh voter to a cluster has always carried a risk: the new member counts
toward the quorum the moment it joins, but it starts with an empty log. Until it
catches up, every commit and election needs a majority that now includes a node
with nothing in it — so a single other failure can wedge the cluster. The standard
remedy (Raft thesis §4.2.1) is to add the node as a learner first.
A learner:
- receives the replicated log —
AppendEntries, andInstallSnapshotwhen it
is far enough behind — and applies committed entries, exactly like a follower; - counts toward no quorum — not commit, not elections, not linearizable-read
confirmation — so the voter majority is unchanged while it catches up; - never campaigns — a node that finds itself listed as a learner follows
quietly and starts no elections, so it cannot disrupt the cluster.
The workflow:
use raft_io::Event;
// On the leader: add a far-behind node as a learner. The voter set — and the
// quorum — is unchanged, so availability is not affected while it catches up.
let _ = node.step(Event::AddLearner(4))?;
// ... the leader replicates the backlog (and a snapshot if needed) to node 4;
// you can watch its progress. Once it has caught up:
let _ = node.step(Event::PromoteLearner(4))?; // now a full voting member
# Ok::<(), raft_io::Error>(())Event::RemoveServer drops a learner just as it drops a voter, and the new
RaftNode::learners() accessor reports the current learner set (members()
continues to report only voters). Promotion and addition follow the same
one-change-at-a-time rule as every other membership change.
Byte-compatible encoding — the freeze holds
Learners are durable, replicated cluster state, so they ride in the same
configuration entries (and snapshot membership) as voters. The encoding extends
the existing format with a reserved sentinel (u64::MAX, never a valid node id)
that separates voters from learners — and only appears when learners are
present. A cluster that uses no learners produces configuration entries and
snapshots that are byte-for-byte identical to v0.9, so existing logs, snapshots,
and deployments are wholly unaffected and the v0.7 format freeze holds.
Verified end to end
A new tests/membership.rs scenario adds a learner to a running cluster, confirms
the voter set and quorum are unchanged, keeps committing proposals while the
learner catches up (proving availability is never stalled), then promotes the
caught-up learner and checks the whole cluster — now four voters — agrees. Unit
tests cover that a learner never forms a commit quorum, never campaigns, is
recovered from a snapshot's configuration, and that promotion and removal behave.
Breaking changes
None. Event::AddLearner / Event::PromoteLearner and RaftNode::learners()
are additive; Event::RemoveServer gains the ability to remove a learner but is
otherwise unchanged; and the configuration encoding is identical to v0.9 unless a
learner is actually present. Existing code, wire bytes, and WAL records are
unaffected.
Verification
Run on Windows x86_64, Rust stable; the same commands pass on Linux (WSL2 Ubuntu)
and via the CI matrix:
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check
cargo audit
cargo build --examples --all-features
cargo benchAll green. Test counts at this tag:
--all-features: 124 unit + 25 integration / property tests
(safety.rs,replication.rs,recovery.rs,snapshot.rs,membership.rs,
hardening.rs,kv_consumer.rs) + 60 doctests.
The hardening suite was additionally run at PROPTEST_CASES=6000 with no failures
— adding the learner split between voting and non-voting replicas disturbed no
safety property. loom is not exercised: the core is a single-threaded, owned
state machine with no lock-free or shared-state path.
What's next
- RC → 1.0. With the protocol now complete — election, replication,
persistence, snapshots, membership with learners, leadership transfer, and
linearizable reads — what remains is the run to 1.0: committed benchmark
baselines for the hot paths, an extended soak, a final pass over the docs and the
protocol spec, and the 1.0 freeze.
Installation
[dependencies]
raft-io = "0.10"
# Optional features:
raft-io = { version = "0.10", features = ["persistence"] } # durable wal-db-backed log
raft-io = { version = "0.10", features = ["framing"] } # pack-io wire framingMSRV: Rust 1.85 (edition 2024).
Documentation
Full diff: v0.9.0...v0.10.0.
Changelog: CHANGELOG.md.