laminar_core/cluster/discovery/
mod.rs1#![allow(clippy::disallowed_types)] mod static_discovery;
6pub use static_discovery::{StaticDiscovery, StaticDiscoveryConfig};
7
8mod gossip_discovery;
9pub use gossip_discovery::{keys, GossipDiscovery, GossipDiscoveryConfig};
10
11use std::collections::HashMap;
12use std::fmt;
13
14use serde::{Deserialize, Serialize};
15use tokio::sync::watch;
16
17pub use crate::state::NodeId;
18
19pub(crate) fn publish_if_changed(tx: &watch::Sender<Vec<NodeInfo>>, mut peer_list: Vec<NodeInfo>) {
23 peer_list.sort_by_key(|n| n.id.0); tx.send_if_modified(|cur| {
25 fn same_member(a: &NodeInfo, b: &NodeInfo) -> bool {
27 let NodeInfo {
30 id,
31 name,
32 rpc_address,
33 raft_address,
34 state,
35 metadata,
36 last_heartbeat_ms: _,
37 } = a;
38 id == &b.id
39 && name == &b.name
40 && rpc_address == &b.rpc_address
41 && raft_address == &b.raft_address
42 && state == &b.state
43 && metadata == &b.metadata
44 }
45 let same = cur.len() == peer_list.len()
46 && cur.iter().zip(&peer_list).all(|(a, b)| same_member(a, b));
47 if same {
48 false
49 } else {
50 *cur = peer_list;
51 true
52 }
53 });
54}
55
56#[derive(
58 Debug,
59 Clone,
60 Copy,
61 PartialEq,
62 Eq,
63 Serialize,
64 Deserialize,
65 rkyv::Archive,
66 rkyv::Serialize,
67 rkyv::Deserialize,
68)]
69pub enum NodeState {
70 Joining,
72 Active,
74 Suspected,
76 Draining,
78 Left,
80}
81
82impl fmt::Display for NodeState {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Joining => write!(f, "joining"),
86 Self::Active => write!(f, "active"),
87 Self::Suspected => write!(f, "suspected"),
88 Self::Draining => write!(f, "draining"),
89 Self::Left => write!(f, "left"),
90 }
91 }
92}
93
94#[derive(
96 Debug,
97 Clone,
98 PartialEq,
99 Serialize,
100 Deserialize,
101 rkyv::Archive,
102 rkyv::Serialize,
103 rkyv::Deserialize,
104)]
105pub struct NodeMetadata {
106 pub cores: u32,
108 pub memory_bytes: u64,
110 pub failure_domain: Option<String>,
112 pub tags: HashMap<String, String>,
114 pub version: String,
116}
117
118impl Default for NodeMetadata {
119 fn default() -> Self {
120 Self {
121 cores: 1,
122 memory_bytes: 0,
123 failure_domain: None,
124 tags: HashMap::new(),
125 version: String::new(),
126 }
127 }
128}
129
130#[derive(
132 Debug,
133 Clone,
134 PartialEq,
135 Serialize,
136 Deserialize,
137 rkyv::Archive,
138 rkyv::Serialize,
139 rkyv::Deserialize,
140)]
141pub struct NodeInfo {
142 pub id: NodeId,
144 pub name: String,
146 pub rpc_address: String,
148 pub raft_address: String,
150 pub state: NodeState,
152 pub metadata: NodeMetadata,
154 pub last_heartbeat_ms: i64,
156}
157
158#[must_use]
162pub fn assignable_node_ids(members: &[NodeInfo]) -> Vec<NodeId> {
163 let mut ids: Vec<NodeId> = members
164 .iter()
165 .filter(|m| matches!(m.state, NodeState::Active))
166 .map(|m| m.id)
167 .filter(|id| !id.is_unassigned())
168 .collect();
169 ids.sort_unstable();
170 ids.dedup();
171 ids
172}
173
174#[derive(Debug, Clone)]
176pub enum MembershipEvent {
177 NodeJoined(Box<NodeInfo>),
179 NodeStateChanged {
181 node_id: NodeId,
183 old_state: NodeState,
185 new_state: NodeState,
187 },
188 NodeLeft(NodeId),
190}
191
192#[derive(Debug, thiserror::Error)]
194pub enum DiscoveryError {
195 #[error("bind error: {0}")]
197 Bind(String),
198
199 #[error("connection error to {address}: {reason}")]
201 Connection {
202 address: String,
204 reason: String,
206 },
207
208 #[error("serialization error: {0}")]
210 Serialization(String),
211
212 #[error("discovery not started")]
214 NotStarted,
215
216 #[error("discovery shut down")]
218 ShutDown,
219
220 #[error("I/O error: {0}")]
222 Io(#[from] std::io::Error),
223}
224
225#[allow(async_fn_in_trait)]
231pub trait Discovery: Send + Sync + 'static {
232 async fn start(&mut self) -> Result<(), DiscoveryError>;
236
237 async fn peers(&self) -> Result<Vec<NodeInfo>, DiscoveryError>;
239
240 async fn announce(&self, info: NodeInfo) -> Result<(), DiscoveryError>;
242
243 fn membership_watch(&self) -> watch::Receiver<Vec<NodeInfo>>;
248
249 async fn stop(&mut self) -> Result<(), DiscoveryError>;
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_node_id_display() {
259 assert_eq!(NodeId(42).to_string(), "node-42");
260 }
261
262 #[test]
263 fn test_node_id_unassigned() {
264 assert!(NodeId::UNASSIGNED.is_unassigned());
265 assert!(!NodeId(1).is_unassigned());
266 }
267
268 #[test]
269 fn test_node_state_display() {
270 assert_eq!(NodeState::Active.to_string(), "active");
271 assert_eq!(NodeState::Suspected.to_string(), "suspected");
272 assert_eq!(NodeState::Draining.to_string(), "draining");
273 }
274
275 fn info_with(id: u64, state: NodeState) -> NodeInfo {
276 NodeInfo {
277 id: NodeId(id),
278 name: format!("n{id}"),
279 rpc_address: String::new(),
280 raft_address: String::new(),
281 state,
282 metadata: NodeMetadata::default(),
283 last_heartbeat_ms: 0,
284 }
285 }
286
287 #[test]
288 fn assignable_includes_only_active_sorted_deduped() {
289 let members = vec![
290 info_with(5, NodeState::Active),
291 info_with(2, NodeState::Joining),
292 info_with(3, NodeState::Suspected),
293 info_with(4, NodeState::Draining),
294 info_with(6, NodeState::Left),
295 info_with(1, NodeState::Active),
296 info_with(1, NodeState::Active), ];
298 assert_eq!(assignable_node_ids(&members), vec![NodeId(1), NodeId(5)]);
299 }
300
301 #[test]
302 fn assignable_drops_unassigned() {
303 let mut unassigned = info_with(7, NodeState::Active);
304 unassigned.id = NodeId::UNASSIGNED;
305 let members = vec![unassigned, info_with(7, NodeState::Active)];
306 assert_eq!(assignable_node_ids(&members), vec![NodeId(7)]);
307 }
308
309 #[test]
310 fn test_node_metadata_default() {
311 let meta = NodeMetadata::default();
312 assert_eq!(meta.cores, 1);
313 assert_eq!(meta.memory_bytes, 0);
314 assert!(meta.failure_domain.is_none());
315 assert!(meta.tags.is_empty());
316 }
317
318 #[test]
319 fn test_node_id_serialization() {
320 let id = NodeId(123);
321 let json = serde_json::to_string(&id).unwrap();
322 let back: NodeId = serde_json::from_str(&json).unwrap();
323 assert_eq!(id, back);
324 }
325
326 #[test]
327 fn test_node_info_serialization() {
328 let info = NodeInfo {
329 id: NodeId(1),
330 name: "test-node".into(),
331 rpc_address: "127.0.0.1:9000".into(),
332 raft_address: "127.0.0.1:9001".into(),
333 state: NodeState::Active,
334 metadata: NodeMetadata::default(),
335 last_heartbeat_ms: 1000,
336 };
337 let json = serde_json::to_string(&info).unwrap();
338 let back: NodeInfo = serde_json::from_str(&json).unwrap();
339 assert_eq!(back.id, info.id);
340 assert_eq!(back.name, "test-node");
341 }
342}