Skip to main content

oximedia_distributed/
coordinator.rs

1//! Central coordinator service for distributed encoding.
2//!
3//! The coordinator manages:
4//! - Worker registration and health monitoring
5//! - Job distribution and assignment
6//! - Progress tracking
7//! - Result collection and aggregation
8//! - Fault tolerance and job rescheduling
9
10use crate::pb::coordinator_service_server::CoordinatorService;
11use crate::pb::{
12    job_assignment, job_cancellation, job_status_request, worker_status_request,
13    CancellationResponse, EncodingTask, FailureAcknowledgment, HeartbeatResponse, JobAssignment,
14    JobCancellation, JobFailure, JobRequest, JobResult, JobStatusRequest, JobStatusResponse,
15    ProgressAcknowledgment, ProgressReport, ResultAcknowledgment, UnregistrationResponse,
16    WorkerHeartbeat, WorkerInfo, WorkerMetrics, WorkerRegistration, WorkerRegistrationResponse,
17    WorkerStatus, WorkerStatusRequest, WorkerStatusResponse, WorkerUnregistration,
18};
19use crate::scheduler::{JobScheduler, ScheduledJob};
20use crate::{JobPriority, Result};
21use dashmap::DashMap;
22use std::io::{BufRead, BufReader, Write};
23use std::net::{SocketAddr, TcpListener};
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::Arc;
26use std::time::{Duration, SystemTime, UNIX_EPOCH};
27use tokio::sync::{mpsc, RwLock};
28use tonic::{Request, Response, Status};
29use tracing::{debug, error, info, warn};
30use uuid::Uuid;
31
32/// Coordinator state and configuration
33pub struct Coordinator {
34    /// Worker registry
35    workers: Arc<DashMap<String, WorkerState>>,
36
37    /// Job registry
38    jobs: Arc<DashMap<String, JobState>>,
39
40    /// Job scheduler
41    scheduler: Arc<RwLock<JobScheduler>>,
42
43    /// Statistics
44    stats: Arc<CoordinatorStats>,
45
46    /// Configuration
47    config: CoordinatorConfig,
48
49    /// Shutdown signal
50    shutdown_tx: mpsc::Sender<()>,
51    #[allow(dead_code)]
52    shutdown_rx: Arc<RwLock<mpsc::Receiver<()>>>,
53}
54
55/// Coordinator configuration
56#[derive(Debug, Clone)]
57pub struct CoordinatorConfig {
58    /// Maximum workers
59    pub max_workers: usize,
60
61    /// Worker timeout
62    pub worker_timeout: Duration,
63
64    /// Job retry limit
65    pub max_job_retries: u32,
66
67    /// Enable preemption
68    pub enable_preemption: bool,
69
70    /// Load balancing strategy
71    pub load_balancing: LoadBalancingStrategy,
72}
73
74impl Default for CoordinatorConfig {
75    fn default() -> Self {
76        Self {
77            max_workers: 1000,
78            worker_timeout: Duration::from_secs(90),
79            max_job_retries: 3,
80            enable_preemption: false,
81            load_balancing: LoadBalancingStrategy::LeastLoaded,
82        }
83    }
84}
85
86/// Load balancing strategies
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum LoadBalancingStrategy {
89    /// Assign to worker with fewest jobs
90    LeastLoaded,
91    /// Round-robin assignment
92    RoundRobin,
93    /// Assign to fastest worker
94    FastestFirst,
95    /// Consider worker capabilities
96    CapabilityBased,
97}
98
99/// Worker state tracking
100#[allow(dead_code)]
101#[derive(Debug, Clone)]
102struct WorkerState {
103    worker_id: String,
104    hostname: String,
105    ip_address: String,
106    port: u32,
107    capabilities: worker_capabilities::Capabilities,
108    status: worker_status::State,
109    active_jobs: Vec<String>,
110    metrics: worker_metrics::Metrics,
111    last_heartbeat: SystemTime,
112    total_jobs_completed: u64,
113    total_jobs_failed: u64,
114}
115
116mod worker_capabilities {
117    #[allow(dead_code)]
118    #[derive(Debug, Clone)]
119    pub struct Capabilities {
120        pub cpu_cores: u32,
121        pub memory_bytes: u64,
122        pub gpu_devices: Vec<String>,
123        pub supported_codecs: Vec<String>,
124        pub supported_hwaccels: Vec<String>,
125        pub relative_speed: f32,
126        pub max_concurrent_jobs: u32,
127    }
128
129    impl Default for Capabilities {
130        fn default() -> Self {
131            Self {
132                cpu_cores: 1,
133                memory_bytes: 1_073_741_824, // 1 GB
134                gpu_devices: Vec::new(),
135                supported_codecs: vec!["h264".to_string()],
136                supported_hwaccels: Vec::new(),
137                relative_speed: 1.0,
138                max_concurrent_jobs: 2,
139            }
140        }
141    }
142}
143
144mod worker_status {
145    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
146    pub enum State {
147        Idle,
148        Busy,
149        Full,
150        Draining,
151        Error,
152    }
153
154    impl From<i32> for State {
155        fn from(value: i32) -> Self {
156            match value {
157                0 => State::Idle,
158                1 => State::Busy,
159                2 => State::Full,
160                3 => State::Draining,
161                4 => State::Error,
162                _ => State::Error,
163            }
164        }
165    }
166
167    impl From<State> for i32 {
168        fn from(state: State) -> Self {
169            match state {
170                State::Idle => 0,
171                State::Busy => 1,
172                State::Full => 2,
173                State::Draining => 3,
174                State::Error => 4,
175            }
176        }
177    }
178}
179
180mod worker_metrics {
181    #[allow(dead_code)]
182    #[derive(Debug, Clone, Default)]
183    pub struct Metrics {
184        pub cpu_usage: f32,
185        pub memory_usage: f32,
186        pub gpu_usage: f32,
187        pub bytes_processed: u64,
188        pub frames_encoded: u32,
189    }
190}
191
192/// Job state tracking
193#[allow(dead_code)]
194#[derive(Debug, Clone)]
195struct JobState {
196    job_id: String,
197    task_id: String,
198    assigned_worker: Option<String>,
199    status: job_status::State,
200    priority: u32,
201    retry_count: u32,
202    progress: f32,
203    created_at: SystemTime,
204    started_at: Option<SystemTime>,
205    completed_at: Option<SystemTime>,
206    encoding_task: Option<EncodingTask>,
207}
208
209mod job_status {
210    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
211    pub enum State {
212        Pending,
213        Assigned,
214        InProgress,
215        Completed,
216        Failed,
217        Cancelled,
218    }
219
220    impl From<State> for i32 {
221        fn from(state: State) -> Self {
222            match state {
223                State::Pending => 0,
224                State::Assigned => 1,
225                State::InProgress => 2,
226                State::Completed => 3,
227                State::Failed => 4,
228                State::Cancelled => 5,
229            }
230        }
231    }
232}
233
234/// Coordinator statistics
235#[derive(Debug, Default)]
236struct CoordinatorStats {
237    total_workers: AtomicU64,
238    active_workers: AtomicU64,
239    total_jobs_submitted: AtomicU64,
240    total_jobs_completed: AtomicU64,
241    total_jobs_failed: AtomicU64,
242    total_bytes_processed: AtomicU64,
243}
244
245impl Coordinator {
246    /// Create a new coordinator
247    #[must_use]
248    pub fn new(config: CoordinatorConfig) -> Self {
249        let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
250
251        Self {
252            workers: Arc::new(DashMap::new()),
253            jobs: Arc::new(DashMap::new()),
254            scheduler: Arc::new(RwLock::new(JobScheduler::new())),
255            stats: Arc::new(CoordinatorStats::default()),
256            config,
257            shutdown_tx,
258            shutdown_rx: Arc::new(RwLock::new(shutdown_rx)),
259        }
260    }
261
262    /// Start the coordinator server
263    pub async fn serve(self, addr: SocketAddr) -> Result<()> {
264        info!("Starting coordinator on {}", addr);
265
266        let coordinator = Arc::new(self);
267
268        // Start background tasks
269        let coord_clone = coordinator.clone();
270        tokio::spawn(async move {
271            coord_clone.health_check_loop().await;
272        });
273
274        let coord_clone = coordinator.clone();
275        tokio::spawn(async move {
276            coord_clone.job_assignment_loop().await;
277        });
278
279        // Start in-process TCP control server on configured port.
280        // Accepts basic text commands: "status", "nodes", "jobs" and returns JSON responses.
281        {
282            let coord_for_tcp = coordinator.clone();
283            let tcp_addr = addr;
284            std::thread::spawn(move || {
285                let listener = match TcpListener::bind(tcp_addr) {
286                    Ok(l) => l,
287                    Err(e) => {
288                        error!("TCP control server failed to bind {}: {}", tcp_addr, e);
289                        return;
290                    }
291                };
292                info!("TCP control server listening on {}", tcp_addr);
293
294                for stream in listener.incoming() {
295                    match stream {
296                        Ok(mut tcp_stream) => {
297                            let coord = coord_for_tcp.clone();
298                            let cloned = match tcp_stream.try_clone() {
299                                Ok(s) => s,
300                                Err(e) => {
301                                    debug!("TCP stream clone error: {}", e);
302                                    continue;
303                                }
304                            };
305                            std::thread::spawn(move || {
306                                let reader = BufReader::new(cloned);
307                                for line in reader.lines() {
308                                    let command = match line {
309                                        Ok(l) => l.trim().to_lowercase(),
310                                        Err(_) => break,
311                                    };
312
313                                    let response = match command.as_str() {
314                                        "status" => {
315                                            let stats = coord.stats();
316                                            format!(
317                                                "{{\"total_workers\":{},\"active_workers\":{},\"total_jobs_submitted\":{},\"total_jobs_completed\":{},\"total_jobs_failed\":{},\"total_bytes_processed\":{}}}\n",
318                                                stats.total_workers,
319                                                stats.active_workers,
320                                                stats.total_jobs_submitted,
321                                                stats.total_jobs_completed,
322                                                stats.total_jobs_failed,
323                                                stats.total_bytes_processed,
324                                            )
325                                        }
326                                        "nodes" => {
327                                            let workers: Vec<String> = coord
328                                                .workers
329                                                .iter()
330                                                .map(|e| {
331                                                    format!(
332                                                        "{{\"id\":\"{}\",\"host\":\"{}\",\"jobs\":{}}}",
333                                                        e.value().worker_id,
334                                                        e.value().hostname,
335                                                        e.value().active_jobs.len(),
336                                                    )
337                                                })
338                                                .collect();
339                                            format!("[{}]\n", workers.join(","))
340                                        }
341                                        "jobs" => {
342                                            let jobs: Vec<String> = coord
343                                                .jobs
344                                                .iter()
345                                                .map(|e| {
346                                                    let state: i32 =
347                                                        e.value().status
348                                                            .into();
349                                                    format!(
350                                                        "{{\"id\":\"{}\",\"state\":{},\"progress\":{}}}",
351                                                        e.value().job_id,
352                                                        state,
353                                                        e.value().progress,
354                                                    )
355                                                })
356                                                .collect();
357                                            format!("[{}]\n", jobs.join(","))
358                                        }
359                                        "" => continue,
360                                        _ => "{\"error\":\"unknown command; try: status, nodes, jobs\"}\n".to_string(),
361                                    };
362
363                                    if tcp_stream.write_all(response.as_bytes()).is_err() {
364                                        break;
365                                    }
366                                }
367                            });
368                        }
369                        Err(e) => {
370                            debug!("TCP control accept error: {}", e);
371                        }
372                    }
373                }
374            });
375        }
376
377        // Wait for shutdown signal
378        tokio::signal::ctrl_c().await?;
379        info!("Coordinator shutting down");
380        Ok(())
381    }
382
383    /// Health check loop for monitoring workers
384    async fn health_check_loop(&self) {
385        let mut interval = tokio::time::interval(Duration::from_secs(30));
386
387        loop {
388            interval.tick().await;
389
390            let now = SystemTime::now();
391            let timeout = self.config.worker_timeout;
392
393            // Check for timed-out workers
394            let timed_out_workers: Vec<String> = self
395                .workers
396                .iter()
397                .filter_map(|entry| {
398                    let worker_id = entry.key().clone();
399                    let worker = entry.value();
400
401                    if let Ok(elapsed) = now.duration_since(worker.last_heartbeat) {
402                        if elapsed > timeout {
403                            return Some(worker_id);
404                        }
405                    }
406                    None
407                })
408                .collect();
409
410            // Handle timed-out workers
411            for worker_id in timed_out_workers {
412                warn!("Worker {} timed out, marking as failed", worker_id);
413                self.handle_worker_failure(&worker_id).await;
414            }
415
416            // Update statistics
417            self.stats
418                .active_workers
419                .store(self.workers.len() as u64, Ordering::Relaxed);
420
421            debug!(
422                "Health check: {} active workers, {} jobs",
423                self.workers.len(),
424                self.jobs.len()
425            );
426        }
427    }
428
429    /// Job assignment loop
430    async fn job_assignment_loop(&self) {
431        let mut interval = tokio::time::interval(Duration::from_secs(5));
432
433        loop {
434            interval.tick().await;
435
436            let mut scheduler = self.scheduler.write().await;
437
438            // Get pending jobs from scheduler
439            while let Some(scheduled_job) = scheduler.next_job() {
440                // Find suitable worker
441                if let Some(worker_id) = self.find_suitable_worker(&scheduled_job).await {
442                    debug!(
443                        "Assigning job {} to worker {}",
444                        scheduled_job.job_id, worker_id
445                    );
446
447                    // Update job state
448                    if let Some(mut job) = self.jobs.get_mut(&scheduled_job.job_id) {
449                        job.assigned_worker = Some(worker_id.clone());
450                        job.status = job_status::State::Assigned;
451                    }
452
453                    // Update worker state
454                    if let Some(mut worker) = self.workers.get_mut(&worker_id) {
455                        worker.active_jobs.push(scheduled_job.job_id.clone());
456                    }
457                } else {
458                    // No suitable worker, put job back in queue
459                    scheduler.enqueue(scheduled_job);
460                    break;
461                }
462            }
463        }
464    }
465
466    /// Find a suitable worker for a job
467    async fn find_suitable_worker(&self, job: &ScheduledJob) -> Option<String> {
468        match self.config.load_balancing {
469            LoadBalancingStrategy::LeastLoaded => self.find_least_loaded_worker(),
470            LoadBalancingStrategy::RoundRobin => self.find_round_robin_worker(),
471            LoadBalancingStrategy::FastestFirst => self.find_fastest_worker(),
472            LoadBalancingStrategy::CapabilityBased => self.find_capability_worker(job),
473        }
474    }
475
476    fn find_least_loaded_worker(&self) -> Option<String> {
477        self.workers
478            .iter()
479            .filter(|entry| {
480                let worker = entry.value();
481                worker.status != worker_status::State::Full
482                    && worker.status != worker_status::State::Error
483                    && worker.status != worker_status::State::Draining
484            })
485            .min_by_key(|entry| entry.value().active_jobs.len())
486            .map(|entry| entry.key().clone())
487    }
488
489    fn find_round_robin_worker(&self) -> Option<String> {
490        // Simple round-robin based on total jobs completed
491        self.workers
492            .iter()
493            .filter(|entry| {
494                let worker = entry.value();
495                worker.status != worker_status::State::Full
496                    && worker.status != worker_status::State::Error
497            })
498            .min_by_key(|entry| entry.value().total_jobs_completed)
499            .map(|entry| entry.key().clone())
500    }
501
502    fn find_fastest_worker(&self) -> Option<String> {
503        self.workers
504            .iter()
505            .filter(|entry| {
506                let worker = entry.value();
507                worker.status != worker_status::State::Full
508                    && worker.status != worker_status::State::Error
509            })
510            .max_by(|a, b| {
511                a.value()
512                    .capabilities
513                    .relative_speed
514                    .partial_cmp(&b.value().capabilities.relative_speed)
515                    .unwrap_or(std::cmp::Ordering::Equal)
516            })
517            .map(|entry| entry.key().clone())
518    }
519
520    fn find_capability_worker(&self, job: &ScheduledJob) -> Option<String> {
521        // Find worker with best match for job requirements
522        self.workers
523            .iter()
524            .filter(|entry| {
525                let worker = entry.value();
526                if worker.status == worker_status::State::Full
527                    || worker.status == worker_status::State::Error
528                {
529                    return false;
530                }
531
532                // Check codec support
533                worker.capabilities.supported_codecs.iter().any(|codec| {
534                    job.encoding_task
535                        .as_ref()
536                        .is_some_and(|task| task.codec == *codec)
537                })
538            })
539            .max_by_key(|entry| {
540                let worker = entry.value();
541                // Score based on relative speed and current load
542                let load_factor = 1.0
543                    - (worker.active_jobs.len() as f32
544                        / worker.capabilities.max_concurrent_jobs as f32);
545                (worker.capabilities.relative_speed * load_factor * 1000.0) as u64
546            })
547            .map(|entry| entry.key().clone())
548    }
549
550    /// Handle worker failure
551    async fn handle_worker_failure(&self, worker_id: &str) {
552        if let Some(worker) = self.workers.get(worker_id) {
553            let failed_jobs = worker.active_jobs.clone();
554
555            // Reschedule failed jobs
556            for job_id in failed_jobs {
557                if let Some(mut job) = self.jobs.get_mut(&job_id) {
558                    job.retry_count += 1;
559
560                    if job.retry_count < self.config.max_job_retries {
561                        info!("Rescheduling job {} after worker failure", job_id);
562                        job.assigned_worker = None;
563                        job.status = job_status::State::Pending;
564
565                        // Add back to scheduler
566                        let mut scheduler = self.scheduler.write().await;
567                        if let Some(task) = &job.encoding_task {
568                            scheduler.enqueue(ScheduledJob {
569                                job_id: job_id.clone(),
570                                task_id: job.task_id.clone(),
571                                priority: JobPriority::Normal,
572                                deadline: None,
573                                encoding_task: Some(task.clone()),
574                            });
575                        }
576                    } else {
577                        error!("Job {} failed after {} retries", job_id, job.retry_count);
578                        job.status = job_status::State::Failed;
579                        self.stats.total_jobs_failed.fetch_add(1, Ordering::Relaxed);
580                    }
581                }
582            }
583        }
584
585        // Remove worker
586        self.workers.remove(worker_id);
587        info!("Worker {} removed after failure", worker_id);
588    }
589
590    /// Submit a new encoding job
591    pub async fn submit_job(&self, task: EncodingTask, priority: u32) -> Result<String> {
592        let job_id = Uuid::new_v4().to_string();
593        let task_id = task.task_id.clone();
594
595        let job_state = JobState {
596            job_id: job_id.clone(),
597            task_id: task_id.clone(),
598            assigned_worker: None,
599            status: job_status::State::Pending,
600            priority,
601            retry_count: 0,
602            progress: 0.0,
603            created_at: SystemTime::now(),
604            started_at: None,
605            completed_at: None,
606            encoding_task: Some(task.clone()),
607        };
608
609        self.jobs.insert(job_id.clone(), job_state);
610
611        // Add to scheduler
612        let mut scheduler = self.scheduler.write().await;
613        let priority_enum = match priority {
614            3 => JobPriority::Critical,
615            2 => JobPriority::High,
616            1 => JobPriority::Normal,
617            _ => JobPriority::Low,
618        };
619
620        scheduler.enqueue(ScheduledJob {
621            job_id: job_id.clone(),
622            task_id,
623            priority: priority_enum,
624            deadline: None,
625            encoding_task: Some(task),
626        });
627
628        self.stats
629            .total_jobs_submitted
630            .fetch_add(1, Ordering::Relaxed);
631
632        info!("Job {} submitted with priority {}", job_id, priority);
633        Ok(job_id)
634    }
635
636    /// Get coordinator statistics
637    #[must_use]
638    pub fn stats(&self) -> CoordinatorStatistics {
639        CoordinatorStatistics {
640            total_workers: self.stats.total_workers.load(Ordering::Relaxed),
641            active_workers: self.stats.active_workers.load(Ordering::Relaxed),
642            total_jobs_submitted: self.stats.total_jobs_submitted.load(Ordering::Relaxed),
643            total_jobs_completed: self.stats.total_jobs_completed.load(Ordering::Relaxed),
644            total_jobs_failed: self.stats.total_jobs_failed.load(Ordering::Relaxed),
645            total_bytes_processed: self.stats.total_bytes_processed.load(Ordering::Relaxed),
646        }
647    }
648
649    /// Shutdown the coordinator
650    pub async fn shutdown(&self) -> Result<()> {
651        info!("Shutting down coordinator");
652        let _ = self.shutdown_tx.send(()).await;
653        Ok(())
654    }
655}
656
657/// Coordinator statistics snapshot
658#[derive(Debug, Clone)]
659pub struct CoordinatorStatistics {
660    pub total_workers: u64,
661    pub active_workers: u64,
662    pub total_jobs_submitted: u64,
663    pub total_jobs_completed: u64,
664    pub total_jobs_failed: u64,
665    pub total_bytes_processed: u64,
666}
667
668/// gRPC service implementation
669#[derive(Clone)]
670struct CoordinatorServiceImpl {
671    coordinator: Arc<Coordinator>,
672}
673
674#[tonic::async_trait]
675impl CoordinatorService for CoordinatorServiceImpl {
676    async fn register_worker(
677        &self,
678        request: Request<WorkerRegistration>,
679    ) -> std::result::Result<Response<WorkerRegistrationResponse>, Status> {
680        let registration = request.into_inner();
681        let worker_id = if registration.worker_id.is_empty() {
682            Uuid::new_v4().to_string()
683        } else {
684            registration.worker_id
685        };
686
687        info!("Registering worker: {}", worker_id);
688
689        // Check worker limit
690        if self.coordinator.workers.len() >= self.coordinator.config.max_workers {
691            return Ok(Response::new(WorkerRegistrationResponse {
692                success: false,
693                message: "Maximum worker limit reached".to_string(),
694                assigned_worker_id: String::new(),
695            }));
696        }
697
698        let capabilities = registration
699            .capabilities
700            .map(|c| worker_capabilities::Capabilities {
701                cpu_cores: c.cpu_cores,
702                memory_bytes: c.memory_bytes,
703                gpu_devices: c.gpu_devices,
704                supported_codecs: c.supported_codecs,
705                supported_hwaccels: c.supported_hwaccels,
706                relative_speed: c.relative_speed,
707                max_concurrent_jobs: c.max_concurrent_jobs,
708            })
709            .unwrap_or_default();
710
711        let worker_state = WorkerState {
712            worker_id: worker_id.clone(),
713            hostname: registration.hostname,
714            ip_address: registration.ip_address,
715            port: registration.port,
716            capabilities,
717            status: worker_status::State::Idle,
718            active_jobs: Vec::new(),
719            metrics: worker_metrics::Metrics::default(),
720            last_heartbeat: SystemTime::now(),
721            total_jobs_completed: 0,
722            total_jobs_failed: 0,
723        };
724
725        self.coordinator
726            .workers
727            .insert(worker_id.clone(), worker_state);
728        self.coordinator
729            .stats
730            .total_workers
731            .fetch_add(1, Ordering::Relaxed);
732
733        Ok(Response::new(WorkerRegistrationResponse {
734            success: true,
735            message: "Worker registered successfully".to_string(),
736            assigned_worker_id: worker_id,
737        }))
738    }
739
740    async fn heartbeat(
741        &self,
742        request: Request<WorkerHeartbeat>,
743    ) -> std::result::Result<Response<HeartbeatResponse>, Status> {
744        let heartbeat = request.into_inner();
745        let worker_id = heartbeat.worker_id;
746
747        if let Some(mut worker) = self.coordinator.workers.get_mut(&worker_id) {
748            worker.last_heartbeat = SystemTime::now();
749
750            if let Some(status) = heartbeat.status {
751                worker.status = worker_status::State::from(status.state);
752            }
753
754            if let Some(metrics) = heartbeat.metrics {
755                worker.metrics = worker_metrics::Metrics {
756                    cpu_usage: metrics.cpu_usage,
757                    memory_usage: metrics.memory_usage,
758                    gpu_usage: metrics.gpu_usage,
759                    bytes_processed: metrics.bytes_processed,
760                    frames_encoded: metrics.frames_encoded,
761                };
762            }
763
764            worker.active_jobs = heartbeat.active_job_ids;
765        } else {
766            return Err(Status::not_found(format!("Worker {worker_id} not found")));
767        }
768
769        Ok(Response::new(HeartbeatResponse {
770            acknowledged: true,
771            jobs_to_cancel: Vec::new(),
772            should_drain: false,
773        }))
774    }
775
776    async fn unregister_worker(
777        &self,
778        request: Request<WorkerUnregistration>,
779    ) -> std::result::Result<Response<UnregistrationResponse>, Status> {
780        let unreg = request.into_inner();
781        info!(
782            "Unregistering worker: {} ({})",
783            unreg.worker_id, unreg.reason
784        );
785
786        self.coordinator.workers.remove(&unreg.worker_id);
787
788        Ok(Response::new(UnregistrationResponse { success: true }))
789    }
790
791    async fn request_job(
792        &self,
793        request: Request<JobRequest>,
794    ) -> std::result::Result<Response<JobAssignment>, Status> {
795        let req = request.into_inner();
796        let worker_id = req.worker_id;
797
798        // Find jobs assigned to this worker
799        let assigned_jobs: Vec<job_assignment::Job> = self
800            .coordinator
801            .jobs
802            .iter()
803            .filter(|entry| {
804                entry.value().assigned_worker.as_ref() == Some(&worker_id)
805                    && entry.value().status == job_status::State::Assigned
806            })
807            .take(req.max_jobs as usize)
808            .map(|entry| {
809                let job = entry.value();
810                job_assignment::Job {
811                    job_id: job.job_id.clone(),
812                    task: job.encoding_task.clone().map(Box::new),
813                    priority: job.priority,
814                    deadline_timestamp: 0,
815                }
816            })
817            .collect();
818
819        Ok(Response::new(JobAssignment {
820            jobs: assigned_jobs,
821            has_more: false,
822        }))
823    }
824
825    async fn report_progress(
826        &self,
827        request: Request<ProgressReport>,
828    ) -> std::result::Result<Response<ProgressAcknowledgment>, Status> {
829        let progress = request.into_inner();
830
831        if let Some(mut job) = self.coordinator.jobs.get_mut(&progress.job_id) {
832            job.progress = progress.progress;
833            if job.status == job_status::State::Assigned {
834                job.status = job_status::State::InProgress;
835                job.started_at = Some(SystemTime::now());
836            }
837        }
838
839        Ok(Response::new(ProgressAcknowledgment { acknowledged: true }))
840    }
841
842    async fn submit_result(
843        &self,
844        request: Request<JobResult>,
845    ) -> std::result::Result<Response<ResultAcknowledgment>, Status> {
846        let result = request.into_inner();
847        info!(
848            "Job {} completed by worker {}",
849            result.job_id, result.worker_id
850        );
851
852        if let Some(mut job) = self.coordinator.jobs.get_mut(&result.job_id) {
853            job.status = job_status::State::Completed;
854            job.completed_at = Some(SystemTime::now());
855
856            // Update worker stats
857            if let Some(mut worker) = self.coordinator.workers.get_mut(&result.worker_id) {
858                worker.total_jobs_completed += 1;
859                worker.active_jobs.retain(|j| j != &result.job_id);
860            }
861
862            self.coordinator
863                .stats
864                .total_jobs_completed
865                .fetch_add(1, Ordering::Relaxed);
866            self.coordinator
867                .stats
868                .total_bytes_processed
869                .fetch_add(result.output_size, Ordering::Relaxed);
870        }
871
872        Ok(Response::new(ResultAcknowledgment {
873            acknowledged: true,
874            next_job_id: String::new(),
875        }))
876    }
877
878    async fn report_failure(
879        &self,
880        request: Request<JobFailure>,
881    ) -> std::result::Result<Response<FailureAcknowledgment>, Status> {
882        let failure = request.into_inner();
883        error!(
884            "Job {} failed on worker {}: {}",
885            failure.job_id, failure.worker_id, failure.error_message
886        );
887
888        let should_retry = if let Some(mut job) = self.coordinator.jobs.get_mut(&failure.job_id) {
889            job.retry_count += 1;
890
891            if failure.is_transient && job.retry_count < self.coordinator.config.max_job_retries {
892                job.assigned_worker = None;
893                job.status = job_status::State::Pending;
894                true
895            } else {
896                job.status = job_status::State::Failed;
897                self.coordinator
898                    .stats
899                    .total_jobs_failed
900                    .fetch_add(1, Ordering::Relaxed);
901                false
902            }
903        } else {
904            false
905        };
906
907        // Update worker stats
908        if let Some(mut worker) = self.coordinator.workers.get_mut(&failure.worker_id) {
909            worker.total_jobs_failed += 1;
910            worker.active_jobs.retain(|j| j != &failure.job_id);
911        }
912
913        Ok(Response::new(FailureAcknowledgment {
914            should_retry,
915            reassigned_job_id: String::new(),
916        }))
917    }
918
919    async fn get_worker_status(
920        &self,
921        request: Request<WorkerStatusRequest>,
922    ) -> std::result::Result<Response<WorkerStatusResponse>, Status> {
923        let req = request.into_inner();
924
925        let workers = match req.query {
926            Some(worker_status_request::Query::WorkerId(id)) => {
927                if let Some(worker) = self.coordinator.workers.get(&id) {
928                    vec![self.worker_to_info(&worker)]
929                } else {
930                    Vec::new()
931                }
932            }
933            Some(worker_status_request::Query::AllWorkers(true)) | None => self
934                .coordinator
935                .workers
936                .iter()
937                .map(|entry| self.worker_to_info(&entry))
938                .collect(),
939            _ => Vec::new(),
940        };
941
942        Ok(Response::new(WorkerStatusResponse { workers }))
943    }
944
945    async fn get_job_status(
946        &self,
947        request: Request<JobStatusRequest>,
948    ) -> std::result::Result<Response<JobStatusResponse>, Status> {
949        let req = request.into_inner();
950
951        let job_id = match req.query {
952            Some(job_status_request::Query::JobId(id)) => id,
953            Some(job_status_request::Query::TaskId(_task_id)) => {
954                // Find first job for task
955                String::new()
956            }
957            None => String::new(),
958        };
959
960        if let Some(job) = self.coordinator.jobs.get(&job_id) {
961            let started = job
962                .started_at
963                .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
964                .map_or(0, |d| d.as_secs() as i64);
965
966            let completed = job
967                .completed_at
968                .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
969                .map_or(0, |d| d.as_secs() as i64);
970
971            Ok(Response::new(JobStatusResponse {
972                job_id: job.job_id.clone(),
973                state: job.status.into(),
974                assigned_worker_id: job.assigned_worker.clone().unwrap_or_default(),
975                progress: job.progress,
976                started_timestamp: started,
977                completed_timestamp: completed,
978            }))
979        } else {
980            Err(Status::not_found(format!("Job {job_id} not found")))
981        }
982    }
983
984    async fn cancel_job(
985        &self,
986        request: Request<JobCancellation>,
987    ) -> std::result::Result<Response<CancellationResponse>, Status> {
988        let cancellation = request.into_inner();
989
990        let mut cancelled_jobs = Vec::new();
991
992        match cancellation.target {
993            Some(job_cancellation::Target::JobId(job_id)) => {
994                if let Some(mut job) = self.coordinator.jobs.get_mut(&job_id) {
995                    job.status = job_status::State::Cancelled;
996                    cancelled_jobs.push(job_id);
997                }
998            }
999            Some(job_cancellation::Target::TaskId(task_id)) => {
1000                for mut entry in self.coordinator.jobs.iter_mut() {
1001                    if entry.value().task_id == task_id {
1002                        entry.value_mut().status = job_status::State::Cancelled;
1003                        cancelled_jobs.push(entry.key().clone());
1004                    }
1005                }
1006            }
1007            None => {}
1008        }
1009
1010        Ok(Response::new(CancellationResponse {
1011            success: !cancelled_jobs.is_empty(),
1012            cancelled_job_ids: cancelled_jobs,
1013        }))
1014    }
1015}
1016
1017impl CoordinatorServiceImpl {
1018    #[allow(dead_code)]
1019    fn worker_to_info(&self, worker: &WorkerState) -> WorkerInfo {
1020        let last_heartbeat = worker
1021            .last_heartbeat
1022            .duration_since(UNIX_EPOCH)
1023            .map(|d| d.as_secs() as i64)
1024            .unwrap_or(0);
1025
1026        WorkerInfo {
1027            worker_id: worker.worker_id.clone(),
1028            hostname: worker.hostname.clone(),
1029            status: Some(Box::new(WorkerStatus {
1030                state: worker.status.into(),
1031                active_jobs: worker.active_jobs.len() as u32,
1032                queued_jobs: 0,
1033            })),
1034            metrics: Some(Box::new(WorkerMetrics {
1035                cpu_usage: worker.metrics.cpu_usage,
1036                memory_usage: worker.metrics.memory_usage,
1037                gpu_usage: worker.metrics.gpu_usage,
1038                bytes_processed: worker.metrics.bytes_processed,
1039                frames_encoded: worker.metrics.frames_encoded,
1040            })),
1041            last_heartbeat_timestamp: last_heartbeat,
1042        }
1043    }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use super::*;
1049
1050    #[test]
1051    fn test_coordinator_creation() {
1052        let config = CoordinatorConfig::default();
1053        let coordinator = Coordinator::new(config);
1054        assert_eq!(coordinator.workers.len(), 0);
1055        assert_eq!(coordinator.jobs.len(), 0);
1056    }
1057
1058    #[test]
1059    fn test_worker_status_conversion() {
1060        assert_eq!(i32::from(worker_status::State::Idle), 0);
1061        assert_eq!(i32::from(worker_status::State::Busy), 1);
1062        assert_eq!(worker_status::State::from(0), worker_status::State::Idle);
1063    }
1064
1065    #[test]
1066    fn test_load_balancing_strategies() {
1067        let config = CoordinatorConfig {
1068            load_balancing: LoadBalancingStrategy::LeastLoaded,
1069            ..Default::default()
1070        };
1071        assert_eq!(config.load_balancing, LoadBalancingStrategy::LeastLoaded);
1072    }
1073}