Skip to main content

nodedb_cluster/multi_raft/
membership.rs

1// SPDX-License-Identifier: BUSL-1.1
2
3//! Group-level membership helpers consumed by the tick loop's join /
4//! promotion phases.
5//!
6//! - `commit_index_for(group)`: used by the join flow to wait until a
7//!   proposed `AddLearner` conf-change commits before replying to the
8//!   joining node.
9//! - `ready_learners(group)`: used by the tick loop's "promote
10//!   caught-up learners" phase — returns every learner in the group
11//!   whose `match_index` on this (leader) node is at least the current
12//!   `commit_index`, i.e. learners that have replicated enough log to be
13//!   safely promoted.
14//! - `group_leader(group)`: leader id observed by this node's local
15//!   RaftNode state, used by the join flow to decide redirect vs admit.
16//! - `group_role_is_leader(group)`: cheap leader-check helper.
17
18use nodedb_raft::NodeRole;
19
20use crate::error::{ClusterError, Result};
21
22use super::core::MultiRaft;
23
24impl MultiRaft {
25    /// Whether a node is already admitted as a voter or learner.
26    pub fn group_contains_node(&self, group_id: u64, node_id: u64) -> Option<bool> {
27        let membership = self.group_membership(group_id)?;
28        Some(membership.voters.contains(&node_id) || membership.learners.contains(&node_id))
29    }
30
31    /// Current commit index for a group, or `None` if the group is not
32    /// hosted on this node.
33    pub fn commit_index_for(&self, group_id: u64) -> Option<u64> {
34        self.groups.get(&group_id).map(|n| n.commit_index())
35    }
36
37    /// Learners in `group_id` whose `match_index` on this leader has
38    /// caught up to the current `commit_index` — safe to promote.
39    ///
40    /// Returns an empty vec if this node is not the leader of the group
41    /// or the group is not hosted here.
42    pub fn ready_learners(&self, group_id: u64) -> Vec<u64> {
43        let Some(node) = self.groups.get(&group_id) else {
44            return Vec::new();
45        };
46        if node.role() != NodeRole::Leader {
47            return Vec::new();
48        }
49        let commit = node.commit_index();
50        node.learners()
51            .iter()
52            .copied()
53            .filter(|&learner| node.match_index_for(learner).unwrap_or(0) >= commit)
54            .collect()
55    }
56
57    /// Observed leader id for a group (0 = unknown / no election yet).
58    pub fn group_leader(&self, group_id: u64) -> u64 {
59        self.groups
60            .get(&group_id)
61            .map(|n| n.leader_id())
62            .unwrap_or(0)
63    }
64
65    /// Whether this node is currently the leader of `group_id`.
66    pub fn group_role_is_leader(&self, group_id: u64) -> bool {
67        self.groups
68            .get(&group_id)
69            .map(|n| n.role() == NodeRole::Leader)
70            .unwrap_or(false)
71    }
72
73    /// Initiate a leadership transfer for `group_id` to `target`.
74    ///
75    /// Delegates to `RaftNode::transfer_leadership`. Returns
76    /// [`ClusterError::GroupNotFound`] if the group is not hosted on this node.
77    /// The outbound `TimeoutNow` trigger is emitted into the group's `Ready`
78    /// output and dispatched by the next tick.
79    pub fn transfer_leadership(&mut self, group_id: u64, target: u64) -> Result<()> {
80        let node = self
81            .groups
82            .get_mut(&group_id)
83            .ok_or(ClusterError::GroupNotFound { group_id })?;
84        node.transfer_leadership(target).map_err(ClusterError::Raft)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use std::time::{Duration, Instant};
91
92    use super::*;
93    use crate::routing::RoutingTable;
94
95    fn make_multi_raft_single_node(node_id: u64) -> MultiRaft {
96        let dir = tempfile::tempdir().unwrap();
97        let rt = RoutingTable::uniform(1, &[node_id], 1);
98        let mut mr = MultiRaft::new(node_id, rt, dir.path().to_path_buf());
99        mr.add_group(0, vec![]).unwrap();
100        mr
101    }
102
103    #[test]
104    fn transfer_leadership_group_not_found() {
105        let mut mr = make_multi_raft_single_node(1);
106        let err = mr.transfer_leadership(999, 2).unwrap_err();
107        assert!(
108            matches!(err, ClusterError::GroupNotFound { group_id: 999 }),
109            "expected GroupNotFound, got {err:?}"
110        );
111    }
112
113    #[test]
114    fn transfer_leadership_delegates_to_raft_node() {
115        let mut mr = make_multi_raft_single_node(1);
116        // Force election so node 1 becomes the leader of group 0.
117        if let Some(node) = mr.groups_mut().get_mut(&0) {
118            node.election_deadline_override(Instant::now() - Duration::from_millis(1));
119            node.tick();
120            // Single-voter group: node 1 is immediately leader after one tick.
121        }
122        // Node 1 is a single-voter group — it is leader, but transfer to self
123        // is rejected with InvalidTransferTarget, which confirms delegation.
124        let err = mr.transfer_leadership(0, 1).unwrap_err();
125        assert!(
126            matches!(err, ClusterError::Raft(_)),
127            "expected Raft error from transfer_leadership, got {err:?}"
128        );
129    }
130}