How RiftCluster works · sections 13–14

Write-ahead logs and convergence

13 · Two write-ahead logs

Every node keeps two redb files in its state directory, and both are write-ahead logs in the strict sense: nothing is acknowledged until the record that makes it recoverable is on disk, to the degree that write's contract demands. The degree is what differs. raft.redb, the control plane's log, pays an fsync on every append. flow.redb, the data plane's, pays one fsync per 50 ms for everything written in between. They are separate files on purpose: a redb file has one commit lock, and a flow-state group fsync must never queue behind, or in front of, a Raft append.

The one redb rule both depend on

A redb write transaction commits at a durability level. Immediate: the data is on disk when commit() returns, because redb fsyncs before returning. None: the commit is visible to readers and correctly ordered, but sits in the OS page cache, and becomes durable only when a later Immediate commit lands. That second half is the whole trick behind group commit: many cheap None commits, then one Immediate commit makes all of them durable together.

raft.redb (control plane)flow.redb (data plane)
HoldsThe Raft log (raft_log), the vote, snapshot metadata; the state machine's tables (sm_configs, sm_routes, sm_op_dedup, sm_applied, …); and this node's own pending_intents, which are never replicated.flow_kv: every flow key this node owns or replicates, each value stamped (m_idx, v, origin); flow_meta per flow for TTL and LRU.
Written byopenraft's storage callbacks, on every node.One flow-shard-writer thread per node, batching up to 256 writes per transaction.
FsyncEvery log append and every vote: Immediate. Applying committed entries: one transaction per batch at redb's default, which is also Immediate.Chosen per imposter. sync: Immediate before the ack. async (default): None, plus one Immediate commit every 50 ms. none: never touches the file.
AcknowledgedWhen a majority of voters have the entry fsynced (commit), and then the barrier.sync: after the fsync. async: as soon as the write is in memory and handed to the writer, which is one channel send.
After a crashEverything committed is replayed. Truncations and purges are deliberately not fsynced, because openraft re-drives them after a restart.recover replays flow_kv, drops expired entries and trims to the flow cap; the first touch of each flow re-checks it against its replicas.

The Raft log, one entry on one follower

The entry itself is small JSON: a ControlRequest carrying the op_id, the operation (PutImposter, a stub patch, a delete…), and issued_at_secs, which is the state machine's only clock. Nothing in apply reads the local time, so every node computes the same result from the same entry.

Snapshots keep the log from growing forever. Every 5,000 entries openraft builds one from the state machine; the payload is written to a temporary file, fsynced, renamed into place, the directory is fsynced, and only then is a row naming it committed, so a crash at any point leaves either the old snapshot or the new one, never half of one. The 1,000 most recent entries are kept and older ones purged. A follower that needs a purged entry is sent the snapshot instead (section 14).

The third table in raft.redb, pending_intents, is the R4 log from section 6: the receiving node writes each admin request there with an Immediate commit before forwarding it, and clears it once the outcome is committed. It is local to that node: it is never replicated and never part of a snapshot.

The flow log, under load

Flow writes arrive at mock-traffic rates, so the writer never fsyncs per write in the default mode. The figure runs that writer: every acknowledged write is a bar on the top lane, red until a group fsync covers it and green after. The gauge is the node's real metric, rift_cluster_flow_wal_lag_ops: acknowledged writes that are committed at None but not yet fsynced. It is exactly what a power cut on this node, right now, would take off this disk.

One node's flow writerlive · simulated time at ¼ speed
writes / s600/s
WAL LAG (UNSYNCED ACKS)
0
FSYNCS / S
0
AN ACK WAITS FOR
—

Two things keep the async window honest. Each write was also pushed to the key's two replicas, which run their own writers and their own 50 ms tickers, so a write is lost from the cluster only if all three holders lose power inside the same interval (section 12's figure). And replicas persist in the origin's mode: the durability travels with the replication message, so an imposter that asked for none is not quietly written to disk by its replicas, and one that asked for sync is not demoted.

14 · How the fleet converges

“Consistent” means two different things here, because the two planes make different promises.

Config converges by replaying one log. Every node applies the same committed entries in the same order, and apply is deterministic: the leader validated the entry before appending it, and the only clock apply reads is the entry's own. So two nodes at the same applied index hold identical config, and the revision a client sees is the log index. Two nodes can only ever differ by lag, one behind the other, never by content. Lag is closed three ways:

  1. Backfill. The leader tracks how far each follower's log matches its own and resends from there. On a mismatch it walks back until the logs agree, and the follower truncates its unagreed tail. A node that restarts, or comes back from a partition, catches up this way at heartbeat speed.
  2. Snapshot install. If the entries a follower needs have been purged, the leader sends its latest snapshot. The follower replaces every state-machine table in one durable transaction, then continues by backfill from the snapshot's index.
  3. Admission. A new node joins as a learner and catches up by the same two mechanisms. The leader's 1 s sweep promotes it once its replication lag is within 16 entries, while there are fewer than 9 voters.

The barrier from section 6 is what turns will converge into R1's has converged by the time you see the 2xx. Drive it below: write, slow a node's apply, crash one, or add an empty node and watch it catch up from a snapshot.

Five members applying one loglive · a fixed leader; snapshot every 20 entries, keeping 6 (the real numbers are 5,000 and 1,000)
appliedin log, not appliedsnapshot
TRY THISWrite once: every member applies within a tick and the write returns a clean 201. Slow C and write again: the commit is instant (a majority has it on disk) but the 201 waits out the 2 s barrier and names C unapplied. Crash D and write: the barrier gives up on D after half a second and names it unreachable. Send 25 writes, then restart D: it is behind the purge horizon, so it gets a snapshot and then backfill, and until it has caught up it reports itself not Ready and the barrier doesn't wait for it. Add F: it joins as a learner, catches up the same way, and is promoted to voter.

Flow state converges toward its owner. There is no log to replay. Each key has one writer, the owner at the current m_idx, and every other copy follows it. The owner pushes each accepted write to the key's two successors, fire-and-forget, and acknowledges without waiting for them. Two mechanisms close the gaps a lost push leaves:

A strong read always goes to the owner, so a replica's lag is never visible to it. Only an imposter that opted into readConsistency: "local" can see it, by up to one push or one 5 s pull.

What you can rely on

QuestionAnswer
Can two nodes serve different config?Only one behind the other, until it applies. The 2xx waits up to 2 s for every Ready member and names any straggler in Rift-Cluster-Warnings.
Can two nodes accept writes for one flow?No. There is one owner per key at each m_idx; an old owner that is partitioned away refuses because it is isolated (D-17), and a deposed owner's writes lose any comparison by m_idx.
Can a strong flow read be stale?No. The owner answers it. When the owner can't, the read fails with 503; it never answers from a copy.
What is only eventually consistent?Flow replicas (push, then the 5 s pull), readConsistency: "local" reads, and config on a member that is lagging.
What can be lost?Config: nothing once committed. Flow state: writes an owner acknowledged but had not yet pushed, if that owner is then lost for good; and, at async, the last fsync interval if all three holders die together.