Skip to main content

noxu_rep/
rep_group.rs

1//! Replication group management.
2//!
3
4use hashbrown::{HashMap, HashSet};
5
6use crate::node_type::NodeType;
7use crate::quorum_policy::QuorumPolicy;
8use crate::rep_node::RepNode;
9
10/// A replication group consisting of named nodes.
11///
12/// The group tracks its members and provides queries for electable nodes,
13/// monitors, quorum size, etc. Each node in the group has a unique name.
14#[derive(Debug, Clone)]
15pub struct RepGroup {
16    /// The name of this replication group.
17    name: String,
18    /// A unique identifier for this group instance.
19    group_id: u64,
20    /// Map from node name to node info.
21    nodes: HashMap<String, RepNode>,
22    /// Quorum policy controlling Phase 1 / Phase 2 election sizes.
23    quorum_policy: QuorumPolicy,
24}
25
26impl RepGroup {
27    /// Creates a new replication group with the given name and ID.
28    ///
29    /// Defaults to [`QuorumPolicy::SimpleMajority`].  Use
30    /// [`RepGroup::with_policy`] to specify a Flexible or Expression policy.
31    pub fn new(name: String, group_id: u64) -> Self {
32        Self {
33            name,
34            group_id,
35            nodes: HashMap::new(),
36            quorum_policy: QuorumPolicy::SimpleMajority,
37        }
38    }
39
40    /// Creates a new replication group with an explicit quorum policy.
41    pub fn with_policy(
42        name: String,
43        group_id: u64,
44        policy: QuorumPolicy,
45    ) -> Self {
46        Self { name, group_id, nodes: HashMap::new(), quorum_policy: policy }
47    }
48
49    /// Replace the quorum policy for this group.
50    pub fn set_quorum_policy(&mut self, policy: QuorumPolicy) {
51        self.quorum_policy = policy;
52    }
53
54    /// Returns a reference to the current quorum policy.
55    pub fn quorum_policy(&self) -> &QuorumPolicy {
56        &self.quorum_policy
57    }
58
59    /// Returns the group name.
60    pub fn name(&self) -> &str {
61        &self.name
62    }
63
64    /// Returns the group ID.
65    pub fn group_id(&self) -> u64 {
66        self.group_id
67    }
68
69    /// Adds a node to the group. Returns the previous node with the same
70    /// name, if any.
71    pub fn add_node(&mut self, node: RepNode) -> Option<RepNode> {
72        self.nodes.insert(node.name.clone(), node)
73    }
74
75    /// Removes a node from the group by name. Returns the removed node,
76    /// if it existed.
77    pub fn remove_node(&mut self, name: &str) -> Option<RepNode> {
78        self.nodes.remove(name)
79    }
80
81    /// Returns a reference to the node with the given name, if present.
82    pub fn get_node(&self, name: &str) -> Option<&RepNode> {
83        self.nodes.get(name)
84    }
85
86    /// Returns all nodes in the group.
87    pub fn get_nodes(&self) -> Vec<&RepNode> {
88        self.nodes.values().collect()
89    }
90
91    /// Returns all electable nodes (those that participate in elections).
92    pub fn get_electable_nodes(&self) -> Vec<&RepNode> {
93        self.nodes.values().filter(|n| n.node_type().is_electable()).collect()
94    }
95
96    /// Returns all monitor nodes.
97    pub fn get_monitors(&self) -> Vec<&RepNode> {
98        self.nodes
99            .values()
100            .filter(|n| n.node_type() == NodeType::Monitor)
101            .collect()
102    }
103
104    /// Returns the number of electable nodes in the group.
105    pub fn electable_count(&self) -> u32 {
106        self.nodes.values().filter(|n| n.node_type().is_electable()).count()
107            as u32
108    }
109
110    /// Returns the Phase 1 (Prepare/Promise) quorum size under the current policy.
111    pub fn phase1_quorum(&self) -> usize {
112        self.quorum_policy.phase1_quorum(self.electable_count() as usize)
113    }
114
115    /// Returns the Phase 2 (Accept/Commit) quorum size under the current policy.
116    pub fn phase2_quorum(&self) -> usize {
117        self.quorum_policy.phase2_quorum(self.electable_count() as usize)
118    }
119
120    /// Returns `true` if `voters` satisfies the Phase 2 quorum requirement.
121    pub fn is_valid_phase2_quorum(&self, voters: &HashSet<&str>) -> bool {
122        self.quorum_policy
123            .is_valid_phase2_quorum(voters, self.electable_count() as usize)
124    }
125
126    /// Validate and optionally rebuild the quorum system after a membership
127    /// change.  For `SimpleMajority` and `Expression` policies this is always
128    /// valid; for `Flexible` it checks `phase1 + phase2 > n`.
129    ///
130    /// Returns `Err` if the current policy is unsafe for the new group size.
131    pub fn rebuild_quorum_system(&self) -> Result<(), String> {
132        self.quorum_policy.validate(self.electable_count() as usize)
133    }
134
135    /// Returns the phase-2 quorum size for the configured quorum policy.
136    ///
137    /// This is a compatibility shim that returns
138    /// [`phase2_quorum`](Self::phase2_quorum) cast to `u32`. The result is
139    /// **policy-dependent**: under the default `Majority` policy this is a
140    /// simple majority of electable nodes, but under `Flexible` or
141    /// `Expression` policies it may be larger or smaller than a simple
142    /// majority. New code should call [`phase2_quorum`](Self::phase2_quorum)
143    /// directly.
144    pub fn quorum_size(&self) -> u32 {
145        self.phase2_quorum() as u32
146    }
147
148    /// Returns `true` if the group contains a node with the given name.
149    pub fn contains_node(&self, name: &str) -> bool {
150        self.nodes.contains_key(name)
151    }
152
153    /// Returns the total number of nodes in the group.
154    pub fn node_count(&self) -> usize {
155        self.nodes.len()
156    }
157}
158
159impl std::fmt::Display for RepGroup {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        write!(
162            f,
163            "RepGroup(name={}, id={}, nodes={})",
164            self.name,
165            self.group_id,
166            self.nodes.len()
167        )
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn make_electable(name: &str, id: u32) -> RepNode {
176        RepNode::new(
177            name.to_string(),
178            NodeType::Electable,
179            "localhost".to_string(),
180            5000 + id as u16,
181            id,
182        )
183    }
184
185    fn make_monitor(name: &str, id: u32) -> RepNode {
186        RepNode::new(
187            name.to_string(),
188            NodeType::Monitor,
189            "localhost".to_string(),
190            5000 + id as u16,
191            id,
192        )
193    }
194
195    fn make_secondary(name: &str, id: u32) -> RepNode {
196        RepNode::new(
197            name.to_string(),
198            NodeType::Secondary,
199            "localhost".to_string(),
200            5000 + id as u16,
201            id,
202        )
203    }
204
205    fn make_arbiter(name: &str, id: u32) -> RepNode {
206        RepNode::new(
207            name.to_string(),
208            NodeType::Arbiter,
209            "localhost".to_string(),
210            5000 + id as u16,
211            id,
212        )
213    }
214
215    #[test]
216    fn test_new_group() {
217        let group = RepGroup::new("testgroup".to_string(), 1);
218        assert_eq!(group.name(), "testgroup");
219        assert_eq!(group.group_id(), 1);
220        assert_eq!(group.node_count(), 0);
221    }
222
223    #[test]
224    fn test_add_and_get_node() {
225        let mut group = RepGroup::new("g".to_string(), 1);
226        let node = make_electable("n1", 1);
227        assert!(group.add_node(node).is_none());
228        assert!(group.get_node("n1").is_some());
229        assert_eq!(group.get_node("n1").unwrap().name(), "n1");
230    }
231
232    #[test]
233    fn test_add_replaces_existing() {
234        let mut group = RepGroup::new("g".to_string(), 1);
235        group.add_node(make_electable("n1", 1));
236        let old = group.add_node(make_electable("n1", 2));
237        assert!(old.is_some());
238        assert_eq!(old.unwrap().node_id(), 1);
239        assert_eq!(group.get_node("n1").unwrap().node_id(), 2);
240    }
241
242    #[test]
243    fn test_remove_node() {
244        let mut group = RepGroup::new("g".to_string(), 1);
245        group.add_node(make_electable("n1", 1));
246        let removed = group.remove_node("n1");
247        assert!(removed.is_some());
248        assert!(!group.contains_node("n1"));
249        assert!(group.remove_node("n1").is_none());
250    }
251
252    #[test]
253    fn test_contains_node() {
254        let mut group = RepGroup::new("g".to_string(), 1);
255        assert!(!group.contains_node("n1"));
256        group.add_node(make_electable("n1", 1));
257        assert!(group.contains_node("n1"));
258    }
259
260    #[test]
261    fn test_get_nodes() {
262        let mut group = RepGroup::new("g".to_string(), 1);
263        group.add_node(make_electable("n1", 1));
264        group.add_node(make_monitor("m1", 2));
265        assert_eq!(group.get_nodes().len(), 2);
266    }
267
268    #[test]
269    fn test_get_electable_nodes() {
270        let mut group = RepGroup::new("g".to_string(), 1);
271        group.add_node(make_electable("n1", 1));
272        group.add_node(make_electable("n2", 2));
273        group.add_node(make_monitor("m1", 3));
274        group.add_node(make_secondary("s1", 4));
275        group.add_node(make_arbiter("a1", 5));
276
277        let electables = group.get_electable_nodes();
278        // Electable + Arbiter = 3
279        assert_eq!(electables.len(), 3);
280    }
281
282    #[test]
283    fn test_get_monitors() {
284        let mut group = RepGroup::new("g".to_string(), 1);
285        group.add_node(make_electable("n1", 1));
286        group.add_node(make_monitor("m1", 2));
287        group.add_node(make_monitor("m2", 3));
288
289        let monitors = group.get_monitors();
290        assert_eq!(monitors.len(), 2);
291    }
292
293    #[test]
294    fn test_electable_count() {
295        let mut group = RepGroup::new("g".to_string(), 1);
296        assert_eq!(group.electable_count(), 0);
297
298        group.add_node(make_electable("n1", 1));
299        group.add_node(make_electable("n2", 2));
300        group.add_node(make_monitor("m1", 3));
301        assert_eq!(group.electable_count(), 2);
302
303        group.add_node(make_arbiter("a1", 4));
304        assert_eq!(group.electable_count(), 3);
305    }
306
307    #[test]
308    fn test_quorum_size_empty() {
309        let group = RepGroup::new("g".to_string(), 1);
310        assert_eq!(group.quorum_size(), 0);
311    }
312
313    #[test]
314    fn test_quorum_size_one() {
315        let mut group = RepGroup::new("g".to_string(), 1);
316        group.add_node(make_electable("n1", 1));
317        // 1/2 + 1 = 1
318        assert_eq!(group.quorum_size(), 1);
319    }
320
321    #[test]
322    fn test_quorum_size_two() {
323        let mut group = RepGroup::new("g".to_string(), 1);
324        group.add_node(make_electable("n1", 1));
325        group.add_node(make_electable("n2", 2));
326        // 2/2 + 1 = 2
327        assert_eq!(group.quorum_size(), 2);
328    }
329
330    #[test]
331    fn test_quorum_size_three() {
332        let mut group = RepGroup::new("g".to_string(), 1);
333        group.add_node(make_electable("n1", 1));
334        group.add_node(make_electable("n2", 2));
335        group.add_node(make_electable("n3", 3));
336        // 3/2 + 1 = 2
337        assert_eq!(group.quorum_size(), 2);
338    }
339
340    #[test]
341    fn test_quorum_size_five() {
342        let mut group = RepGroup::new("g".to_string(), 1);
343        for i in 1..=5 {
344            group.add_node(make_electable(&format!("n{}", i), i));
345        }
346        // 5/2 + 1 = 3
347        assert_eq!(group.quorum_size(), 3);
348    }
349
350    #[test]
351    fn test_quorum_ignores_non_electable() {
352        let mut group = RepGroup::new("g".to_string(), 1);
353        group.add_node(make_electable("n1", 1));
354        group.add_node(make_electable("n2", 2));
355        group.add_node(make_electable("n3", 3));
356        group.add_node(make_monitor("m1", 4));
357        group.add_node(make_secondary("s1", 5));
358        // Only 3 electable: 3/2 + 1 = 2
359        assert_eq!(group.quorum_size(), 2);
360    }
361
362    #[test]
363    fn test_display() {
364        let mut group = RepGroup::new("mygroup".to_string(), 42);
365        group.add_node(make_electable("n1", 1));
366        let s = group.to_string();
367        assert!(s.contains("mygroup"));
368        assert!(s.contains("42"));
369        assert!(s.contains("1"));
370    }
371
372    #[test]
373    fn test_clone() {
374        let mut group = RepGroup::new("g".to_string(), 1);
375        group.add_node(make_electable("n1", 1));
376        let cloned = group.clone();
377        assert_eq!(cloned.name(), group.name());
378        assert_eq!(cloned.node_count(), group.node_count());
379    }
380
381    #[test]
382    fn test_get_node_not_found() {
383        let group = RepGroup::new("g".to_string(), 1);
384        assert!(group.get_node("nonexistent").is_none());
385    }
386}