1#![allow(dead_code)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum NodeRole {
16 Coordinator,
18 Worker,
20 Storage,
22 Gateway,
24}
25
26impl NodeRole {
27 #[must_use]
29 pub fn as_str(self) -> &'static str {
30 match self {
31 Self::Coordinator => "coordinator",
32 Self::Worker => "worker",
33 Self::Storage => "storage",
34 Self::Gateway => "gateway",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum NodeStatus {
46 Healthy,
48 Degraded,
50 Suspended,
52 Removed,
54}
55
56impl NodeStatus {
57 #[must_use]
59 pub fn is_available(self) -> bool {
60 matches!(self, Self::Healthy | Self::Degraded)
61 }
62}
63
64#[derive(Debug, Clone)]
70pub struct NodeInfo {
71 pub id: String,
73 pub address: String,
75 pub role: NodeRole,
77 pub status: NodeStatus,
79 pub cpu_cores: u32,
81 pub memory_mb: u32,
83 pub has_gpu: bool,
85 pub registered_at_ms: u64,
87 pub last_seen_ms: u64,
89 pub tags: Vec<String>,
91}
92
93impl NodeInfo {
94 #[must_use]
96 pub fn new(
97 id: impl Into<String>,
98 address: impl Into<String>,
99 role: NodeRole,
100 cpu_cores: u32,
101 memory_mb: u32,
102 has_gpu: bool,
103 now_ms: u64,
104 ) -> Self {
105 Self {
106 id: id.into(),
107 address: address.into(),
108 role,
109 status: NodeStatus::Healthy,
110 cpu_cores,
111 memory_mb,
112 has_gpu,
113 registered_at_ms: now_ms,
114 last_seen_ms: now_ms,
115 tags: Vec::new(),
116 }
117 }
118
119 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
121 self.tags.push(tag.into());
122 self
123 }
124
125 pub fn touch(&mut self, now_ms: u64, status: NodeStatus) {
127 self.last_seen_ms = now_ms;
128 self.status = status;
129 }
130
131 #[must_use]
133 pub fn is_available(&self) -> bool {
134 self.status.is_available()
135 }
136
137 #[must_use]
139 pub fn has_tag(&self, tag: &str) -> bool {
140 self.tags.iter().any(|t| t == tag)
141 }
142
143 #[must_use]
145 pub fn age_ms(&self, now_ms: u64) -> u64 {
146 now_ms.saturating_sub(self.registered_at_ms)
147 }
148}
149
150#[derive(Debug, Default)]
158pub struct NodeRegistry {
159 nodes: Vec<NodeInfo>,
160}
161
162impl NodeRegistry {
163 #[must_use]
165 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn register(&mut self, node: NodeInfo) {
172 if let Some(existing) = self.nodes.iter_mut().find(|n| n.id == node.id) {
173 *existing = node;
174 } else {
175 self.nodes.push(node);
176 }
177 }
178
179 pub fn deregister(&mut self, id: &str) -> Option<NodeInfo> {
181 if let Some(pos) = self.nodes.iter().position(|n| n.id == id) {
182 Some(self.nodes.remove(pos))
183 } else {
184 None
185 }
186 }
187
188 #[must_use]
190 pub fn get(&self, id: &str) -> Option<&NodeInfo> {
191 self.nodes.iter().find(|n| n.id == id)
192 }
193
194 #[must_use]
196 pub fn get_mut(&mut self, id: &str) -> Option<&mut NodeInfo> {
197 self.nodes.iter_mut().find(|n| n.id == id)
198 }
199
200 #[must_use]
202 pub fn len(&self) -> usize {
203 self.nodes.len()
204 }
205
206 #[must_use]
208 pub fn is_empty(&self) -> bool {
209 self.nodes.is_empty()
210 }
211
212 #[must_use]
214 pub fn by_role(&self, role: NodeRole) -> Vec<&NodeInfo> {
215 self.nodes.iter().filter(|n| n.role == role).collect()
216 }
217
218 #[must_use]
220 pub fn by_status(&self, status: NodeStatus) -> Vec<&NodeInfo> {
221 self.nodes.iter().filter(|n| n.status == status).collect()
222 }
223
224 #[must_use]
226 pub fn available_workers(&self) -> Vec<&NodeInfo> {
227 self.nodes
228 .iter()
229 .filter(|n| n.role == NodeRole::Worker && n.is_available())
230 .collect()
231 }
232
233 #[must_use]
235 pub fn by_tag(&self, tag: &str) -> Vec<&NodeInfo> {
236 self.nodes.iter().filter(|n| n.has_tag(tag)).collect()
237 }
238
239 pub fn remove_node(&mut self, id: &str, now_ms: u64) {
241 if let Some(n) = self.get_mut(id) {
242 n.touch(now_ms, NodeStatus::Removed);
243 }
244 }
245
246 pub fn evict_stale(&mut self, now_ms: u64, ttl_ms: u64) -> Vec<String> {
250 let cutoff = now_ms.saturating_sub(ttl_ms);
251 let mut evicted = Vec::new();
252 self.nodes.retain(|n| {
253 if n.last_seen_ms < cutoff {
254 evicted.push(n.id.clone());
255 false
256 } else {
257 true
258 }
259 });
260 evicted
261 }
262
263 #[must_use]
265 pub fn status_summary(&self) -> (usize, usize, usize, usize, usize) {
266 let total = self.nodes.len();
267 let healthy = self.by_status(NodeStatus::Healthy).len();
268 let degraded = self.by_status(NodeStatus::Degraded).len();
269 let suspended = self.by_status(NodeStatus::Suspended).len();
270 let removed = self.by_status(NodeStatus::Removed).len();
271 (total, healthy, degraded, suspended, removed)
272 }
273}
274
275#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn worker(id: &str, now_ms: u64) -> NodeInfo {
284 NodeInfo::new(
285 id,
286 format!("10.0.0.1:5000"),
287 NodeRole::Worker,
288 8,
289 16_384,
290 false,
291 now_ms,
292 )
293 }
294
295 fn gpu_worker(id: &str, now_ms: u64) -> NodeInfo {
296 NodeInfo::new(
297 id,
298 format!("10.0.0.2:5000"),
299 NodeRole::Worker,
300 16,
301 32_768,
302 true,
303 now_ms,
304 )
305 .with_tag("gpu")
306 }
307
308 fn coordinator(id: &str, now_ms: u64) -> NodeInfo {
309 NodeInfo::new(
310 id,
311 format!("10.0.0.3:5000"),
312 NodeRole::Coordinator,
313 4,
314 8_192,
315 false,
316 now_ms,
317 )
318 }
319
320 #[test]
323 fn test_node_role_as_str() {
324 assert_eq!(NodeRole::Worker.as_str(), "worker");
325 assert_eq!(NodeRole::Coordinator.as_str(), "coordinator");
326 assert_eq!(NodeRole::Storage.as_str(), "storage");
327 assert_eq!(NodeRole::Gateway.as_str(), "gateway");
328 }
329
330 #[test]
333 fn test_healthy_is_available() {
334 assert!(NodeStatus::Healthy.is_available());
335 }
336
337 #[test]
338 fn test_degraded_is_available() {
339 assert!(NodeStatus::Degraded.is_available());
340 }
341
342 #[test]
343 fn test_suspended_not_available() {
344 assert!(!NodeStatus::Suspended.is_available());
345 }
346
347 #[test]
348 fn test_removed_not_available() {
349 assert!(!NodeStatus::Removed.is_available());
350 }
351
352 #[test]
355 fn test_node_info_initial_status_healthy() {
356 let n = worker("n0", 1000);
357 assert_eq!(n.status, NodeStatus::Healthy);
358 }
359
360 #[test]
361 fn test_node_info_has_tag() {
362 let n = gpu_worker("n0", 1000);
363 assert!(n.has_tag("gpu"));
364 assert!(!n.has_tag("av1"));
365 }
366
367 #[test]
368 fn test_node_info_age_ms() {
369 let n = worker("n0", 1000);
370 assert_eq!(n.age_ms(3000), 2000);
371 }
372
373 #[test]
374 fn test_node_info_touch_updates_status() {
375 let mut n = worker("n0", 1000);
376 n.touch(2000, NodeStatus::Degraded);
377 assert_eq!(n.status, NodeStatus::Degraded);
378 assert_eq!(n.last_seen_ms, 2000);
379 }
380
381 #[test]
384 fn test_registry_empty() {
385 let reg = NodeRegistry::new();
386 assert!(reg.is_empty());
387 assert_eq!(reg.len(), 0);
388 }
389
390 #[test]
391 fn test_registry_register_and_get() {
392 let mut reg = NodeRegistry::new();
393 reg.register(worker("n0", 1000));
394 let n = reg.get("n0").expect("get should return a value");
395 assert_eq!(n.id, "n0");
396 }
397
398 #[test]
399 fn test_registry_register_replaces_existing() {
400 let mut reg = NodeRegistry::new();
401 reg.register(worker("n0", 1000));
402 reg.register(gpu_worker("n0", 2000)); assert_eq!(reg.len(), 1);
404 assert!(reg.get("n0").expect("get should return a value").has_gpu);
405 }
406
407 #[test]
408 fn test_registry_deregister() {
409 let mut reg = NodeRegistry::new();
410 reg.register(worker("n0", 1000));
411 let removed = reg.deregister("n0");
412 assert!(removed.is_some());
413 assert!(reg.is_empty());
414 }
415
416 #[test]
417 fn test_registry_by_role() {
418 let mut reg = NodeRegistry::new();
419 reg.register(worker("w0", 1000));
420 reg.register(worker("w1", 1000));
421 reg.register(coordinator("c0", 1000));
422 assert_eq!(reg.by_role(NodeRole::Worker).len(), 2);
423 assert_eq!(reg.by_role(NodeRole::Coordinator).len(), 1);
424 }
425
426 #[test]
427 fn test_registry_available_workers() {
428 let mut reg = NodeRegistry::new();
429 let mut w1 = worker("w0", 1000);
430 w1.status = NodeStatus::Suspended;
431 reg.register(w1);
432 reg.register(worker("w1", 1000));
433 assert_eq!(reg.available_workers().len(), 1);
434 }
435
436 #[test]
437 fn test_registry_by_tag() {
438 let mut reg = NodeRegistry::new();
439 reg.register(gpu_worker("g0", 1000));
440 reg.register(worker("w0", 1000));
441 assert_eq!(reg.by_tag("gpu").len(), 1);
442 }
443
444 #[test]
445 fn test_registry_remove_node_soft_delete() {
446 let mut reg = NodeRegistry::new();
447 reg.register(worker("w0", 1000));
448 reg.remove_node("w0", 2000);
449 assert_eq!(
450 reg.get("w0").expect("get should return a value").status,
451 NodeStatus::Removed
452 );
453 assert!(!reg
454 .get("w0")
455 .expect("get should return a value")
456 .is_available());
457 }
458
459 #[test]
460 fn test_registry_evict_stale() {
461 let mut reg = NodeRegistry::new();
462 reg.register(worker("old", 100));
463 reg.register(worker("fresh", 5000));
464 let evicted = reg.evict_stale(6000, 1000); assert!(evicted.contains(&"old".to_string()));
466 assert_eq!(reg.len(), 1);
467 }
468
469 #[test]
470 fn test_registry_status_summary() {
471 let mut reg = NodeRegistry::new();
472 reg.register(worker("w0", 1000));
473 reg.register(worker("w1", 1000));
474 let (total, healthy, _, _, _) = reg.status_summary();
475 assert_eq!(total, 2);
476 assert_eq!(healthy, 2);
477 }
478}