How RiftCluster works · sections 4–5

Raft, and Raft under the hood

4 · Raft: agreeing on the boring things

The control plane is an embedded Raft group (openraft), running inside every rift-cluster-server process and talking over the cluster port. Raft keeps a replicated log. One node is the leader; it appends entries and copies them to followers. An entry is committed once a majority has it on disk; after that it can never be lost or reordered, and every node applies committed entries in the same order.

Followers expect a heartbeat from the leader every 50 ms. A follower that hears nothing for a random 150–300 ms becomes a candidate: it increments the term, votes for itself, and asks for votes. A node grants at most one vote per term, and only to a candidate whose log is at least as up to date as its own. A majority of votes makes a leader. The randomness keeps two candidates from splitting the vote forever.

The simulation below runs that algorithm. Time is slowed about 15× and network delay is exaggerated so you can watch messages travel.

Five voters electing a leader and committing writeslive · click a node to crash or restart it
vote requestvote grantedheartbeatappend entries
TRY THISWait for a leader, then send a few writes: solid squares are committed, hollow ones are not yet. Crash the leader and watch a new term begin. Then partition the fleet: the two-node side can't commit anything, and its writes come back 503 with an op-id. Heal it, and the stale leader steps down.

What the simulation leaves out, and what the real cluster adds on top:

5 · Raft under the hood

Section 4 is the algorithm. This section is what the cluster actually puts on the wire to run it. There are two moving parts. openraft is the engine: it owns terms, votes, logs, timers and the decision of what to send to whom. A small network layer in this crate carries those messages between processes, signs them, and decides how hard to try.

One port, signed HTTP

Every node binds one cluster port: --cluster-bind, which has no default because exposing it should be a deliberate choice. It tells peers where to reach it with --cluster-advertise, which defaults to the bind address and may be a hostname. Everything between nodes is a JSON POST over plain HTTP/1.1 on that port, from a pooled client with TCP_NODELAY, a 500 ms connect timeout and a 2 s request timeout:

PathCarries
/internal/v1/raft/appendAppendEntries: log entries, or none at all (a heartbeat)
/internal/v1/raft/voteRequestVote
/internal/v1/raft/snapshotone chunk of a snapshot install
/internal/v1/cluster/join · leave · retiremembership requests, answered by the leader
/internal/v1/cluster/writean admin write forwarded to the leader
/internal/v1/cluster/membership · /internal/v1/appliedread-only: who are you and what is your membership; how far have you applied
/_cluster/flow/* · seq/* · proxy/*the data plane's owner RPCs (sections 10 and 11)

There is no TLS on this port. It is meant for a private network, and a public bind address is refused unless acknowledged. What it does have is authentication on every single request. The sender signs it with HMAC-SHA256 under the shared --cluster-secret, over five length-prefixed fields: a timestamp, a random nonce, the method, the path with its query, and the whole body. The receiver checks in a fixed order, and the order is part of the design: cheap checks that need no secret come first, and the nonce is recorded last, so an unauthenticated sender can't fill the replay cache. A request sent to a member, rather than to an address, also names its recipient: ?to=<node id>, inside the signed path. After the signature checks, and before any handler runs, the receiver refuses a request named for another node (D-96). Build a request below and tamper with it.

One request on the cluster portlive · the MAC is computed for real (HMAC-SHA256, demo secret)

  

Two details are easy to miss. The protocol check runs before the body is read, so a node on a different major version is turned away with 426 before it costs any parsing. And the nonce cache (100,000 entries, each kept for 61 s, twice the 30 s skew allowance plus one) fails closed: if it is full of live entries, new requests are refused rather than admitted unchecked. A node run with --cluster-insecure skips signing and verification entirely and says so loudly at startup.

Finding each other: seeds and the membership

Nodes don't discover each other by broadcasting. A new node knows one thing, its --cluster-seeds, and uses them for exactly one request: join. From then on, the source of truth for “who is in the cluster, and where” is the Raft membership itself. Every member's advertise address is stored, verbatim, in the committed membership entry, so every node reads the same peer list at the same log index.

The timers

TimerValueWhat it does
Heartbeat50 msThe leader sends AppendEntries to every follower at least this often; each call is given 50 ms before openraft moves on.
Election timeoutrandom 150–300 msHow long a node with no leader waits before campaigning. The randomness keeps two nodes from splitting the vote forever.
Leader lease300 msA follower that heard a leader refuses vote requests for this long, and waits lease + election timeout (450–600 ms) before campaigning itself. openraft 0.9 has no pre-vote; the lease is what protects a healthy leader from a node that merely lost touch.
Restart grace3 sA restarting member of a multi-voter fleet may not campaign until it hears a leader or 3 s pass.
Isolation window900 msA leader with no quorum acknowledgement for 3 × 300 ms, or a node that knows no leader, reports itself isolated, and refuses owner-side flow operations (D-17).
Liveness probeevery 50 ms, 150 ms deadlineThe second heartbeat, explained below.
Peer health3 failures → 5 s, half-open every 250 msOrdinary calls to a peer that keeps failing are refused locally instead of timing out, with one trial call let through per 250 ms (D-78).

These values are fixed, not operator knobs, and a test pins them (D-42): the isolation window, the probe and the snapshot timeouts are all derived from them, and changing one alone breaks the arithmetic that keeps the others safe.

An election, message by message

The second heartbeat

openraft's heartbeat is an ordinary AppendEntries with no entries. It works until the leader has something big in flight to a follower: a multi-megabyte append, or a snapshot being installed chunk by chunk. While that transfer is running, openraft sends that follower nothing else, and a follower that hears nothing for longer than its election timeout campaigns. Its term then runs ahead of the leader's, the leader refuses it, and the pair never reconcile.

So the network layer runs its own liveness ticker for each peer. Every 50 ms, if this node is the leader right now (read live, not cached) and nothing has gone to that peer in the last 50 ms, it sends a minimal AppendEntries carrying only the leader's vote and no log position. On the follower the vote is checked before the log, so this refreshes its election timer and can never truncate anything. It goes through the probe path, which bypasses the peer-health gate (D-22): a heartbeat refused locally because the peer “recently failed” is exactly how a restarted voter would never hear the leader. It waits at most 150 ms for a reply (D-83), because its job is done once it arrives.

Replicating entries