Skip to main content

nodedb_raft/node/
config.rs

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