Skip to main content

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}