v0.4.0 — Durable Log + Crash Recovery
Pre-releaseraft-io v0.4.0 — Durable Log + Crash Recovery
A cluster that survives restarts. v0.4.0 adds durability: under the new
persistence feature, a node backs its log with WalLog, a wal-db-backed
store whose entries and hard state (term, vote) are written before the node acts
on them and recovered on restart. A node killed at any point — mid-election,
mid-replication — comes back, recovers its log from disk, and rejoins without
violating safety. This is verified by a crash-recovery property test that
interleaves node restarts into an adversarial schedule. The in-memory path is
untouched: persistence is purely additive and off by default.
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 (logical ticks, inbound messages,
client proposals) and it returns actions (send these messages, apply this
committed command). 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.4.0
WalLog — a durable, crash-recoverable log
WalLog implements the same RaftLog trait as the in-memory store, so the
protocol core does not change — you just hand the node a durable log instead:
use raft_io::{RaftConfig, RaftNode, WalLog};
let log = WalLog::open("node-1.wal")?;
let mut node = RaftNode::with_log(RaftConfig::single(1), log);
# let _ = &mut node;
# Ok::<(), raft_io::Error>(())It is log-structured. Every mutation — an appended entry, a hard-state
update, a truncation — is encoded as a record and appended to a wal-db
write-ahead log, which frames and checksums each record. An in-memory index
mirrors the current state so reads stay fast. On open, the records are replayed
in order to rebuild that index exactly:
use raft_io::{LogEntry, RaftLog, WalLog};
let mut log = WalLog::open("node-1.wal")?;
log.append(&[LogEntry::new(1, 1, b"set x = 1".to_vec())])?;
log.sync()?; // durable from here
// A restart recovers everything that was written.
let recovered = WalLog::open("node-1.wal")?;
assert_eq!(recovered.last_index(), 1);
# Ok::<(), raft_io::Error>(())raft-io uses wal-db's byte-record API directly and frames its own records, so
it does not pull in wal-db's pack-io feature. Truncated entries remain
physically in the WAL until log compaction (snapshots, v0.5); replay still
reconstructs the correct logical state.
Persist before you respond
Raft's safety rests on current_term, voted_for, and the log being durable
before the node acts on them — a node that forgot it had voted could help elect
two leaders. The node already calls sync on its log at the right moments
(before granting a vote, before acknowledging an append); with WalLog that
sync is a real fsync. A unit test pins the contract down: granting a vote
persists and syncs the vote before the reply is produced, and a no-op vote makes
no durable write.
Crash-recovery testing
tests/recovery.rs (feature-gated) is the centrepiece. Nodes are backed by
WalLog files in a temp directory and can be "crashed" — dropped and rebuilt
from the same file, exactly as a process restart would. A proptest interleaves
crashes into a schedule of ticks, proposals, and reordered deliveries across a
three-node cluster, asserting after every step that no two nodes (or
incarnations) ever apply a different command at the same index. Deterministic
tests confirm a fully replicated log survives a restart of every node — recovered
byte-for-byte — that the cluster then re-elects and keeps committing, and that a
recovered node's term never regresses.
New example
persistent_node(run with--features persistence) — a node elects
itself, commits proposals, is dropped (closing its WAL), then reopens the same
file, recovers its log and term, and carries on.
Breaking changes
None. persistence is a new, off-by-default feature; the existing in-memory
API is unchanged.
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: 65 unit + 10 integration / property tests
(safety.rs,replication.rs,recovery.rs) + 38 doctests.- default features: 56 unit + 7 integration / property tests + 37 doctests.
Property suites were additionally run at elevated case counts
(PROPTEST_CASES=600+) with no failures. loom is still not exercised: the core
is a single-threaded, owned state machine with no lock-free or shared-state path.
What's next
- v0.5.0 — Snapshots + log compaction. An
InstallSnapshotRPC, a snapshot
policy hook, log truncation after a snapshot, follower catch-up from snapshot
plus tail, andpack-ioframing for snapshot/RPC payloads under theframing
feature.
Installation
[dependencies]
raft-io = "0.4"
# For the durable, crash-recoverable log:
raft-io = { version = "0.4", features = ["persistence"] }MSRV: Rust 1.85 (edition 2024).
Documentation
Full diff: v0.3.0...v0.4.0.
Changelog: CHANGELOG.md.