1#![allow(dead_code)]
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum NodeRole {
11 Leader,
13 Follower,
15 Candidate,
17 Observer,
19}
20
21impl NodeRole {
22 #[must_use]
24 pub fn can_vote(&self) -> bool {
25 matches!(self, Self::Leader | Self::Follower | Self::Candidate)
26 }
27
28 #[must_use]
30 pub fn is_leader(&self) -> bool {
31 matches!(self, Self::Leader)
32 }
33
34 #[must_use]
36 pub fn name(&self) -> &str {
37 match self {
38 Self::Leader => "Leader",
39 Self::Follower => "Follower",
40 Self::Candidate => "Candidate",
41 Self::Observer => "Observer",
42 }
43 }
44}
45
46impl std::fmt::Display for NodeRole {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(f, "{}", self.name())
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum NodeHealth {
55 Healthy,
57 Degraded,
59 Unreachable,
61 Draining,
63}
64
65impl NodeHealth {
66 #[must_use]
68 pub fn is_active(&self) -> bool {
69 matches!(self, Self::Healthy | Self::Degraded)
70 }
71
72 #[must_use]
74 pub fn description(&self) -> &str {
75 match self {
76 Self::Healthy => "Healthy",
77 Self::Degraded => "Degraded",
78 Self::Unreachable => "Unreachable",
79 Self::Draining => "Draining",
80 }
81 }
82}
83
84impl std::fmt::Display for NodeHealth {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.description())
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct ClusterNode {
93 pub node_id: String,
95 pub address: String,
97 pub role: NodeRole,
99 pub health: NodeHealth,
101 pub last_heartbeat_epoch: u64,
103}
104
105impl ClusterNode {
106 #[must_use]
108 pub fn new(
109 node_id: impl Into<String>,
110 address: impl Into<String>,
111 role: NodeRole,
112 health: NodeHealth,
113 last_heartbeat_epoch: u64,
114 ) -> Self {
115 Self {
116 node_id: node_id.into(),
117 address: address.into(),
118 role,
119 health,
120 last_heartbeat_epoch,
121 }
122 }
123
124 #[must_use]
126 pub fn is_stale(&self, now_epoch: u64, timeout_secs: u64) -> bool {
127 now_epoch.saturating_sub(self.last_heartbeat_epoch) > timeout_secs
128 }
129
130 #[must_use]
132 pub fn can_vote(&self) -> bool {
133 self.role.can_vote()
134 }
135
136 #[must_use]
138 pub fn is_active(&self) -> bool {
139 self.health.is_active()
140 }
141}
142
143#[derive(Debug, Default)]
145pub struct ClusterTopology {
146 pub nodes: Vec<ClusterNode>,
148}
149
150impl ClusterTopology {
151 #[must_use]
153 pub fn new() -> Self {
154 Self { nodes: Vec::new() }
155 }
156
157 pub fn add_node(&mut self, node: ClusterNode) {
161 if let Some(existing) = self.nodes.iter_mut().find(|n| n.node_id == node.node_id) {
162 *existing = node;
163 } else {
164 self.nodes.push(node);
165 }
166 }
167
168 #[must_use]
170 pub fn find_leader(&self) -> Option<&ClusterNode> {
171 self.nodes.iter().find(|n| n.role.is_leader())
172 }
173
174 #[must_use]
176 pub fn healthy_nodes(&self) -> Vec<&ClusterNode> {
177 self.nodes.iter().filter(|n| n.health.is_active()).collect()
178 }
179
180 #[must_use]
184 pub fn quorum_size(&self) -> usize {
185 let voters = self.nodes.iter().filter(|n| n.can_vote()).count();
186 voters / 2 + 1
187 }
188
189 #[must_use]
191 pub fn has_quorum(&self) -> bool {
192 let healthy_voters = self
193 .nodes
194 .iter()
195 .filter(|n| n.can_vote() && n.health.is_active())
196 .count();
197 healthy_voters >= self.quorum_size()
198 }
199
200 #[must_use]
202 pub fn node_count(&self) -> usize {
203 self.nodes.len()
204 }
205
206 pub fn remove_node(&mut self, node_id: &str) -> bool {
210 let before = self.nodes.len();
211 self.nodes.retain(|n| n.node_id != node_id);
212 self.nodes.len() < before
213 }
214
215 #[must_use]
217 pub fn find_by_id(&self, node_id: &str) -> Option<&ClusterNode> {
218 self.nodes.iter().find(|n| n.node_id == node_id)
219 }
220
221 #[must_use]
223 pub fn stale_nodes(&self, now_epoch: u64, timeout_secs: u64) -> Vec<&ClusterNode> {
224 self.nodes
225 .iter()
226 .filter(|n| n.is_stale(now_epoch, timeout_secs))
227 .collect()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn make_node(id: &str, role: NodeRole, health: NodeHealth, ts: u64) -> ClusterNode {
236 ClusterNode::new(id, format!("10.0.0.1:{}", id), role, health, ts)
237 }
238
239 #[test]
240 fn test_node_role_can_vote() {
241 assert!(NodeRole::Leader.can_vote());
242 assert!(NodeRole::Follower.can_vote());
243 assert!(NodeRole::Candidate.can_vote());
244 assert!(!NodeRole::Observer.can_vote());
245 }
246
247 #[test]
248 fn test_node_role_is_leader() {
249 assert!(NodeRole::Leader.is_leader());
250 assert!(!NodeRole::Follower.is_leader());
251 assert!(!NodeRole::Candidate.is_leader());
252 assert!(!NodeRole::Observer.is_leader());
253 }
254
255 #[test]
256 fn test_node_role_display() {
257 assert_eq!(NodeRole::Leader.to_string(), "Leader");
258 assert_eq!(NodeRole::Follower.to_string(), "Follower");
259 assert_eq!(NodeRole::Observer.to_string(), "Observer");
260 }
261
262 #[test]
263 fn test_node_health_is_active() {
264 assert!(NodeHealth::Healthy.is_active());
265 assert!(NodeHealth::Degraded.is_active());
266 assert!(!NodeHealth::Unreachable.is_active());
267 assert!(!NodeHealth::Draining.is_active());
268 }
269
270 #[test]
271 fn test_node_health_display() {
272 assert_eq!(NodeHealth::Healthy.to_string(), "Healthy");
273 assert_eq!(NodeHealth::Unreachable.to_string(), "Unreachable");
274 }
275
276 #[test]
277 fn test_cluster_node_is_stale() {
278 let node = make_node("n1", NodeRole::Follower, NodeHealth::Healthy, 1000);
279 assert!(!node.is_stale(1020, 30)); assert!(node.is_stale(1031, 30)); }
283
284 #[test]
285 fn test_cluster_topology_add_node() {
286 let mut topo = ClusterTopology::new();
287 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
288 topo.add_node(make_node(
289 "n2",
290 NodeRole::Follower,
291 NodeHealth::Healthy,
292 100,
293 ));
294 assert_eq!(topo.node_count(), 2);
295 }
296
297 #[test]
298 fn test_cluster_topology_add_node_replaces_existing() {
299 let mut topo = ClusterTopology::new();
300 topo.add_node(make_node(
301 "n1",
302 NodeRole::Follower,
303 NodeHealth::Healthy,
304 100,
305 ));
306 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Degraded, 200));
307 assert_eq!(topo.node_count(), 1);
308 assert_eq!(topo.nodes[0].role, NodeRole::Leader);
309 }
310
311 #[test]
312 fn test_cluster_topology_find_leader() {
313 let mut topo = ClusterTopology::new();
314 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
315 topo.add_node(make_node(
316 "n2",
317 NodeRole::Follower,
318 NodeHealth::Healthy,
319 100,
320 ));
321
322 let leader = topo.find_leader();
323 assert!(leader.is_some());
324 assert_eq!(leader.expect("leader should exist").node_id, "n1");
325 }
326
327 #[test]
328 fn test_cluster_topology_no_leader() {
329 let mut topo = ClusterTopology::new();
330 topo.add_node(make_node(
331 "n1",
332 NodeRole::Follower,
333 NodeHealth::Healthy,
334 100,
335 ));
336 assert!(topo.find_leader().is_none());
337 }
338
339 #[test]
340 fn test_cluster_topology_healthy_nodes() {
341 let mut topo = ClusterTopology::new();
342 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
343 topo.add_node(make_node(
344 "n2",
345 NodeRole::Follower,
346 NodeHealth::Degraded,
347 100,
348 ));
349 topo.add_node(make_node(
350 "n3",
351 NodeRole::Follower,
352 NodeHealth::Unreachable,
353 100,
354 ));
355
356 let healthy = topo.healthy_nodes();
357 assert_eq!(healthy.len(), 2);
358 }
359
360 #[test]
361 fn test_cluster_topology_quorum_size() {
362 let mut topo = ClusterTopology::new();
363 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
365 topo.add_node(make_node(
366 "n2",
367 NodeRole::Follower,
368 NodeHealth::Healthy,
369 100,
370 ));
371 topo.add_node(make_node(
372 "n3",
373 NodeRole::Follower,
374 NodeHealth::Healthy,
375 100,
376 ));
377 topo.add_node(make_node(
379 "n4",
380 NodeRole::Observer,
381 NodeHealth::Healthy,
382 100,
383 ));
384
385 assert_eq!(topo.quorum_size(), 2);
386 }
387
388 #[test]
389 fn test_cluster_topology_has_quorum() {
390 let mut topo = ClusterTopology::new();
391 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
392 topo.add_node(make_node(
393 "n2",
394 NodeRole::Follower,
395 NodeHealth::Healthy,
396 100,
397 ));
398 topo.add_node(make_node(
399 "n3",
400 NodeRole::Follower,
401 NodeHealth::Unreachable,
402 100,
403 ));
404
405 assert!(topo.has_quorum());
407 }
408
409 #[test]
410 fn test_cluster_topology_no_quorum() {
411 let mut topo = ClusterTopology::new();
412 topo.add_node(make_node(
413 "n1",
414 NodeRole::Leader,
415 NodeHealth::Unreachable,
416 100,
417 ));
418 topo.add_node(make_node(
419 "n2",
420 NodeRole::Follower,
421 NodeHealth::Unreachable,
422 100,
423 ));
424 topo.add_node(make_node(
425 "n3",
426 NodeRole::Follower,
427 NodeHealth::Healthy,
428 100,
429 ));
430
431 assert!(!topo.has_quorum());
433 }
434
435 #[test]
436 fn test_cluster_topology_remove_node() {
437 let mut topo = ClusterTopology::new();
438 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 100));
439 topo.add_node(make_node(
440 "n2",
441 NodeRole::Follower,
442 NodeHealth::Healthy,
443 100,
444 ));
445
446 assert!(topo.remove_node("n1"));
447 assert_eq!(topo.node_count(), 1);
448 assert!(!topo.remove_node("MISSING"));
449 }
450
451 #[test]
452 fn test_cluster_topology_stale_nodes() {
453 let mut topo = ClusterTopology::new();
454 topo.add_node(make_node("n1", NodeRole::Leader, NodeHealth::Healthy, 1000));
455 topo.add_node(make_node(
456 "n2",
457 NodeRole::Follower,
458 NodeHealth::Healthy,
459 900,
460 ));
461
462 let stale = topo.stale_nodes(1025, 30);
464 assert_eq!(stale.len(), 1);
465 assert_eq!(stale[0].node_id, "n2");
466 }
467}