raftbare/
config.rs

1use crate::node::NodeId;
2use std::{
3    collections::{
4        BTreeSet,
5        btree_set::{Iter, Union},
6    },
7    iter::Peekable,
8};
9
10/// Cluster configuration (membership).
11///
12/// # Examples
13///
14/// ```
15/// use raftbare::{ClusterConfig, NodeId};
16///
17/// // Makes a new cluster configuration with two voting nodes.
18/// let mut config = ClusterConfig::new();
19/// config.voters.insert(NodeId::new(0));
20/// config.voters.insert(NodeId::new(1));
21/// assert!(!config.is_joint_consensus());
22///
23/// // Adds a new non-voting node.
24/// config.non_voters.insert(NodeId::new(2));
25/// assert!(!config.is_joint_consensus());
26///
27/// // Updates the configuration to add a new voting node.
28/// config.new_voters = config.voters.clone();
29/// config.new_voters.insert(NodeId::new(3));
30/// assert!(config.is_joint_consensus());
31/// ```
32///
33/// The `config` value in the example above can be applied to a cluster via [`Node::propose_config()`][crate::Node::propose_config].
34///
35#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
36pub struct ClusterConfig {
37    /// Voting nodes.
38    ///
39    /// For a cluster to be available,
40    /// the majority of the voters must be alive and able to communicate with each other.
41    pub voters: BTreeSet<NodeId>,
42
43    /// New voting nodes during joint consensus.
44    ///
45    /// When a cluster changes its configuration, it enters a joint consensus state.
46    /// In that state, `voters` and `new_voters` represent the old and new configurations, respectively.
47    ///
48    /// During joint consensus, the cluster requires the majority of both the old and new voters to be available
49    /// to proceed with leader election and log entry commit.
50    ///
51    /// Once the log entry for this joint configuration is committed, `voters` takes the value of `new_voters`,
52    /// and the cluster exits the joint consensus state.
53    ///
54    /// If `new_voters` is empty, it means there are no configuration changes in progress.
55    pub new_voters: BTreeSet<NodeId>,
56
57    /// Non-voting nodes.
58    ///
59    /// Non-voting nodes do not participate in the leader election and log entry commit quorum,
60    /// but they do replicate log entries from the leader.
61    /// Additionally, non-voting nodes never transition to candidate or leader.
62    ///
63    /// When adding more than half of the current number of nodes to a cluster that has a large snapshot or many log entries,
64    /// it is recommended to first add the new nodes as non-voters to avoid blocking subsequent log commits.
65    /// Allow these nodes to catch up with the leader before promoting them to voters.
66    ///
67    /// Note that adding or removing non-voters does not require a joint consensus.
68    pub non_voters: BTreeSet<NodeId>,
69}
70
71impl ClusterConfig {
72    /// Makes a new empty [`ClusterConfig`] instance.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Returns `true` if the given node is in this configuration.
78    pub fn contains(&self, id: NodeId) -> bool {
79        self.voters.contains(&id) || self.new_voters.contains(&id) || self.non_voters.contains(&id)
80    }
81
82    /// Returns `true` if this configuration represents a joint consensus state.
83    pub fn is_joint_consensus(&self) -> bool {
84        !self.new_voters.is_empty()
85    }
86
87    /// Gets an iterator over all unique [`NodeId`]s in this configuration, in sorted order.
88    pub fn unique_nodes(&self) -> impl '_ + Iterator<Item = NodeId> {
89        UniqueNodes {
90            voters: self.voters.union(&self.new_voters).peekable(),
91            non_voters: self.non_voters.iter().peekable(),
92        }
93    }
94
95    pub(crate) fn unique_voters(&self) -> impl '_ + Iterator<Item = NodeId> {
96        self.voters.union(&self.new_voters).copied()
97    }
98
99    pub(crate) fn is_voter(&self, id: NodeId) -> bool {
100        self.voters.contains(&id) || self.new_voters.contains(&id)
101    }
102
103    /// Converts this configuration to a joint consensus by adding and removing voters.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use raftbare::{Node, NodeId};
109    ///
110    /// fn add_node(node: &mut Node, adding_node_id: NodeId) {
111    ///     let new_config = node.config().to_joint_consensus(&[adding_node_id], &[]);
112    ///     assert_eq!(new_config.voters.len() + 1, new_config.new_voters.len());
113    ///
114    ///     node.propose_config(new_config);
115    /// }
116    ///
117    /// fn remove_node(node: &mut Node, removing_id: NodeId) {
118    ///     let new_config = node.config().to_joint_consensus(&[], &[removing_id]);
119    ///     assert_eq!(new_config.voters.len() - 1, new_config.new_voters.len());
120    ///
121    ///     node.propose_config(new_config);
122    /// }
123    /// ```
124    pub fn to_joint_consensus(&self, adding_voters: &[NodeId], removing_voters: &[NodeId]) -> Self {
125        let mut config = self.clone();
126        config.new_voters = config.voters.clone();
127        config.new_voters.extend(adding_voters.iter().copied());
128        config.new_voters.retain(|id| !removing_voters.contains(id));
129        config
130    }
131
132    pub(crate) fn voter_majority_count(&self) -> usize {
133        self.voters.len() / 2 + 1
134    }
135
136    pub(crate) fn new_voter_majority_count(&self) -> usize {
137        if self.new_voters.is_empty() {
138            0
139        } else {
140            self.new_voters.len() / 2 + 1
141        }
142    }
143}
144
145#[derive(Debug)]
146pub struct UniqueNodes<'a> {
147    voters: Peekable<Union<'a, NodeId>>,
148    non_voters: Peekable<Iter<'a, NodeId>>,
149}
150
151impl Iterator for UniqueNodes<'_> {
152    type Item = NodeId;
153
154    fn next(&mut self) -> Option<Self::Item> {
155        let a = self.voters.peek().copied();
156        let b = self.non_voters.peek().copied();
157        match (a, b) {
158            (None, None) => None,
159            (Some(a), None) => {
160                self.voters.next();
161                Some(*a)
162            }
163            (None, Some(b)) => {
164                self.non_voters.next();
165                Some(*b)
166            }
167            (Some(a), Some(b)) if a == b => {
168                self.voters.next();
169                self.non_voters.next();
170                Some(*a)
171            }
172            (Some(a), Some(b)) if a < b => {
173                self.voters.next();
174                Some(*a)
175            }
176            (Some(_), Some(b)) => {
177                self.non_voters.next();
178                Some(*b)
179            }
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn unique_nodes() {
190        let mut config = ClusterConfig::new();
191        config.voters.insert(id(1));
192        config.voters.insert(id(2));
193        config.new_voters.insert(id(2));
194        config.new_voters.insert(id(3));
195        config.non_voters.insert(id(4));
196        config.non_voters.insert(id(5));
197        config.non_voters.insert(id(6));
198
199        let nodes: Vec<_> = config.unique_nodes().map(|n| n.get()).collect();
200        assert_eq!(nodes, vec![1, 2, 3, 4, 5, 6]);
201    }
202
203    fn id(n: u64) -> NodeId {
204        NodeId::new(n)
205    }
206}