1use 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
32pub struct Coordinator {
34 workers: Arc<DashMap<String, WorkerState>>,
36
37 jobs: Arc<DashMap<String, JobState>>,
39
40 scheduler: Arc<RwLock<JobScheduler>>,
42
43 stats: Arc<CoordinatorStats>,
45
46 config: CoordinatorConfig,
48
49 shutdown_tx: mpsc::Sender<()>,
51 #[allow(dead_code)]
52 shutdown_rx: Arc<RwLock<mpsc::Receiver<()>>>,
53}
54
55#[derive(Debug, Clone)]
57pub struct CoordinatorConfig {
58 pub max_workers: usize,
60
61 pub worker_timeout: Duration,
63
64 pub max_job_retries: u32,
66
67 pub enable_preemption: bool,
69
70 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum LoadBalancingStrategy {
89 LeastLoaded,
91 RoundRobin,
93 FastestFirst,
95 CapabilityBased,
97}
98
99#[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, 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#[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#[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 #[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 pub async fn serve(self, addr: SocketAddr) -> Result<()> {
264 info!("Starting coordinator on {}", addr);
265
266 let coordinator = Arc::new(self);
267
268 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 {
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 tokio::signal::ctrl_c().await?;
379 info!("Coordinator shutting down");
380 Ok(())
381 }
382
383 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 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 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 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 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 while let Some(scheduled_job) = scheduler.next_job() {
440 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 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 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 scheduler.enqueue(scheduled_job);
460 break;
461 }
462 }
463 }
464 }
465
466 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 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 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 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 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 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 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 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 self.workers.remove(worker_id);
587 info!("Worker {} removed after failure", worker_id);
588 }
589
590 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 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 #[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 pub async fn shutdown(&self) -> Result<()> {
651 info!("Shutting down coordinator");
652 let _ = self.shutdown_tx.send(()).await;
653 Ok(())
654 }
655}
656
657#[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#[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 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 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 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 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 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}