v0.9.0 — Beta
Pre-releaseraft-io v0.9.0 — Beta: linearizable reads
Reads you can trust. v0.9.0 adds the one capability a database-grade Raft
still needed: linearizable reads via the ReadIndex protocol. A leader now
answers a read only after confirming it still leads a quorum and has applied
through the read point — so a read never returns stale state from a leader that
has silently lost its mandate. 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.9.0
Linearizable reads (the ReadIndex protocol)
Through v0.8 a leader could only serve a read from whatever it happened to have
applied, with no guarantee it was still the leader — a node deposed by a partition
it had not yet noticed could answer with stale state. That is exactly the caveat
the v0.8 notes flagged. v0.9 closes it.
Ask for a read with the new event, carrying a caller token:
use raft_io::{Action, Event};
// On the leader: request a linearizable read.
for action in node.step(Event::Read { id: 7 })? {
if let Action::ReadReady { id, index } = action {
// The state machine now reflects every command committed before this
// read was requested. Read it and answer the client holding `id`.
let _ = (id, index);
}
}
# Ok::<(), raft_io::Error>(())Under the hood this is the ReadIndex protocol (Raft thesis §6.4):
- The leader records its commit index as the read point.
- It confirms it is still the leader by exchanging a
ReadProberound with a
quorum — a deposed leader cannot complete this, so it never answers stale. - Once a quorum has acknowledged and the leader has applied through the read
point, it emitsAction::ReadReady { id, index }.
The read is served entirely off the existing log — no entry is appended — so
reads do not grow the log or compete with writes for replication. If leadership is
lost before confirmation the read is simply dropped (no action is emitted) and the
client retries against the new leader. On a single-node cluster the leader is its
own quorum, so a read is ready immediately.
The confirmation round rides on two new messages, Message::ReadProbe and
Message::ReadProbeReply. Like v0.8's pre-vote messages, they are additive
#[non_exhaustive] enum variants — every existing message keeps its exact wire
encoding, so the v0.7 freeze holds.
A no-op on election, for an accurate commit index
ReadIndex is only correct if the leader's commit index truly reflects everything
committed before it took over. A freshly elected leader that inherited an
uncommitted tail of earlier-term entries cannot prove those committed by replica
count alone (§5.4.2). So, following the thesis (§6.4 / §8), a new leader that took
over with such a tail now appends a no-op entry of its own term; committing it
carries the earlier entries over the commit line and pins down an accurate commit
index.
The no-op is represented as a configuration entry that re-asserts the current
membership — which means it is never surfaced to the application, exactly like a
real membership entry. One consequence, shared with membership entries and worth
stating plainly: applied indices increase strictly but are not contiguous. A
consumer keys its applied state by the index in each Action::Apply, never by
counting applies. (The bundled WalLog and the example state machines already do
this; the recovery test harness was updated to match.)
Verified end to end
tests/kv_consumer.rs now issues a linearizable read on the healed cluster after
its full partition/snapshot fault schedule and asserts the read is never stale —
its read index always covers everything committed before the request. The
deterministic suites additionally check that a read after a partition heal reflects
the majority's writes, and that a read on a quiesced cluster returns exactly the
committed model. examples/kv_store.rs demonstrates a linearizable read of a key.
Breaking changes
None. The read-probe messages and Event::Read / Action::ReadReady are
additive; the election no-op changes no public signature and is invisible to the
application. Existing code, wire bytes, and WAL records are unaffected. The one
behavioural note — non-contiguous applied indices — already held for any cluster
that used membership changes.
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: 112 unit + 24 integration / property tests
(safety.rs,replication.rs,recovery.rs,snapshot.rs,membership.rs,
hardening.rs,kv_consumer.rs) + 57 doctests.
The hardening suite was additionally run at PROPTEST_CASES=6000 and the
key-value consumer suite at PROPTEST_CASES=2000, both with no failures — the
election no-op did not disturb any 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
- v0.9.1+ → RC → 1.0. Bug fixes only, broader soak, and final committed
benchmarks toward the 1.0 freeze. A remaining candidate addition is witness /
learner (non-voting) members for safe catch-up before promotion.
Installation
[dependencies]
raft-io = "0.9"
# Optional features:
raft-io = { version = "0.9", features = ["persistence"] } # durable wal-db-backed log
raft-io = { version = "0.9", features = ["framing"] } # pack-io wire framingMSRV: Rust 1.85 (edition 2024).
Documentation
Full diff: v0.8.0...v0.9.0.
Changelog: CHANGELOG.md.