1use std::collections::{HashMap, VecDeque};
9use std::net::SocketAddr;
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13
14use arc_swap::ArcSwap;
15
16pub use crate::protocol::NodeResources;
17
18const NODE_HISTORY_CAP: usize = 32;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum NodeStatus {
23 #[default]
25 Active,
26 Degraded,
28 Failed,
30}
31
32#[derive(Clone, Debug)]
34pub struct NodeMetrics {
35 pub name: String,
37 pub status: NodeStatus,
39 pub vram_gb: f32,
41 pub total_vram_gb: f32,
43 pub system_memory_gb: f32,
45 pub compute_capability: String,
47 pub gpu_name: Option<String>,
49 pub ip_address: Option<SocketAddr>,
51
52 pub last_heartbeat: Instant,
54 pub heartbeat_timeout: Duration,
56
57 pub avg_latency_us: f32,
59 pub min_latency_us: f32,
61 pub max_latency_us: f32,
63 pub latency_samples: u64,
65
66 pub delivery_ratio: f32,
68 pub throughput_gbps: f32,
70
71 pub latency_history_us: VecDeque<f32>,
73 pub throughput_history_gbps: VecDeque<f32>,
75
76 pub used_vram_gb: f32,
78 pub available_vram_gb: f32,
80
81 pub streaming_layers: Option<(usize, usize)>,
83
84 pub af_xdp_gbps: f32,
86 pub latency_micros: f32,
88 pub(crate) delivery_ratio_initialized: bool,
90}
91
92impl Default for NodeMetrics {
93 fn default() -> Self {
94 Self {
95 name: String::new(),
96 status: NodeStatus::Active,
97 vram_gb: 0.0,
98 total_vram_gb: 0.0,
99 system_memory_gb: 0.0,
100 compute_capability: String::new(),
101 gpu_name: None,
102 last_heartbeat: Instant::now(),
103 heartbeat_timeout: Duration::from_secs(5),
104 avg_latency_us: 0.0,
105 min_latency_us: f32::MAX,
106 max_latency_us: 0.0,
107 latency_samples: 0,
108 delivery_ratio: 1.0,
109 throughput_gbps: 0.0,
110 latency_history_us: VecDeque::with_capacity(NODE_HISTORY_CAP),
111 throughput_history_gbps: VecDeque::with_capacity(NODE_HISTORY_CAP),
112 used_vram_gb: 0.0,
113 available_vram_gb: 0.0,
114 streaming_layers: None,
115 af_xdp_gbps: 0.0,
116 latency_micros: 0.0,
117 delivery_ratio_initialized: false,
118 ip_address: None,
119 }
120 }
121}
122
123impl NodeMetrics {
124 pub fn new(
126 vram_gb: f32,
127 system_memory_gb: f32,
128 compute_capability: String,
129 heartbeat_timeout: Duration,
130 ) -> Self {
131 Self {
132 name: String::new(),
133 status: NodeStatus::Active,
134 vram_gb,
135 total_vram_gb: vram_gb,
136 system_memory_gb,
137 compute_capability,
138 gpu_name: None,
139 ip_address: None,
140 last_heartbeat: Instant::now(),
141 heartbeat_timeout,
142 avg_latency_us: 0.0,
143 min_latency_us: f32::MAX,
144 max_latency_us: 0.0,
145 latency_samples: 0,
146 delivery_ratio: 1.0,
147 throughput_gbps: 0.0,
148 latency_history_us: VecDeque::with_capacity(NODE_HISTORY_CAP),
149 throughput_history_gbps: VecDeque::with_capacity(NODE_HISTORY_CAP),
150 used_vram_gb: 0.0,
151 available_vram_gb: vram_gb,
152 streaming_layers: None,
153 af_xdp_gbps: 0.0,
154 latency_micros: 0.0,
155 delivery_ratio_initialized: false,
156 }
157 }
158
159 pub fn record_latency(&mut self, latency_us: f32) {
161 if latency_us < self.min_latency_us {
162 self.min_latency_us = latency_us;
163 }
164 if latency_us > self.max_latency_us {
165 self.max_latency_us = latency_us;
166 }
167
168 self.latency_samples += 1;
169 if self.latency_samples == 1 {
170 self.avg_latency_us = latency_us;
171 } else {
172 self.avg_latency_us = self.avg_latency_us * 0.9 + latency_us * 0.1;
173 }
174
175 if self.latency_history_us.len() >= NODE_HISTORY_CAP {
176 self.latency_history_us.pop_front();
177 }
178 self.latency_history_us.push_back(latency_us);
179 }
180
181 pub fn record_delivery_ratio(&mut self, ratio: f32) {
183 if !self.delivery_ratio_initialized {
184 self.delivery_ratio = ratio;
185 self.delivery_ratio_initialized = true;
186 } else {
187 self.delivery_ratio = self.delivery_ratio * 0.9 + ratio * 0.1;
189 }
190 }
191
192 pub fn record_throughput(&mut self, throughput_gbps: f32) {
194 self.throughput_gbps = self.throughput_gbps * 0.9 + throughput_gbps * 0.1;
196
197 if self.throughput_history_gbps.len() >= NODE_HISTORY_CAP {
198 self.throughput_history_gbps.pop_front();
199 }
200 self.throughput_history_gbps.push_back(throughput_gbps);
201 }
202
203 pub fn record_vram_usage(&mut self, used_vram_gb: f32) {
205 self.used_vram_gb = used_vram_gb;
206 self.available_vram_gb = self.total_vram_gb - used_vram_gb;
207 }
208
209 pub fn set_streaming_layers(&mut self, start: usize, end: usize) {
211 self.streaming_layers = Some((start, end));
212 }
213
214 pub fn clear_streaming_layers(&mut self) {
216 self.streaming_layers = None;
217 }
218}
219
220#[derive(Debug)]
222pub struct ClusterState {
223 nodes: Arc<Mutex<HashMap<String, NodeResources>>>,
225 nodes_snapshot: Arc<ArcSwap<Vec<NodeResources>>>,
227 nodes_snapshot_dirty: Arc<AtomicBool>,
229 pub(crate) metrics: Arc<Mutex<HashMap<String, NodeMetrics>>>,
231 last_update: Arc<AtomicU64>,
233 total_vram_cache: Arc<AtomicU64>,
235 total_system_memory_cache: Arc<AtomicU64>,
237 circuit_breakers: Arc<Mutex<HashMap<String, crate::circuit_breaker::CircuitBreaker>>>,
243}
244
245impl Clone for ClusterState {
246 fn clone(&self) -> Self {
247 Self {
248 nodes: Arc::clone(&self.nodes),
249 nodes_snapshot: Arc::clone(&self.nodes_snapshot),
250 nodes_snapshot_dirty: Arc::clone(&self.nodes_snapshot_dirty),
251 metrics: Arc::clone(&self.metrics),
252 last_update: Arc::clone(&self.last_update),
253 total_vram_cache: Arc::clone(&self.total_vram_cache),
254 total_system_memory_cache: Arc::clone(&self.total_system_memory_cache),
255 circuit_breakers: Arc::clone(&self.circuit_breakers),
256 }
257 }
258}
259
260impl Default for ClusterState {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266impl ClusterState {
267 pub fn new() -> Self {
269 Self {
270 nodes: Arc::new(Mutex::new(HashMap::new())),
271 nodes_snapshot: Arc::new(ArcSwap::from_pointee(Vec::<NodeResources>::new())),
272 nodes_snapshot_dirty: Arc::new(AtomicBool::new(false)),
273 metrics: Arc::new(Mutex::new(HashMap::new())),
274 last_update: Arc::new(AtomicU64::new(0)),
275 total_vram_cache: Arc::new(AtomicU64::new(0.0_f64.to_bits())),
276 total_system_memory_cache: Arc::new(AtomicU64::new(0.0_f64.to_bits())),
277 circuit_breakers: Arc::new(Mutex::new(HashMap::new())),
278 }
279 }
280
281 pub fn circuit_breaker_for(&self, node_id: &str) -> crate::circuit_breaker::CircuitBreaker {
287 let mut breakers = self
288 .circuit_breakers
289 .lock()
290 .unwrap_or_else(|poison| poison.into_inner());
291 breakers.entry(node_id.to_string()).or_default().clone()
292 }
293
294 pub fn register(&self, node: NodeResources) {
296 self.register_with_addr(node, None);
297 }
298
299 pub fn register_with_addr(&self, node: NodeResources, addr: Option<SocketAddr>) {
301 let mut nodes = self
302 .nodes
303 .lock()
304 .unwrap_or_else(|poison| poison.into_inner());
305 let mut metrics = self
306 .metrics
307 .lock()
308 .unwrap_or_else(|poison| poison.into_inner());
309
310 let vram_gb = node.vram_gb;
315 let system_memory_gb = node.system_memory_gb;
316 let mut vram_delta = vram_gb;
317 let mut system_memory_delta = system_memory_gb;
318 let mut resources_changed = true;
319
320 if let Some(existing_metrics) = metrics.get_mut(&node.id) {
325 existing_metrics.vram_gb = vram_gb;
326 existing_metrics.total_vram_gb = vram_gb;
327 existing_metrics.system_memory_gb = system_memory_gb;
328 if existing_metrics.compute_capability != node.compute_capability {
329 existing_metrics
330 .compute_capability
331 .clone_from(&node.compute_capability);
332 }
333 if existing_metrics.gpu_name != node.gpu_name {
334 existing_metrics.gpu_name.clone_from(&node.gpu_name);
335 }
336 existing_metrics.heartbeat_timeout = Duration::from_secs(5);
337 if addr.is_some() {
338 existing_metrics.ip_address = addr;
339 }
340 } else {
341 let node_id = node.id.clone();
342 let mut node_metrics = NodeMetrics::new(
343 vram_gb,
344 system_memory_gb,
345 node.compute_capability.clone(),
346 Duration::from_secs(5),
347 );
348 node_metrics.name = node_id.clone();
349 node_metrics.gpu_name = node.gpu_name.clone();
350 node_metrics.ip_address = addr;
351 metrics.insert(node_id, node_metrics);
352 }
353
354 if let Some(existing) = nodes.get_mut(&node.id) {
355 vram_delta = vram_gb - existing.vram_gb;
356 system_memory_delta = system_memory_gb - existing.system_memory_gb;
357
358 if existing.vram_gb == node.vram_gb
360 && existing.system_memory_gb == node.system_memory_gb
361 && existing.compute_capability == node.compute_capability
362 && existing.gpu_name == node.gpu_name
363 && existing.rpc_port == node.rpc_port
364 && existing.rpc_build_id == node.rpc_build_id
365 {
366 resources_changed = false;
367 } else {
368 *existing = node; }
370 } else {
371 let node_id = node.id.clone();
372 nodes.insert(node_id, node); }
374
375 if resources_changed {
376 self.nodes_snapshot_dirty.store(true, Ordering::Release);
377 }
378
379 if vram_delta != 0.0 {
380 let current_total_vram = f64::from_bits(self.total_vram_cache.load(Ordering::Acquire));
381 self.total_vram_cache.store(
382 (current_total_vram + vram_delta as f64).to_bits(),
383 Ordering::Release,
384 );
385 }
386
387 if system_memory_delta != 0.0 {
388 let current_total_system_mem =
389 f64::from_bits(self.total_system_memory_cache.load(Ordering::Acquire));
390 self.total_system_memory_cache.store(
391 (current_total_system_mem + system_memory_delta as f64).to_bits(),
392 Ordering::Release,
393 );
394 }
395
396 self.last_update.store(
397 std::time::SystemTime::now()
398 .duration_since(std::time::UNIX_EPOCH)
399 .unwrap_or(Duration::ZERO)
400 .as_millis() as u64,
401 Ordering::Release,
402 );
403 }
404
405 pub fn nodes(&self) -> Vec<NodeResources> {
407 self.nodes_snapshot().as_ref().to_vec()
408 }
409
410 pub fn node_count(&self) -> usize {
412 self.nodes_snapshot().len()
413 }
414
415 pub fn nodes_snapshot(&self) -> Arc<Vec<NodeResources>> {
417 if self.nodes_snapshot_dirty.load(Ordering::Relaxed)
418 && self.nodes_snapshot_dirty.swap(false, Ordering::AcqRel)
419 {
420 let nodes_map = self
421 .nodes
422 .lock()
423 .unwrap_or_else(|poison| poison.into_inner());
424 let mut nodes: Vec<_> = nodes_map.values().cloned().collect();
425 nodes.sort_unstable_by(|a, b| a.id.cmp(&b.id));
427 self.nodes_snapshot.store(Arc::new(nodes));
428 }
429 self.nodes_snapshot.load_full()
430 }
431
432 pub fn with_metrics<F, R>(&self, f: F) -> R
434 where
435 F: FnOnce(&HashMap<String, NodeMetrics>) -> R,
436 {
437 let metrics = self
438 .metrics
439 .lock()
440 .unwrap_or_else(|poison| poison.into_inner());
441 f(&metrics)
442 }
443
444 pub fn get_metrics(&self, node_id: &str) -> Option<NodeMetrics> {
446 let metrics = self
447 .metrics
448 .lock()
449 .unwrap_or_else(|poison| poison.into_inner());
450 metrics.get(node_id).cloned()
451 }
452
453 pub fn update_heartbeat(&self, node_id: &str) {
455 self.get_metrics_mut(node_id, |metrics| {
456 metrics.last_heartbeat = Instant::now();
457 });
458 }
459
460 pub fn get_metrics_mut<F, R>(&self, node_id: &str, f: F) -> Option<R>
462 where
463 F: FnOnce(&mut NodeMetrics) -> R,
464 {
465 let mut metrics = self
466 .metrics
467 .lock()
468 .unwrap_or_else(|poison| poison.into_inner());
469 metrics.get_mut(node_id).map(f)
470 }
471
472 pub fn check_heartbeat_timeout(&self, node_id: &str) -> bool {
474 let metrics = self
476 .metrics
477 .lock()
478 .unwrap_or_else(|poison| poison.into_inner());
479 if let Some(m) = metrics.get(node_id) {
480 Instant::now().duration_since(m.last_heartbeat) >= m.heartbeat_timeout
481 } else {
482 false
483 }
484 }
485
486 pub fn mark_failed(&self, node_id: &str) {
488 self.get_metrics_mut(node_id, |metrics| {
489 metrics.status = NodeStatus::Failed;
490 });
491 }
492
493 pub fn mark_timed_out_nodes_failed(&self) -> usize {
499 let now = Instant::now();
500 let mut failed_count = 0;
501 let mut metrics = self
502 .metrics
503 .lock()
504 .unwrap_or_else(|poison| poison.into_inner());
505
506 for m in metrics.values_mut() {
507 if m.status != NodeStatus::Failed
508 && now.saturating_duration_since(m.last_heartbeat) >= m.heartbeat_timeout
509 {
510 m.status = NodeStatus::Failed;
511 failed_count += 1;
512 }
513 }
514 failed_count
515 }
516
517 pub fn recover_node(&self, node_id: &str) {
519 self.get_metrics_mut(node_id, |metrics| {
520 metrics.status = NodeStatus::Active;
521 });
522 }
523
524 pub fn active_nodes(&self) -> Vec<NodeMetrics> {
526 let metrics = self
527 .metrics
528 .lock()
529 .unwrap_or_else(|poison| poison.into_inner());
530 metrics
531 .values()
532 .filter(|m| m.status == NodeStatus::Active)
533 .cloned()
534 .collect()
535 }
536
537 pub fn active_nodes_count(&self) -> usize {
539 let metrics = self
540 .metrics
541 .lock()
542 .unwrap_or_else(|poison| poison.into_inner());
543 metrics
544 .values()
545 .filter(|m| m.status == NodeStatus::Active)
546 .count()
547 }
548
549 pub fn total_vram_gb(&self) -> f32 {
551 f64::from_bits(self.total_vram_cache.load(Ordering::Acquire)) as f32
552 }
553
554 pub fn total_system_memory_gb(&self) -> f32 {
556 f64::from_bits(self.total_system_memory_cache.load(Ordering::Acquire)) as f32
557 }
558}
559
560#[derive(Clone, Debug)]
562pub struct ClusterHealthMonitor {
563 cluster: Arc<ClusterState>,
564 check_interval: Duration,
566}
567
568impl ClusterHealthMonitor {
569 pub fn new(cluster: Arc<ClusterState>, check_interval: Duration) -> Self {
571 Self {
572 cluster,
573 check_interval,
574 }
575 }
576
577 pub fn check_health(&self) {
579 self.cluster.mark_timed_out_nodes_failed();
580
581 self.cluster.last_update.store(
583 std::time::SystemTime::now()
584 .duration_since(std::time::UNIX_EPOCH)
585 .unwrap_or(Duration::ZERO)
586 .as_millis() as u64,
587 Ordering::Release,
588 );
589 }
590
591 pub fn health_report(&self) -> String {
593 let active_count = self.cluster.active_nodes_count();
594 let total_nodes = self.cluster.nodes_snapshot().len();
595
596 format!(
597 "Cluster Health Report\n\
598 =================\n\
599 Active nodes: {}/{}\n\
600 Total VRAM: {:.1} GB\n\
601 System memory: {:.1} GB\n",
602 active_count,
603 total_nodes,
604 self.cluster.total_vram_gb(),
605 self.cluster.total_system_memory_gb()
606 )
607 }
608
609 pub fn start_periodic_checks(&self) {
611 let this = self.clone();
612
613 std::thread::spawn(move || loop {
615 this.check_health();
616 std::thread::sleep(this.check_interval);
617 });
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use std::sync::Arc;
625 use std::thread;
626 use std::time::Duration;
627
628 #[test]
629 fn register_replaces_existing_nodes() {
630 let cluster = ClusterState::new();
631 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
632 cluster.register(NodeResources::new("node-a", 48.0, 128.0, "9.0", None));
633
634 assert_eq!(cluster.nodes().len(), 1);
635 assert_eq!(cluster.nodes()[0].vram_gb, 48.0);
636 }
637
638 #[test]
639 fn heartbeat_timeout_detection() {
640 let cluster = ClusterState::new();
641 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
642
643 thread::sleep(Duration::from_secs(6));
645
646 assert!(cluster.check_heartbeat_timeout("node-a"));
647 }
648
649 #[test]
650 fn node_metrics_keep_recent_latency_and_throughput_history() {
651 let mut metrics = NodeMetrics::default();
652
653 for idx in 0..40 {
654 metrics.record_latency(idx as f32);
655 metrics.record_throughput(idx as f32 / 10.0);
656 }
657
658 assert_eq!(metrics.latency_history_us.len(), NODE_HISTORY_CAP);
659 assert_eq!(metrics.throughput_history_gbps.len(), NODE_HISTORY_CAP);
660 assert_eq!(metrics.latency_history_us.front().copied(), Some(8.0));
661 assert_eq!(metrics.throughput_history_gbps.back().copied(), Some(3.9));
662 }
663
664 #[test]
665 fn health_monitor_reports_active_count() {
666 let cluster = Arc::new(ClusterState::new());
667 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
668 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
669
670 let monitor = ClusterHealthMonitor::new(cluster.clone(), Duration::from_secs(1));
671 monitor.check_health();
672
673 let report = monitor.health_report();
674 assert!(report.contains("Active nodes: 2/2"));
675 }
676
677 #[test]
678 fn metrics_record_latency() {
679 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
680
681 metrics.record_latency(1.0);
682 assert_eq!(metrics.avg_latency_us, 1.0);
683 assert_eq!(metrics.min_latency_us, 1.0);
684 assert_eq!(metrics.max_latency_us, 1.0);
685
686 metrics.record_latency(2.0);
687 assert!((metrics.avg_latency_us - 1.1).abs() < 1e-6);
689 }
690
691 #[test]
692 fn metrics_record_delivery_ratio() {
693 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
694
695 metrics.record_delivery_ratio(0.98);
696 assert!((metrics.delivery_ratio - 0.98).abs() < 1e-6);
697
698 metrics.record_delivery_ratio(0.90);
699 assert!((metrics.delivery_ratio - 0.972).abs() < 1e-6);
701 }
702
703 #[test]
704 fn with_metrics_allows_single_pass_inspection() {
705 let cluster = ClusterState::new();
706 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
707 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
708
709 let total_nodes = cluster.with_metrics(|metrics_map| {
710 assert!(metrics_map.contains_key("node-a"));
711 assert!(metrics_map.contains_key("node-b"));
712 metrics_map.len()
713 });
714
715 assert_eq!(total_nodes, 2);
716 }
717
718 #[test]
719 fn cluster_tracks_total_vram() {
720 let cluster = ClusterState::new();
721 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
722 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
723
724 assert_eq!(cluster.total_vram_gb(), 36.0);
725 }
726
727 #[test]
732 fn cluster_health_monitor_checks_all_nodes() {
733 let cluster = Arc::new(ClusterState::new());
734 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
735 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
736
737 let monitor = ClusterHealthMonitor::new(cluster.clone(), Duration::from_millis(100));
738 monitor.check_health();
739
740 let report = monitor.health_report();
741 assert!(
742 report.contains("Active nodes: 2/2"),
743 "Report should show both nodes as active"
744 );
745 }
746
747 #[test]
748 fn cluster_metrics_use_exponential_moving_average() {
749 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
750
751 metrics.record_latency(1.0);
753 let avg1 = metrics.avg_latency_us;
754 assert_eq!(avg1, 1.0);
755
756 metrics.record_latency(2.0);
757 let avg2 = metrics.avg_latency_us;
758 assert!(
760 (avg2 - 1.1).abs() < 1e-6,
761 "EMA calculation: expected 1.1, got {avg2}"
762 );
763
764 metrics.record_latency(3.0);
765 let avg3 = metrics.avg_latency_us;
766 assert!(
768 (avg3 - 1.29).abs() < 1e-5,
769 "EMA calculation: expected 1.29, got {avg3}"
770 );
771 }
772
773 #[test]
774 fn cluster_delivery_ratio_tracks_network_quality() {
775 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
776
777 metrics.record_delivery_ratio(0.98);
778 assert!((metrics.delivery_ratio - 0.98).abs() < 1e-6);
779
780 metrics.record_delivery_ratio(0.95);
781 assert!((metrics.delivery_ratio - 0.977).abs() < 1e-6);
783
784 metrics.record_delivery_ratio(0.80);
786 assert!(
788 metrics.delivery_ratio < 0.98,
789 "Delivery ratio should degrade"
790 );
791 assert!(metrics.delivery_ratio > 0.90, "But recover somewhat");
792 }
793
794 #[test]
795 fn cluster_concurrent_metric_updates() {
796 let cluster = Arc::new(ClusterState::new());
797 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
798
799 let mut handles = vec![];
800
801 for _ in 0..10 {
803 let cluster_clone = Arc::clone(&cluster);
804 let handle = thread::spawn(move || {
805 let nodes = cluster_clone.nodes();
806 for _ in 0..100 {
807 let _ = nodes.len();
809 }
810 });
811 handles.push(handle);
812 }
813
814 for handle in handles {
815 handle.join().unwrap();
816 }
817
818 assert_eq!(cluster.nodes().len(), 1);
820 assert_eq!(cluster.total_vram_gb(), 24.0);
821 }
822
823 #[test]
824 fn cluster_handles_rapid_registration_churn() {
825 let cluster = Arc::new(ClusterState::new());
826
827 for iteration in 0..5 {
829 for i in 0..10 {
830 cluster.register(NodeResources::new(
831 format!("node-{i}"),
832 24.0 + (iteration as f32),
833 64.0,
834 "8.9",
835 None,
836 ));
837 }
838
839 let nodes = cluster.nodes();
840 assert_eq!(
841 nodes.len(),
842 10,
843 "Should have 10 nodes at iteration {iteration}"
844 );
845
846 let expected_vram: f32 = (0..10).map(|_| 24.0 + iteration as f32).sum();
848 let actual = cluster.total_vram_gb();
849 assert!((actual - expected_vram).abs() < 0.1);
850 }
851 }
852
853 #[test]
854 fn cluster_health_monitor_reports_accurate_status() {
855 let cluster = Arc::new(ClusterState::new());
856 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
857 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
858
859 let monitor = ClusterHealthMonitor::new(cluster.clone(), Duration::from_millis(100));
860 monitor.check_health();
861
862 let report = monitor.health_report();
863 assert!(
864 report.contains("Active nodes: 2"),
865 "Should report 2 active nodes"
866 );
867 assert!(report.contains("2/2"), "Should show 2 of 2 nodes");
868 }
869
870 #[test]
871 fn node_metrics_track_min_max_latency() {
872 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
873
874 metrics.record_latency(2.0);
875 assert_eq!(metrics.min_latency_us, 2.0);
876 assert_eq!(metrics.max_latency_us, 2.0);
877
878 metrics.record_latency(1.0);
879 assert_eq!(metrics.min_latency_us, 1.0);
880 assert_eq!(metrics.max_latency_us, 2.0);
881
882 metrics.record_latency(5.0);
883 assert_eq!(metrics.min_latency_us, 1.0);
884 assert_eq!(metrics.max_latency_us, 5.0);
885
886 metrics.record_latency(3.0);
887 assert_eq!(metrics.min_latency_us, 1.0);
888 assert_eq!(metrics.max_latency_us, 5.0);
889 }
890}