nodedb_raft/node/config.rs
1// SPDX-License-Identifier: BUSL-1.1
2
3//! Configuration for a single Raft group on this node.
4//!
5//! A Raft group has two kinds of peer membership:
6//!
7//! - **Voters** (`peers`): full members that participate in leader election
8//! and count toward the commit quorum.
9//! - **Learners** (`learners`): non-voting members that receive replicated
10//! log entries but do not vote in elections and do not count toward the
11//! commit quorum. Learners exist so a joining node can catch up to the
12//! leader's log before being promoted to a voter — the standard Raft
13//! single-server conf-change safety pattern.
14//!
15//! `self.config.node_id` is never in either list; the node is implicitly
16//! its own member. `starts_as_learner` controls whether this node boots as
17//! a `Learner` role — set by the joining path when the group is created
18//! from a `JoinResponse` that assigned this node as a learner.
19
20use std::time::Duration;
21
22/// Configuration for a Raft node.
23#[derive(Debug, Clone)]
24pub struct RaftConfig {
25 /// This node's ID (must be unique within the Raft group).
26 pub node_id: u64,
27 /// Raft group ID (for Multi-Raft routing).
28 pub group_id: u64,
29 /// IDs of voting peers in this group (excluding self).
30 pub peers: Vec<u64>,
31 /// IDs of non-voting learner peers in this group (excluding self).
32 ///
33 /// Learners receive log replication but do not vote in elections and
34 /// are not counted in the commit quorum. They are promoted to voters
35 /// once they catch up — see `RaftNode::promote_learner`.
36 pub learners: Vec<u64>,
37 /// IDs of cross-cluster observer peers tracked by this leader.
38 ///
39 /// Observers receive log entries and send advisory acks, but they never
40 /// participate in leader election and are never counted in the commit
41 /// quorum. A slow observer does not stall source commits.
42 pub observers: Vec<u64>,
43 /// Whether this node itself starts in the `Learner` role (boot-time).
44 ///
45 /// Set `true` when a new node joins an existing cluster and is
46 /// created as a learner for a given group; cleared when the node is
47 /// promoted to voter via `promote_self_to_voter`.
48 pub starts_as_learner: bool,
49 /// Whether this node itself starts in the `Observer` role (boot-time).
50 ///
51 /// Set `true` when this node is a cross-cluster mirror replica observing
52 /// a source cluster's Raft group. An observer never participates in
53 /// elections and never contributes to the commit quorum. Acks it sends
54 /// to the source leader are advisory only.
55 pub starts_as_observer: bool,
56 /// Minimum election timeout.
57 pub election_timeout_min: Duration,
58 /// Maximum election timeout.
59 pub election_timeout_max: Duration,
60 /// Heartbeat interval (must be << election_timeout_min).
61 pub heartbeat_interval: Duration,
62}
63
64impl RaftConfig {
65 /// Total number of voters (self + voter peers).
66 ///
67 /// Learners are excluded. This value drives quorum math and so must
68 /// never grow transiently while the learner is catching up — that is
69 /// exactly the safety property the learner phase is designed to give.
70 pub fn cluster_size(&self) -> usize {
71 self.peers.len() + 1
72 }
73
74 /// Quorum size: `floor(n/2) + 1` over the voter set.
75 pub fn quorum(&self) -> usize {
76 self.cluster_size() / 2 + 1
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn cfg(peers: Vec<u64>, learners: Vec<u64>) -> RaftConfig {
85 RaftConfig {
86 node_id: 1,
87 group_id: 0,
88 peers,
89 learners,
90 observers: vec![],
91 starts_as_learner: false,
92 starts_as_observer: false,
93 election_timeout_min: Duration::from_millis(150),
94 election_timeout_max: Duration::from_millis(300),
95 heartbeat_interval: Duration::from_millis(50),
96 }
97 }
98
99 #[test]
100 fn quorum_excludes_learners() {
101 // Single voter (self), two learners catching up → quorum is still 1.
102 let c = cfg(vec![], vec![2, 3]);
103 assert_eq!(c.cluster_size(), 1);
104 assert_eq!(c.quorum(), 1);
105
106 // Three voters + one learner → quorum is 2 (not 3).
107 let c = cfg(vec![2, 3], vec![4]);
108 assert_eq!(c.cluster_size(), 3);
109 assert_eq!(c.quorum(), 2);
110
111 // Five voters + two learners → quorum is 3.
112 let c = cfg(vec![2, 3, 4, 5], vec![6, 7]);
113 assert_eq!(c.cluster_size(), 5);
114 assert_eq!(c.quorum(), 3);
115 }
116}