Skip to content

v0.3.0 — Log Replication

Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 08 Jun 07:06
· 12 commits to main since this release

raft-io v0.3.0 — Log Replication

A correct multi-node cluster. v0.3.0 builds the heart of Raft on top of v0.2's
election layer: a leader replicates its log to followers, tracks each follower's
progress, backtracks fast when a log diverges, and advances the commit index once
a quorum has an entry. The whole thing is held to account by an adversarial
property-test suite that reorders, drops, duplicates, and partitions messages and
asserts that committed entries never diverge. Durable persistence is v0.4.

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.3.0

AppendEntries replication with per-follower progress

A leader now carries log entries to followers, not just heartbeats. For each
follower it keeps a next_index / match_index pair and a state:

  • Probe — when a follower joins or its log diverges, the leader sends
    conservatively and waits for each reply, finding the match point.
  • Replicate — once the match point is known, the leader streams entries and
    advances next_index optimistically, pipelining batches without waiting for
    each acknowledgement. This is the steady-state throughput path.

Batches are bounded by RaftConfig::with_max_batch (default 64) so a far-behind
follower is caught up in steady chunks rather than one unbounded message.

use raft_io::RaftConfig;

// Larger batches for a high-throughput, low-RTT deployment.
let cfg = RaftConfig::new(1, [2, 3]).with_max_batch(256);
# assert_eq!(cfg.max_batch(), 256);

Fast conflict-hint backtracking

When a follower rejects an append because its log does not match, it no longer
forces the leader to step back one entry at a time. The reply now carries a hint:

use raft_io::AppendEntriesReply;

let rejection = AppendEntriesReply {
    term: 5,
    success: false,
    from: 2,
    match_index: 0,
    conflict_index: 3, // probe here next…
    conflict_term: 2,  // …or just past the leader's last entry of this term
};
# assert!(!rejection.success);

The leader uses the hint to skip its next_index back by a whole term in a
single round trip — the fast-backtracking optimisation from the Raft thesis
(§5.3). Divergent logs reconcile in O(terms), not O(entries).

Commit on a quorum, with the current-term safety rule

The commit index advances to the highest entry a majority of nodes has stored —
but only counting an entry as committable if it was created in the current
term
(Raft §5.4.2). Older-term entries ride along once a current-term entry
above them commits. This is the rule that prevents a subtle class of data loss
across leader changes, and it is enforced exactly.

use raft_io::{Action, Event, RaftConfig, RaftNode};

// A single-node cluster is a quorum of one, so it commits at once.
let mut node = RaftNode::new(RaftConfig::single(1));
while !node.is_leader() {
    let _ = node.step(Event::Tick).unwrap();
}
let actions = node.step(Event::Propose(b"x".to_vec())).unwrap();
assert!(actions.iter().any(|a| matches!(a, Action::Apply { .. })));
assert_eq!(node.commit_index(), 1);

Followers reconcile divergent tails

A follower that receives entries conflicting with its own log truncates the
divergent tail and appends the leader's version, then advances its commit index
from the leader and applies committed entries in order. The protocol guarantees
a leader never sends entries that conflict below the commit index, so committed
state is never discarded.

RaftLog::entries(from, to) — bulk range reads

The trait gains a range read so a backend can hand the leader a replication batch
in one call. It has a default implementation over entry, and MemoryLog
overrides it with a slice copy.

Adversarial property testing

tests/replication.rs is the centrepiece. It models a network that can reorder,
drop, duplicate, and partition messages, and proptest generates schedules of
ticks, proposals, and deliveries across 3- and 5-node clusters. After every step
it asserts:

  • State Machine Safety / Log Matching — if any two nodes apply an entry at
    the same index, it is the same command. No committed entry is ever contradicted,
    across leader changes and partitions.
  • Apply ordering — each node applies entries in strict index order, no gaps.

Alongside the property tests are deterministic checks that a healthy cluster
commits on every node, that a minority partition cannot commit while the majority
keeps progressing, and that a healed partition reconciles. The suite has been run
at 3,000 cases per property with no violation.

New examples

  • replicated_log — a 3-node cluster proposes a series of commands and
    prints each node's applied log to show they agree, entry for entry.
  • partition_recovery — a 5-node cluster is split; the majority keeps
    committing, the minority stalls, and healing brings every node back into sync.

Breaking changes

AppendEntriesReply gains two fields, conflict_index and conflict_term,
which carry the backtracking hint. Code that constructs the reply directly must
set them (use 0 for both on a success). This is a pre-1.0 wire-shape change;
the message protocol freezes at v0.7.

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 --all-features -- -D warnings
cargo test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check
cargo audit
cargo build --examples --all-features
cargo bench

All green. Test counts at this tag:

  • 54 unit tests
  • 7 integration / property tests (tests/safety.rs, tests/replication.rs)
  • 37 doctests

Property suites were additionally run at PROPTEST_CASES=3000 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.4.0 — Durable log + crash recovery. A wal-db-backed RaftLog under
    the persistence feature, the term/vote/log durability contract on the RPC
    path, and crash-recovery tests that kill a node mid-replication and verify it
    rejoins without violating safety.

Installation

[dependencies]
raft-io = "0.3"

MSRV: Rust 1.85 (edition 2024).

Documentation


Full diff: v0.2.0...v0.3.0.
Changelog: CHANGELOG.md.