v0.8.0 — Alpha: first consumer, and pre-vote
Pre-releaseraft-io v0.8.0 — Alpha: first consumer, and pre-vote
The first real consumer, and the liveness fix it surfaced. v0.8.0 builds a
replicated key-value store on the core — end to end, with snapshots and faults —
and turns it into a property test. That test found a real disruption corner, and
the fix is pre-vote elections. Everything here is MINOR-compatible: no
existing signature, wire encoding, or WAL record changed.
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.8.0
Pre-vote elections — no disruption on rejoin
A node partitioned away from the cluster keeps timing out and, under plain Raft,
keeps incrementing its term. When it rejoins, that inflated term forces the
sitting leader to step down — even though the rejoining node is behind and cannot
win — and the cluster churns through elections before it re-settles. This is the
classic rejoin-disruption problem, and the standard remedy is pre-vote (Raft
thesis §9.6).
A node now runs a pre-vote round before a real election. It asks each peer
whether it would grant a vote at the next term — without incrementing its own
term or casting a vote — and a peer grants only if it has no active leader and
the candidate's log is at least as up to date as its own. The node campaigns for
real, finally advancing its term, only once a quorum of pre-votes agrees. A
partitioned node never collects that quorum, so its term never climbs; on rejoin
it slots back in behind the existing leader without disrupting it.
The probe rides on two new messages, Message::PreVote and
Message::PreVoteReply. They are additive #[non_exhaustive] enum variants —
every existing message keeps its exact wire encoding, so the freeze declared in
v0.7 holds. TimeoutNow (leadership transfer) still triggers an immediate real
election, bypassing pre-vote, because the leader has already vouched for the
target.
A real consumer: a replicated key-value store
examples/kv_store.rs is the library's first end-to-end consumer. The application
supplies a KvStore state machine and an in-memory transport and drives the node
with step; committed commands arrive as Action::Apply and are decoded and
applied, and the snapshot hooks serialize and restore the store. The demo elects a
leader, replicates a series of writes so every node converges, then adds a fourth
node that catches up entirely from a snapshot and ends with the identical state.
Application-level convergence as a property
tests/kv_consumer.rs lifts that consumer into a property test. Where the
protocol suites check that committed commands never diverge, this checks the
layer an application actually cares about: the materialized state machine. A
key-value store is driven through writes, partitions, and snapshotting; once the
cluster heals and settles, every node's map must equal a single-threaded model
built by applying the committed command sequence in order — including any node
whose state was rebuilt from a snapshot.
This is the test that surfaced the rejoin disruption: under heavy fault injection
it caught a cluster that would not re-converge promptly after a partition healed,
because a term-inflated node kept unseating the leader. Pre-vote fixes it; the
test now passes at elevated case counts (PROPTEST_CASES=4000).
A prelude
use raft_io::prelude::*; now brings in the everyday surface in one line —
RaftNode, RaftConfig, the Event/Action vocabulary, Error/Result, and
the RaftLog/RaftTransport seams with their in-memory implementations. The
message and other value types remain at the crate root for when you implement a
transport or inspect a LogEntry.
A note on reads
A leader serves application state from what it has applied. raft-io does not (yet)
implement read-index or lease-based linearizable reads, so a client that reads
from a node which has just lost leadership without knowing it could observe stale
state. For strongly-consistent reads today, route them through the log as
commands; first-class linearizable reads are a candidate for a later milestone.
Breaking changes
None. The pre-vote messages are additive enum variants, the prelude is new,
and the example and test are additions. 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: 107 unit + 22 integration / property tests
(safety.rs,replication.rs,recovery.rs,snapshot.rs,membership.rs,
hardening.rs,kv_consumer.rs) + 55 doctests.
The hardening suite was additionally run at PROPTEST_CASES=6000 and the
key-value consumer suite at PROPTEST_CASES=4000, both with no failures. loom
is not exercised: the core is a single-threaded, owned state machine with no
lock-free or shared-state path.
What's next
- v0.9.x — Beta → RC. Broaden consumer integration, capture final benchmarks,
and soak toward the 1.0 freeze. Candidate hardening items include linearizable
reads (read-index / leader lease) and witness/learner (non-voting) members.
Installation
[dependencies]
raft-io = "0.8"
# Optional features:
raft-io = { version = "0.8", features = ["persistence"] } # durable wal-db-backed log
raft-io = { version = "0.8", features = ["framing"] } # pack-io wire framingMSRV: Rust 1.85 (edition 2024).
Documentation
Full diff: v0.7.0...v0.8.0.
Changelog: CHANGELOG.md.