Docs/Architecturev0.0.1-preview

Replication and consistency

Raft-family consensus, quorum-gated durability, safe membership changes, and precisely-stated read guarantees.

On this page
  1. Voters, learners, quorum
  2. Elections, without the folklore failure modes
  3. Membership changes are log entries
  4. Read guarantees, precisely
  5. What failure looks like

A TulaDB cluster replicates one totally-ordered log of committed batches across nodes using Raft-family consensus. Writes are linearizable; reads are linearizable by default; and every weaker read mode is opt-in and explicitly bounded.

Voters, learners, quorum

A cluster is a set of voters (typically 3, or 5) plus optional learners. Voters form quorum: a write is committed when a majority of voters hold it durably in their WALs. Learners receive the full log but never vote and never count toward quorum — they’re for read fan-out, analytics isolation, warm standbys, and the safe way to grow a cluster (add as learner, catch up, promote).

Followers proxy writes to the current leader, so a client pointed at any node still lands its write correctly; leader-aware SDK clients skip the hop by routing to the leader directly. A proxied write that reaches a node which is no longer the leader is never executed there: it answers the retryable ERR_QUORUM_TIMEOUT, and the client’s retry under the same identity finds the new leader.

A new node joins as a learner and becomes a voter only once it holds an exact full copy of the cluster’s data — including retained history, which in this release it syncs from the voters and verifies. See Joining a node.

Elections, without the folklore failure modes

Leader election includes the hardening that separates a production consensus implementation from a textbook one:

  • Pre-vote — a partitioned node returning to the cluster can’t bump the term and depose a healthy leader.
  • Check-quorum step-down — a leader that can’t reach a majority stops accepting writes and steps down, closing the stale-leader window.
  • Candidacy suppression — a node that is fail-stopped, badly lagged, or absent from the persisted voter set doesn’t stand for election.
  • Planned leadership transfer — operators move leadership deliberately (tulactl transfer-leadership) before maintenance instead of inducing a failover.

Consensus control traffic (heartbeats, votes) runs on its own port, isolated from bulk replication — snapshot installs can’t delay heartbeats into a spurious election.

Timing defaults. The heartbeat is 50 ms and the election timeout is randomized in 250–500 ms, so a crashed leader is detected within half a second of its last heartbeat; check-quorum step-down defaults to 1 s. A voter also persists the vote fence of the latest leader it acknowledged, so after a restart it refuses (pre-)votes for long enough to honour any lease that leader may still hold — which is what makes a rolling change of heartbeat timing lease-safe once every node runs the same build. Do a binary upgrade and a timing change in separate rolls.

Version fencing. Nodes speak the client wire at exactly WIRE_VERSION 4 and the control protocol at exactly one version; a node on another version can neither vote nor lead, and its frames are dropped and logged rather than half-understood.

Transport security is not optional off loopback. A node whose fabric or heartbeat transport is reachable beyond loopback — a single node included — refuses to start unless mutual TLS is required (network_mtls_required = 1, with TCP heartbeat mTLS), because peer authentication binds each node id to its certificate only under mTLS. Only loopback-bound test clusters run without certificates.

Membership changes are log entries

Cluster configuration is itself replicated through the log and persisted crash-safely. Single-node changes use single-server reconfiguration; anything larger goes through joint consensus, where quorum temporarily requires a majority of both the old and new voter sets — for replication, elections, and read barriers alike. Operators drive changes through tulactl reconfig; the engine derives the safe intermediate configurations itself and refuses client-submitted joint configs.

The operational consequences are simple: grow by adding learners and promoting them one at a time; never shrink by deleting nodes from a config file; after the first committed configuration, the replicated config — not the TOML — is authoritative.

Every promotion path — an operator’s reconfig --promote, promote-standby, and policy-driven auto-promotion — requires the learner to report that it holds the full idempotency set, has no migration in flight, and holds its retained history (HISTORY_OK); there is no override. A change whose proposed voters don’t yet have a live quorum is refused with the retryable ERR_RECONFIG_NO_QUORUM (48), and the same change succeeds once they are up. A reconfig that removes or demotes the current leader reports success when it commits.

Read guarantees, precisely

Default reads are linearizable. The engine implements Raft’s ReadIndex protocol: before answering, the leader captures the commit index and confirms it still has quorum — via a leadership-lease fast path when timing bounds allow, or a heartbeat round otherwise — then waits for local apply to catch up. A deposed leader that hasn’t noticed yet cannot serve you a stale balance, and a newly elected leader serves no linearizable read until an entry of its own term has committed, so it can’t answer from a log it hasn’t yet confirmed. The lease fast path is guarded at startup: the lease is the voters’ recent-leader fence (2 × raft_heartbeat_ms) minus the clock-skew allowance, so cluster_clock_skew_max_ms must be less than 2 × raft_heartbeat_ms (defaults: 25 ms against a 100 ms fence). An unsafe combination refuses to boot rather than serving subtly wrong reads.

Follower reads are opt-in and bounded. A client may explicitly allow a specific read to be served by a follower. Two protections remain: the follower must have recent proof of leader contact, and it will never answer below that client’s own last write — read-your-writes holds. That wait is bounded: a read-your-writes position beyond every batch the node knows of is refused at once, and a node caps how many reads may wait at a time; both answer the retryable ERR_QUORUM_TIMEOUT. Staleness is bounded by the follower’s apply lag. It is not linearizable; use it for dashboards, not admission decisions.

Derived query indexes are asynchronous. History, listings, and search trail applied state slightly by default — see Queries for the gating options.

What failure looks like

Event Behavior
Leader crash Followers elect a new leader; committed writes are preserved; in-flight indeterminate writes resolve on idempotent resubmit
Network partition The majority side continues; the minority side refuses writes and stale-risky reads — no split brain
Follower down Quorum unaffected (3-node cluster tolerates one); on return it catches up from the log, or from a snapshot install followed by a verified history sync
Quorum lost The cluster stops accepting writes — loudly — rather than diverging; reads that require quorum confirmation refuse. A node whose applied-but-uncommitted writes must be truncated fails its WAL closed (quorum_failstop_halt_on_stranded, on by default) and re-derives state from the committed log on restart — run tuladbd under a supervisor that restarts it
Conflicting entry at the same index and term Impossible under correct Raft, so it is treated as corruption: the replica latches ERR_CORRUPTION instead of truncating, and names both entries
Slow follower Lag is visible (tuladb_follower_lag_entries); an optional admission guard can shed writes rather than let a replica fall unrecoverably behind

The theme is consistent with the rest of the system: when safety and availability conflict, TulaDB chooses safety, visibly. For how these behaviors are actually exercised — partitions, crashes, clock skew, storage faults — see How we verify correctness.