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}