Skip to main content

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}