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}