//! Membership state management use crate::identity::GossipId; use chainfire_types::NodeId; use dashmap::DashMap; use std::net::SocketAddr; use tracing::debug; /// Membership change event #[derive(Debug, Clone)] pub enum MembershipChange { /// A member joined or became reachable MemberUp(GossipId), /// A member left or became unreachable MemberDown(GossipId), } /// Manages cluster membership state pub struct MembershipState { /// Known members members: DashMap, } impl MembershipState { /// Create a new membership state pub fn new() -> Self { Self { members: DashMap::new(), } } /// Handle a membership change pub fn handle_change(&self, change: MembershipChange) { match change { MembershipChange::MemberUp(id) => { debug!(node_id = id.node_id, addr = %id.addr, "Adding member"); self.members.insert(id.node_id, id); } MembershipChange::MemberDown(id) => { debug!(node_id = id.node_id, "Removing member"); self.members.remove(&id.node_id); } } } /// Get a member by node ID pub fn get(&self, node_id: NodeId) -> Option { self.members.get(&node_id).map(|r| r.clone()) } /// Get all members pub fn all(&self) -> Vec { self.members.iter().map(|r| r.clone()).collect() } /// Get member count pub fn count(&self) -> usize { self.members.len() } /// Check if a node is a member pub fn contains(&self, node_id: NodeId) -> bool { self.members.contains_key(&node_id) } /// Get all member addresses pub fn addresses(&self) -> Vec { self.members.iter().map(|r| r.addr).collect() } /// Get all control plane members pub fn control_plane_members(&self) -> Vec { self.members .iter() .filter(|r| r.is_control_plane()) .map(|r| r.clone()) .collect() } /// Get all worker members pub fn worker_members(&self) -> Vec { self.members .iter() .filter(|r| r.is_worker()) .map(|r| r.clone()) .collect() } } impl Default for MembershipState { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use chainfire_types::node::NodeRole; fn create_id(node_id: NodeId, role: NodeRole) -> GossipId { GossipId::new( node_id, format!("127.0.0.1:{}", 5000 + node_id).parse().unwrap(), role, ) } #[test] fn test_membership_changes() { let state = MembershipState::new(); let id1 = create_id(1, NodeRole::ControlPlane); let id2 = create_id(2, NodeRole::Worker); state.handle_change(MembershipChange::MemberUp(id1.clone())); state.handle_change(MembershipChange::MemberUp(id2.clone())); assert_eq!(state.count(), 2); assert!(state.contains(1)); assert!(state.contains(2)); state.handle_change(MembershipChange::MemberDown(id1)); assert_eq!(state.count(), 1); assert!(!state.contains(1)); } #[test] fn test_role_filtering() { let state = MembershipState::new(); state.handle_change(MembershipChange::MemberUp(create_id(1, NodeRole::ControlPlane))); state.handle_change(MembershipChange::MemberUp(create_id(2, NodeRole::ControlPlane))); state.handle_change(MembershipChange::MemberUp(create_id(3, NodeRole::Worker))); state.handle_change(MembershipChange::MemberUp(create_id(4, NodeRole::Worker))); state.handle_change(MembershipChange::MemberUp(create_id(5, NodeRole::Worker))); assert_eq!(state.control_plane_members().len(), 2); assert_eq!(state.worker_members().len(), 3); } }