kevy_config/replication.rs
1//! `[replication]` section schema — primary/replica streaming
2//! replication.
3
4/// `[replication]` section — primary/replica streaming replication.
5/// When `role = "standalone"` (default) this whole subsystem is
6/// dormant: no listener, no upstream connection, no buffer
7/// allocated. `role = "primary"` brings up a TCP listener on
8/// `listen_port` that streams every applied mutation to connected
9/// replicas. `role = "replica"` connects to `upstream`, full-syncs
10/// from a snapshot, then applies live frames.
11///
12/// Quorum failover is configured separately via the `[cluster]`
13/// `node_id` + `peers` keys; see [`crate::cluster::ClusterSection`].
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ReplicationSection {
16 /// Node role. `Standalone` (default) disables the whole subsystem.
17 pub role: ReplicationRole,
18 /// `host:port` of the primary, for `role = "replica"`. Ignored when
19 /// `role != "replica"`. `None` for replica = config-error at startup.
20 pub upstream: Option<String>,
21 /// TCP port BASE the primary listens on for incoming replica
22 /// connections — shard `i` binds at `listen_port_base + i`, mirroring
23 /// the cluster listener pattern. `0` (default) = `server.port +
24 /// 10000` as the base, picked at startup. Only meaningful when
25 /// `role = "primary"`. Replicas use a shard-aware client that
26 /// connects to all `nshards` ports to mirror the full keyspace.
27 pub listen_port_base: u16,
28 /// Bounded ring buffer for recent applied frames (bytes). A replica
29 /// that disconnects and reconnects within this backlog window catches
30 /// up without a full snapshot. Default `256mb`.
31 pub replication_buffer_size: u64,
32 /// How long the primary keeps a disconnected replica's slot before
33 /// dropping it (and forcing a full snapshot on reconnect). Default
34 /// `60_000` (60 s).
35 pub reconnect_window_ms: u32,
36 /// v3.14 D5 — primary refuses writes when fewer than this many
37 /// replicas have a live ACK within `min_replicas_max_lag_ms`.
38 /// `0` (default) disables the check. A lag heuristic, not a
39 /// quorum guarantee — the quorum lease (v3.16) is the real
40 /// split-brain fence.
41 pub min_replicas_to_write: u32,
42 /// Freshness window for `min_replicas_to_write` (ms).
43 pub min_replicas_max_lag_ms: u32,
44 /// v3.16 D3 — bounded staleness: a replica whose last primary
45 /// heartbeat is older than this refuses reads with `-STALE`
46 /// (client falls back to the primary). `0` = off (default:
47 /// reads always answer, whatever the lag).
48 pub replica_max_staleness_ms: u32,
49 /// v3.14 — reject client writes while in the replica role
50 /// (`-READONLY`). Default `true`, Redis-compatible; the
51 /// replication apply path and admin verbs bypass the gate.
52 pub replica_read_only: bool,
53 /// v3.2 — the upstream is a SINGLE stream on one port (an embedded
54 /// writer's `embed_writer_listen_addr` source) rather than a
55 /// per-shard port fleet: one routing runner fans frames into local
56 /// shards by key hash. Only meaningful when `role = "replica"`.
57 pub single_source: bool,
58}
59
60impl Default for ReplicationSection {
61 fn default() -> Self {
62 Self {
63 role: ReplicationRole::Standalone,
64 upstream: None,
65 listen_port_base: 0,
66 replication_buffer_size: 256 * 1024 * 1024,
67 reconnect_window_ms: 60_000,
68 min_replicas_to_write: 0,
69 min_replicas_max_lag_ms: 10_000,
70 replica_max_staleness_ms: 0,
71 replica_read_only: true,
72 single_source: false,
73 }
74 }
75}
76
77/// Node role for the `[replication]` subsystem.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79pub enum ReplicationRole {
80 /// Default. Replication subsystem dormant; behaves like pre-v3.
81 #[default]
82 Standalone,
83 /// This node accepts writes and streams mutations to replicas.
84 Primary,
85 /// This node connects to a primary and mirrors its keyspace read-only.
86 Replica,
87}
88
89impl ReplicationRole {
90 /// Canonical name used by `CONFIG GET replication.role` and TOML.
91 pub fn as_str(&self) -> &'static str {
92 match self {
93 Self::Standalone => "standalone",
94 Self::Primary => "primary",
95 Self::Replica => "replica",
96 }
97 }
98 /// Inverse of [`Self::as_str`] — case-insensitive.
99 pub fn parse(s: &str) -> Option<Self> {
100 match s.to_ascii_lowercase().as_str() {
101 "standalone" => Some(Self::Standalone),
102 "primary" => Some(Self::Primary),
103 "replica" => Some(Self::Replica),
104 _ => None,
105 }
106 }
107}