1use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant};
12
13use arc_swap::ArcSwap;
14
15pub use crate::protocol::NodeResources;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum NodeStatus {
20 #[default]
22 Active,
23 Degraded,
25 Failed,
27}
28
29#[derive(Clone, Debug)]
31pub struct NodeMetrics {
32 pub name: String,
34 pub status: NodeStatus,
36 pub vram_gb: f32,
38 pub total_vram_gb: f32,
40 pub system_memory_gb: f32,
42 pub compute_capability: String,
44 pub gpu_name: Option<String>,
46
47 pub last_heartbeat: Instant,
49 pub heartbeat_timeout: Duration,
51
52 pub avg_latency_us: f32,
54 pub min_latency_us: f32,
56 pub max_latency_us: f32,
58 pub latency_samples: u64,
60
61 pub delivery_ratio: f32,
63 pub throughput_gbps: f32,
65
66 pub used_vram_gb: f32,
68 pub available_vram_gb: f32,
70
71 pub streaming_layers: Option<(usize, usize)>,
73
74 pub af_xdp_gbps: f32,
76 pub latency_micros: f32,
78 pub(crate) delivery_ratio_initialized: bool,
80}
81
82impl Default for NodeMetrics {
83 fn default() -> Self {
84 Self {
85 name: String::new(),
86 status: NodeStatus::Active,
87 vram_gb: 0.0,
88 total_vram_gb: 0.0,
89 system_memory_gb: 0.0,
90 compute_capability: String::new(),
91 gpu_name: None,
92 last_heartbeat: Instant::now(),
93 heartbeat_timeout: Duration::from_secs(5),
94 avg_latency_us: 0.0,
95 min_latency_us: f32::MAX,
96 max_latency_us: 0.0,
97 latency_samples: 0,
98 delivery_ratio: 1.0,
99 throughput_gbps: 0.0,
100 used_vram_gb: 0.0,
101 available_vram_gb: 0.0,
102 streaming_layers: None,
103 af_xdp_gbps: 0.0,
104 latency_micros: 0.0,
105 delivery_ratio_initialized: false,
106 }
107 }
108}
109
110impl NodeMetrics {
111 pub fn new(
113 vram_gb: f32,
114 system_memory_gb: f32,
115 compute_capability: String,
116 heartbeat_timeout: Duration,
117 ) -> Self {
118 Self {
119 name: String::new(),
120 status: NodeStatus::Active,
121 vram_gb,
122 total_vram_gb: vram_gb,
123 system_memory_gb,
124 compute_capability,
125 gpu_name: None,
126 last_heartbeat: Instant::now(),
127 heartbeat_timeout,
128 avg_latency_us: 0.0,
129 min_latency_us: f32::MAX,
130 max_latency_us: 0.0,
131 latency_samples: 0,
132 delivery_ratio: 1.0,
133 throughput_gbps: 0.0,
134 used_vram_gb: 0.0,
135 available_vram_gb: vram_gb,
136 streaming_layers: None,
137 af_xdp_gbps: 0.0,
138 latency_micros: 0.0,
139 delivery_ratio_initialized: false,
140 }
141 }
142
143 pub fn record_latency(&mut self, latency_us: f32) {
145 if latency_us < self.min_latency_us {
146 self.min_latency_us = latency_us;
147 }
148 if latency_us > self.max_latency_us {
149 self.max_latency_us = latency_us;
150 }
151
152 self.latency_samples += 1;
153 if self.latency_samples == 1 {
154 self.avg_latency_us = latency_us;
155 } else {
156 self.avg_latency_us = self.avg_latency_us * 0.9 + latency_us * 0.1;
157 }
158 }
159
160 pub fn record_delivery_ratio(&mut self, ratio: f32) {
162 if !self.delivery_ratio_initialized {
163 self.delivery_ratio = ratio;
164 self.delivery_ratio_initialized = true;
165 } else {
166 self.delivery_ratio = self.delivery_ratio * 0.9 + ratio * 0.1;
168 }
169 }
170
171 pub fn record_throughput(&mut self, throughput_gbps: f32) {
173 self.throughput_gbps = self.throughput_gbps * 0.9 + throughput_gbps * 0.1;
175 }
176
177 pub fn record_vram_usage(&mut self, used_vram_gb: f32) {
179 self.used_vram_gb = used_vram_gb;
180 self.available_vram_gb = self.total_vram_gb - used_vram_gb;
181 }
182
183 pub fn set_streaming_layers(&mut self, start: usize, end: usize) {
185 self.streaming_layers = Some((start, end));
186 }
187
188 pub fn clear_streaming_layers(&mut self) {
190 self.streaming_layers = None;
191 }
192}
193
194#[derive(Debug)]
196pub struct ClusterState {
197 nodes: Arc<Mutex<HashMap<String, NodeResources>>>,
199 nodes_snapshot: Arc<ArcSwap<Vec<NodeResources>>>,
201 nodes_snapshot_dirty: Arc<AtomicBool>,
203 metrics: Arc<Mutex<HashMap<String, NodeMetrics>>>,
205 last_update: Arc<AtomicU64>,
207 total_vram_cache: Arc<AtomicU64>,
209}
210
211impl Clone for ClusterState {
212 fn clone(&self) -> Self {
213 Self {
214 nodes: Arc::clone(&self.nodes),
215 nodes_snapshot: Arc::clone(&self.nodes_snapshot),
216 nodes_snapshot_dirty: Arc::clone(&self.nodes_snapshot_dirty),
217 metrics: Arc::clone(&self.metrics),
218 last_update: Arc::clone(&self.last_update),
219 total_vram_cache: Arc::clone(&self.total_vram_cache),
220 }
221 }
222}
223
224impl Default for ClusterState {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl ClusterState {
231 pub fn new() -> Self {
233 Self {
234 nodes: Arc::new(Mutex::new(HashMap::new())),
235 nodes_snapshot: Arc::new(ArcSwap::from_pointee(Vec::<NodeResources>::new())),
236 nodes_snapshot_dirty: Arc::new(AtomicBool::new(false)),
237 metrics: Arc::new(Mutex::new(HashMap::new())),
238 last_update: Arc::new(AtomicU64::new(0)),
239 total_vram_cache: Arc::new(AtomicU64::new(0.0_f64.to_bits())),
240 }
241 }
242
243 pub fn register(&self, node: NodeResources) {
245 let mut nodes = self.nodes.lock().unwrap();
246 let mut metrics = self.metrics.lock().unwrap();
247
248 let id = node.id.clone();
249 let vram_gb = node.vram_gb;
250 let system_memory_gb = node.system_memory_gb;
251 let compute_capability = node.compute_capability.clone();
252 let mut vram_delta = vram_gb;
253
254 if let Some(existing) = nodes.get_mut(&id) {
255 vram_delta = vram_gb - existing.vram_gb;
256 existing.vram_gb = vram_gb;
257 existing.system_memory_gb = system_memory_gb;
258 existing.compute_capability = compute_capability.clone();
259 } else {
260 nodes.insert(id.clone(), node);
261 }
262
263 if let Some(existing_metrics) = metrics.get_mut(&id) {
264 existing_metrics.vram_gb = vram_gb;
265 existing_metrics.total_vram_gb = vram_gb;
266 existing_metrics.system_memory_gb = system_memory_gb;
267 existing_metrics.compute_capability = compute_capability;
268 existing_metrics.heartbeat_timeout = Duration::from_secs(5);
269 } else {
270 metrics.insert(
271 id,
272 NodeMetrics::new(
273 vram_gb,
274 system_memory_gb,
275 compute_capability,
276 Duration::from_secs(5),
277 ),
278 );
279 }
280
281 self.nodes_snapshot_dirty.store(true, Ordering::Release);
282
283 let current_total_vram = f64::from_bits(self.total_vram_cache.load(Ordering::Acquire));
284 self.total_vram_cache.store(
285 (current_total_vram + vram_delta as f64).to_bits(),
286 Ordering::Release,
287 );
288
289 self.last_update.store(
290 std::time::SystemTime::now()
291 .duration_since(std::time::UNIX_EPOCH)
292 .unwrap_or(Duration::ZERO)
293 .as_millis() as u64,
294 Ordering::Release,
295 );
296 }
297
298 pub fn nodes(&self) -> Vec<NodeResources> {
300 self.nodes_snapshot().as_ref().to_vec()
301 }
302
303 pub fn nodes_snapshot(&self) -> Arc<Vec<NodeResources>> {
305 if self.nodes_snapshot_dirty.load(Ordering::Acquire)
306 && self.nodes_snapshot_dirty.swap(false, Ordering::AcqRel)
307 {
308 let nodes = self.nodes.lock().unwrap();
309 self.nodes_snapshot
310 .store(Arc::new(nodes.values().cloned().collect::<Vec<_>>()));
311 }
312
313 self.nodes_snapshot.load_full()
314 }
315
316 pub fn get_metrics(&self, node_id: &str) -> Option<NodeMetrics> {
318 let metrics = self.metrics.lock().unwrap();
319 metrics.get(node_id).cloned()
320 }
321
322 pub fn update_heartbeat(&self, node_id: &str) {
324 self.get_metrics_mut(node_id, |metrics| {
325 metrics.last_heartbeat = Instant::now();
326 });
327 }
328
329 pub fn get_metrics_mut<F, R>(&self, node_id: &str, f: F) -> Option<R>
331 where
332 F: FnOnce(&mut NodeMetrics) -> R,
333 {
334 let mut metrics = self.metrics.lock().unwrap();
335 metrics.get_mut(node_id).map(f)
336 }
337
338 pub fn check_heartbeat_timeout(&self, node_id: &str) -> bool {
340 if let Some(metrics) = self.get_metrics(node_id) {
341 let elapsed = Instant::now().duration_since(metrics.last_heartbeat);
342 elapsed >= metrics.heartbeat_timeout
343 } else {
344 false
345 }
346 }
347
348 pub fn mark_failed(&self, node_id: &str) {
350 self.get_metrics_mut(node_id, |metrics| {
351 metrics.status = NodeStatus::Failed;
352 });
353 }
354
355 pub fn recover_node(&self, node_id: &str) {
357 self.get_metrics_mut(node_id, |metrics| {
358 metrics.status = NodeStatus::Active;
359 });
360 }
361
362 pub fn active_nodes(&self) -> Vec<NodeMetrics> {
364 let metrics = self.metrics.lock().unwrap();
365 metrics
366 .values()
367 .filter(|m| m.status == NodeStatus::Active)
368 .cloned()
369 .collect()
370 }
371
372 pub fn total_vram_gb(&self) -> f32 {
374 f64::from_bits(self.total_vram_cache.load(Ordering::Acquire)) as f32
375 }
376
377 pub fn total_system_memory_gb(&self) -> f32 {
379 let nodes = self.nodes.lock().unwrap();
380 nodes.values().map(|n| n.system_memory_gb).sum()
381 }
382}
383
384#[derive(Clone, Debug)]
386pub struct ClusterHealthMonitor {
387 cluster: Arc<ClusterState>,
388 check_interval: Duration,
390}
391
392impl ClusterHealthMonitor {
393 pub fn new(cluster: Arc<ClusterState>, check_interval: Duration) -> Self {
395 Self {
396 cluster,
397 check_interval,
398 }
399 }
400
401 pub fn check_health(&self) {
403 let failed_nodes: Vec<String> = self
404 .cluster
405 .nodes_snapshot()
406 .iter()
407 .filter(|n| self.cluster.check_heartbeat_timeout(&n.id))
408 .map(|n| n.id.clone())
409 .collect();
410
411 for node_id in &failed_nodes {
413 self.cluster.mark_failed(node_id);
414 }
415
416 self.cluster.last_update.store(
418 std::time::SystemTime::now()
419 .duration_since(std::time::UNIX_EPOCH)
420 .unwrap_or(Duration::ZERO)
421 .as_millis() as u64,
422 Ordering::Release,
423 );
424 }
425
426 pub fn health_report(&self) -> String {
428 let active_count = self.cluster.active_nodes().len();
429 let total_nodes = self.cluster.nodes_snapshot().len();
430
431 format!(
432 "Cluster Health Report\n\
433 =================\n\
434 Active nodes: {}/{}\n\
435 Total VRAM: {:.1} GB\n\
436 System memory: {:.1} GB\n",
437 active_count,
438 total_nodes,
439 self.cluster.total_vram_gb(),
440 self.cluster.total_system_memory_gb()
441 )
442 }
443
444 pub fn start_periodic_checks(&self) {
446 let this = self.clone();
447
448 std::thread::spawn(move || loop {
450 this.check_health();
451 std::thread::sleep(this.check_interval);
452 });
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use std::sync::Arc;
460 use std::thread;
461 use std::time::Duration;
462
463 #[test]
464 fn register_replaces_existing_nodes() {
465 let cluster = ClusterState::new();
466 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
467 cluster.register(NodeResources::new("node-a", 48.0, 128.0, "9.0", None));
468
469 assert_eq!(cluster.nodes().len(), 1);
470 assert_eq!(cluster.nodes()[0].vram_gb, 48.0);
471 }
472
473 #[test]
474 fn heartbeat_timeout_detection() {
475 let cluster = ClusterState::new();
476 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
477
478 thread::sleep(Duration::from_secs(6));
480
481 assert!(cluster.check_heartbeat_timeout("node-a"));
482 }
483
484 #[test]
485 fn health_monitor_reports_active_count() {
486 let cluster = Arc::new(ClusterState::new());
487 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
488 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
489
490 let monitor = ClusterHealthMonitor::new(cluster.clone(), Duration::from_secs(1));
491 monitor.check_health();
492
493 let report = monitor.health_report();
494 assert!(report.contains("Active nodes: 2/2"));
495 }
496
497 #[test]
498 fn metrics_record_latency() {
499 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
500
501 metrics.record_latency(1.0);
502 assert_eq!(metrics.avg_latency_us, 1.0);
503 assert_eq!(metrics.min_latency_us, 1.0);
504 assert_eq!(metrics.max_latency_us, 1.0);
505
506 metrics.record_latency(2.0);
507 assert!((metrics.avg_latency_us - 1.1).abs() < 1e-6);
509 }
510
511 #[test]
512 fn metrics_record_delivery_ratio() {
513 let mut metrics = NodeMetrics::new(24.0, 64.0, "8.9".to_string(), Duration::from_secs(5));
514
515 metrics.record_delivery_ratio(0.98);
516 assert!((metrics.delivery_ratio - 0.98).abs() < 1e-6);
517
518 metrics.record_delivery_ratio(0.90);
519 assert!((metrics.delivery_ratio - 0.972).abs() < 1e-6);
521 }
522
523 #[test]
524 fn cluster_tracks_total_vram() {
525 let cluster = ClusterState::new();
526 cluster.register(NodeResources::new("node-a", 24.0, 64.0, "8.9", None));
527 cluster.register(NodeResources::new("node-b", 12.0, 32.0, "8.6", None));
528
529 assert_eq!(cluster.total_vram_gb(), 36.0);
530 }
531}