1#![allow(dead_code)]
11
12use crate::{DiscoveryMethod, DistributedError, Result};
13use dashmap::DashMap;
14use std::collections::HashMap;
15use std::net::SocketAddr;
16use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
17use std::sync::Arc;
18use std::time::{Duration, SystemTime};
19use tokio::sync::RwLock;
20use tracing::{debug, error, info, warn};
21
22pub struct DiscoveryService {
24 method: DiscoveryMethod,
26
27 workers: Arc<DashMap<String, WorkerEndpoint>>,
29
30 config: DiscoveryConfig,
32
33 health_checker: Arc<HealthChecker>,
35
36 running: Arc<AtomicBool>,
38}
39
40#[derive(Debug, Clone)]
42pub struct DiscoveryConfig {
43 pub service_name: String,
45
46 pub service_port: u16,
48
49 pub discovery_interval: Duration,
51
52 pub health_check_interval: Duration,
54
55 pub worker_timeout: Duration,
57
58 pub etcd_endpoints: Vec<String>,
60
61 pub consul_address: String,
63
64 pub static_workers: Vec<String>,
66}
67
68impl Default for DiscoveryConfig {
69 fn default() -> Self {
70 Self {
71 service_name: "oximedia-worker".to_string(),
72 service_port: 50052,
73 discovery_interval: Duration::from_secs(30),
74 health_check_interval: Duration::from_secs(10),
75 worker_timeout: Duration::from_secs(90),
76 etcd_endpoints: vec!["http://127.0.0.1:2379".to_string()],
77 consul_address: "http://127.0.0.1:8500".to_string(),
78 static_workers: Vec::new(),
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct WorkerEndpoint {
86 pub worker_id: String,
88
89 pub address: SocketAddr,
91
92 pub hostname: String,
94
95 pub capabilities: WorkerCapabilities,
97
98 pub health: HealthStatus,
100
101 pub last_seen: SystemTime,
103
104 pub location: Option<GeoLocation>,
106
107 pub metadata: HashMap<String, String>,
109}
110
111#[derive(Debug, Clone)]
113pub struct WorkerCapabilities {
114 pub cpu_cores: u32,
116
117 pub memory_bytes: u64,
119
120 pub gpu_devices: Vec<String>,
122
123 pub codecs: Vec<String>,
125
126 pub max_jobs: u32,
128
129 pub performance_score: f32,
131}
132
133impl Default for WorkerCapabilities {
134 fn default() -> Self {
135 Self {
136 cpu_cores: 1,
137 memory_bytes: 1_073_741_824,
138 gpu_devices: Vec::new(),
139 codecs: vec!["h264".to_string()],
140 max_jobs: 2,
141 performance_score: 1.0,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum HealthStatus {
149 Healthy,
150 Degraded,
151 Unhealthy,
152 Unknown,
153}
154
155#[derive(Debug, Clone)]
157pub struct GeoLocation {
158 pub region: String,
160
161 pub zone: Option<String>,
163
164 pub datacenter: Option<String>,
166
167 pub latitude: Option<f64>,
169
170 pub longitude: Option<f64>,
172}
173
174pub struct HealthChecker {
176 health_results: Arc<DashMap<String, HealthCheckResult>>,
178
179 stats: HealthStats,
181}
182
183#[derive(Debug, Clone)]
185struct HealthCheckResult {
186 worker_id: String,
187 status: HealthStatus,
188 latency: Duration,
189 last_check: SystemTime,
190 consecutive_failures: u32,
191}
192
193#[derive(Debug, Default)]
195struct HealthStats {
196 total_checks: AtomicU64,
197 successful_checks: AtomicU64,
198 failed_checks: AtomicU64,
199}
200
201impl DiscoveryService {
202 #[must_use]
204 pub fn new(method: DiscoveryMethod, config: DiscoveryConfig) -> Self {
205 Self {
206 method,
207 workers: Arc::new(DashMap::new()),
208 config,
209 health_checker: Arc::new(HealthChecker::new()),
210 running: Arc::new(AtomicBool::new(false)),
211 }
212 }
213
214 pub async fn start(&self) -> Result<()> {
216 info!("Starting discovery service with method: {:?}", self.method);
217 self.running.store(true, Ordering::Relaxed);
218
219 let service = self.clone_refs();
221 tokio::spawn(async move {
222 service.discovery_loop().await;
223 });
224
225 let service = self.clone_refs();
227 tokio::spawn(async move {
228 service.health_check_loop().await;
229 });
230
231 Ok(())
232 }
233
234 fn clone_refs(&self) -> Self {
235 Self {
236 method: self.method,
237 workers: self.workers.clone(),
238 config: self.config.clone(),
239 health_checker: self.health_checker.clone(),
240 running: self.running.clone(),
241 }
242 }
243
244 async fn discovery_loop(&self) {
246 let mut interval = tokio::time::interval(self.config.discovery_interval);
247
248 while self.running.load(Ordering::Relaxed) {
249 interval.tick().await;
250
251 if let Err(e) = self.discover_workers().await {
252 error!("Worker discovery failed: {}", e);
253 }
254 }
255 }
256
257 async fn discover_workers(&self) -> Result<()> {
259 match self.method {
260 DiscoveryMethod::Static => self.discover_static().await,
261 DiscoveryMethod::MDNS => self.discover_mdns().await,
262 DiscoveryMethod::Etcd => self.discover_etcd().await,
263 DiscoveryMethod::Consul => self.discover_consul().await,
264 }
265 }
266
267 async fn discover_static(&self) -> Result<()> {
269 debug!("Discovering static workers");
270
271 for addr_str in &self.config.static_workers {
272 let addr: SocketAddr = addr_str
273 .parse()
274 .map_err(|e| DistributedError::Discovery(format!("Invalid address: {e}")))?;
275
276 let worker_id = format!("static-{addr}");
277
278 let endpoint = WorkerEndpoint {
279 worker_id: worker_id.clone(),
280 address: addr,
281 hostname: addr.ip().to_string(),
282 capabilities: WorkerCapabilities::default(),
283 health: HealthStatus::Unknown,
284 last_seen: SystemTime::now(),
285 location: None,
286 metadata: HashMap::new(),
287 };
288
289 self.workers.insert(worker_id, endpoint);
290 }
291
292 info!(
293 "Discovered {} static workers",
294 self.config.static_workers.len()
295 );
296 Ok(())
297 }
298
299 async fn discover_mdns(&self) -> Result<()> {
301 debug!("Discovering workers via mDNS");
302
303 info!("mDNS discovery completed");
306 Ok(())
307 }
308
309 async fn discover_etcd(&self) -> Result<()> {
311 debug!("Discovering workers via etcd");
312
313 info!("etcd discovery completed");
316 Ok(())
317 }
318
319 async fn discover_consul(&self) -> Result<()> {
321 debug!("Discovering workers via Consul");
322
323 info!("Consul discovery completed");
326 Ok(())
327 }
328
329 async fn health_check_loop(&self) {
331 let mut interval = tokio::time::interval(self.config.health_check_interval);
332
333 while self.running.load(Ordering::Relaxed) {
334 interval.tick().await;
335
336 self.check_worker_health().await;
337 }
338 }
339
340 async fn check_worker_health(&self) {
342 let workers: Vec<_> = self
343 .workers
344 .iter()
345 .map(|e| (e.key().clone(), e.value().clone()))
346 .collect();
347
348 for (worker_id, endpoint) in workers {
349 let result = self.health_checker.check_worker(&endpoint).await;
350
351 if let Some(mut worker) = self.workers.get_mut(&worker_id) {
353 worker.health = result.status;
354 worker.last_seen = SystemTime::now();
355 }
356
357 if result.status == HealthStatus::Unhealthy && result.consecutive_failures > 3 {
359 warn!("Removing unhealthy worker: {}", worker_id);
360 self.workers.remove(&worker_id);
361 }
362 }
363 }
364
365 pub fn register_worker(&self, endpoint: WorkerEndpoint) -> Result<()> {
367 info!("Registering worker: {}", endpoint.worker_id);
368 self.workers.insert(endpoint.worker_id.clone(), endpoint);
369 Ok(())
370 }
371
372 pub fn unregister_worker(&self, worker_id: &str) -> Result<()> {
374 info!("Unregistering worker: {}", worker_id);
375 self.workers.remove(worker_id);
376 Ok(())
377 }
378
379 #[must_use]
381 pub fn get_workers(&self) -> Vec<WorkerEndpoint> {
382 self.workers.iter().map(|e| e.value().clone()).collect()
383 }
384
385 #[must_use]
387 pub fn get_healthy_workers(&self) -> Vec<WorkerEndpoint> {
388 self.workers
389 .iter()
390 .filter(|e| e.value().health == HealthStatus::Healthy)
391 .map(|e| e.value().clone())
392 .collect()
393 }
394
395 #[must_use]
397 pub fn get_worker(&self, worker_id: &str) -> Option<WorkerEndpoint> {
398 self.workers.get(worker_id).map(|e| e.value().clone())
399 }
400
401 #[must_use]
403 pub fn find_workers_by_capability(&self, required_codec: &str) -> Vec<WorkerEndpoint> {
404 self.workers
405 .iter()
406 .filter(|e| {
407 e.value().health == HealthStatus::Healthy
408 && e.value()
409 .capabilities
410 .codecs
411 .iter()
412 .any(|c| c == required_codec)
413 })
414 .map(|e| e.value().clone())
415 .collect()
416 }
417
418 #[must_use]
420 pub fn find_workers_by_region(&self, region: &str) -> Vec<WorkerEndpoint> {
421 self.workers
422 .iter()
423 .filter(|e| {
424 e.value()
425 .location
426 .as_ref()
427 .is_some_and(|l| l.region == region)
428 })
429 .map(|e| e.value().clone())
430 .collect()
431 }
432
433 #[must_use]
435 pub fn get_capacity_stats(&self) -> CapacityStats {
436 let workers = self.get_healthy_workers();
437
438 let total_cpu: u32 = workers.iter().map(|w| w.capabilities.cpu_cores).sum();
439 let total_memory: u64 = workers.iter().map(|w| w.capabilities.memory_bytes).sum();
440 let total_gpus: usize = workers
441 .iter()
442 .map(|w| w.capabilities.gpu_devices.len())
443 .sum();
444 let total_job_slots: u32 = workers.iter().map(|w| w.capabilities.max_jobs).sum();
445
446 CapacityStats {
447 total_workers: workers.len(),
448 total_cpu_cores: total_cpu,
449 total_memory_bytes: total_memory,
450 total_gpu_devices: total_gpus,
451 total_job_slots,
452 average_performance: workers
453 .iter()
454 .map(|w| w.capabilities.performance_score)
455 .sum::<f32>()
456 / workers.len().max(1) as f32,
457 }
458 }
459
460 pub async fn stop(&self) -> Result<()> {
462 info!("Stopping discovery service");
463 self.running.store(false, Ordering::Relaxed);
464 Ok(())
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct CapacityStats {
471 pub total_workers: usize,
472 pub total_cpu_cores: u32,
473 pub total_memory_bytes: u64,
474 pub total_gpu_devices: usize,
475 pub total_job_slots: u32,
476 pub average_performance: f32,
477}
478
479impl HealthChecker {
480 fn new() -> Self {
482 Self {
483 health_results: Arc::new(DashMap::new()),
484 stats: HealthStats::default(),
485 }
486 }
487
488 async fn check_worker(&self, endpoint: &WorkerEndpoint) -> HealthCheckResult {
490 self.stats.total_checks.fetch_add(1, Ordering::Relaxed);
491
492 let start = SystemTime::now();
493
494 let status = self.perform_health_check(endpoint).await;
496
497 let latency = start.elapsed().unwrap_or(Duration::ZERO);
498
499 let mut result = HealthCheckResult {
500 worker_id: endpoint.worker_id.clone(),
501 status,
502 latency,
503 last_check: SystemTime::now(),
504 consecutive_failures: 0,
505 };
506
507 if let Some(prev) = self.health_results.get(&endpoint.worker_id) {
509 if status == HealthStatus::Unhealthy {
510 result.consecutive_failures = prev.consecutive_failures + 1;
511 }
512 } else if status == HealthStatus::Unhealthy {
513 result.consecutive_failures = 1;
514 }
515
516 if status == HealthStatus::Healthy {
518 self.stats.successful_checks.fetch_add(1, Ordering::Relaxed);
519 } else {
520 self.stats.failed_checks.fetch_add(1, Ordering::Relaxed);
521 }
522
523 self.health_results
524 .insert(endpoint.worker_id.clone(), result.clone());
525
526 result
527 }
528
529 async fn perform_health_check(&self, _endpoint: &WorkerEndpoint) -> HealthStatus {
531 HealthStatus::Healthy
534 }
535
536 pub fn statistics(&self) -> HealthCheckStats {
538 HealthCheckStats {
539 total_checks: self.stats.total_checks.load(Ordering::Relaxed),
540 successful_checks: self.stats.successful_checks.load(Ordering::Relaxed),
541 failed_checks: self.stats.failed_checks.load(Ordering::Relaxed),
542 }
543 }
544}
545
546#[derive(Debug, Clone)]
548pub struct HealthCheckStats {
549 pub total_checks: u64,
550 pub successful_checks: u64,
551 pub failed_checks: u64,
552}
553
554impl HealthCheckStats {
555 #[must_use]
557 pub fn success_rate(&self) -> f64 {
558 if self.total_checks == 0 {
559 return 0.0;
560 }
561 self.successful_checks as f64 / self.total_checks as f64
562 }
563}
564
565pub struct WorkerRegistry {
567 workers: Arc<RwLock<HashMap<String, RegisteredWorker>>>,
569
570 backend: RegistryBackend,
572}
573
574#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
576pub struct RegisteredWorker {
577 worker_id: String,
578 address: String,
579 hostname: String,
580 registered_at: u64,
581 last_heartbeat: u64,
582 capabilities: serde_json::Value,
583 metadata: HashMap<String, String>,
584}
585
586#[derive(Debug, Clone, Copy)]
588pub enum RegistryBackend {
589 Memory,
590 Etcd,
591 Consul,
592}
593
594impl WorkerRegistry {
595 #[must_use]
597 pub fn new(backend: RegistryBackend) -> Self {
598 Self {
599 workers: Arc::new(RwLock::new(HashMap::new())),
600 backend,
601 }
602 }
603
604 pub async fn register(&self, worker_id: String, endpoint: WorkerEndpoint) -> Result<()> {
606 info!("Registering worker in registry: {}", worker_id);
607
608 let unix_now = SystemTime::now()
609 .duration_since(SystemTime::UNIX_EPOCH)
610 .unwrap_or(Duration::ZERO)
611 .as_secs();
612
613 let worker = RegisteredWorker {
614 worker_id: worker_id.clone(),
615 address: endpoint.address.to_string(),
616 hostname: endpoint.hostname,
617 registered_at: unix_now,
618 last_heartbeat: unix_now,
619 capabilities: serde_json::json!({}),
620 metadata: endpoint.metadata,
621 };
622
623 let mut workers = self.workers.write().await;
624 workers.insert(worker_id.clone(), worker.clone());
625
626 self.persist_worker(&worker).await?;
628
629 Ok(())
630 }
631
632 pub async fn unregister(&self, worker_id: &str) -> Result<()> {
634 info!("Unregistering worker from registry: {}", worker_id);
635
636 let mut workers = self.workers.write().await;
637 workers.remove(worker_id);
638
639 self.remove_worker(worker_id).await?;
641
642 Ok(())
643 }
644
645 pub async fn update_heartbeat(&self, worker_id: &str) -> Result<()> {
647 let mut workers = self.workers.write().await;
648
649 if let Some(worker) = workers.get_mut(worker_id) {
650 worker.last_heartbeat = SystemTime::now()
651 .duration_since(SystemTime::UNIX_EPOCH)
652 .unwrap_or(Duration::ZERO)
653 .as_secs();
654 }
655
656 Ok(())
657 }
658
659 async fn persist_worker(&self, _worker: &RegisteredWorker) -> Result<()> {
661 match self.backend {
662 RegistryBackend::Memory => Ok(()),
663 RegistryBackend::Etcd => {
664 Ok(())
666 }
667 RegistryBackend::Consul => {
668 Ok(())
670 }
671 }
672 }
673
674 async fn remove_worker(&self, _worker_id: &str) -> Result<()> {
676 match self.backend {
677 RegistryBackend::Memory => Ok(()),
678 RegistryBackend::Etcd => {
679 Ok(())
681 }
682 RegistryBackend::Consul => {
683 Ok(())
685 }
686 }
687 }
688
689 pub async fn get_all(&self) -> Vec<RegisteredWorker> {
691 let workers = self.workers.read().await;
692 workers.values().cloned().collect()
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use std::net::{IpAddr, Ipv4Addr};
700
701 #[test]
702 fn test_discovery_config() {
703 let config = DiscoveryConfig::default();
704 assert_eq!(config.service_name, "oximedia-worker");
705 assert_eq!(config.service_port, 50052);
706 }
707
708 #[test]
709 fn test_worker_capabilities() {
710 let caps = WorkerCapabilities::default();
711 assert_eq!(caps.cpu_cores, 1);
712 assert_eq!(caps.max_jobs, 2);
713 assert!(!caps.codecs.is_empty());
714 }
715
716 #[test]
717 fn test_health_status() {
718 assert_eq!(HealthStatus::Healthy, HealthStatus::Healthy);
719 assert_ne!(HealthStatus::Healthy, HealthStatus::Unhealthy);
720 }
721
722 #[test]
723 fn test_discovery_service_creation() {
724 let config = DiscoveryConfig::default();
725 let service = DiscoveryService::new(DiscoveryMethod::Static, config);
726 assert_eq!(service.method, DiscoveryMethod::Static);
727 assert_eq!(service.workers.len(), 0);
728 }
729
730 #[test]
731 fn test_worker_registration() {
732 let config = DiscoveryConfig::default();
733 let service = DiscoveryService::new(DiscoveryMethod::Static, config);
734
735 let endpoint = WorkerEndpoint {
736 worker_id: "test-worker".to_string(),
737 address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 50052),
738 hostname: "localhost".to_string(),
739 capabilities: WorkerCapabilities::default(),
740 health: HealthStatus::Healthy,
741 last_seen: SystemTime::now(),
742 location: None,
743 metadata: HashMap::new(),
744 };
745
746 assert!(service.register_worker(endpoint).is_ok());
747 assert_eq!(service.workers.len(), 1);
748 }
749
750 #[test]
751 fn test_capacity_stats() {
752 let config = DiscoveryConfig::default();
753 let service = DiscoveryService::new(DiscoveryMethod::Static, config);
754
755 let stats = service.get_capacity_stats();
756 assert_eq!(stats.total_workers, 0);
757 assert_eq!(stats.total_cpu_cores, 0);
758 }
759
760 #[test]
761 fn test_health_check_stats() {
762 let stats = HealthCheckStats {
763 total_checks: 100,
764 successful_checks: 95,
765 failed_checks: 5,
766 };
767
768 assert_eq!(stats.success_rate(), 0.95);
769 }
770
771 #[test]
772 fn test_geo_location() {
773 let location = GeoLocation {
774 region: "us-west-2".to_string(),
775 zone: Some("us-west-2a".to_string()),
776 datacenter: None,
777 latitude: Some(37.7749),
778 longitude: Some(-122.4194),
779 };
780
781 assert_eq!(location.region, "us-west-2");
782 assert!(location.latitude.is_some());
783 }
784}