1use crate::pb;
11use crate::pb::coordinator_service_client::CoordinatorServiceClient;
12use crate::pb::{
13 EncodingTask, JobFailure, JobRequest, JobResult, ProgressReport, ResultMetadata,
14 WorkerHeartbeat, WorkerRegistration, WorkerUnregistration,
15};
16use crate::{DistributedError, Result};
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
20use std::sync::Arc;
21use std::time::{Duration, SystemTime, UNIX_EPOCH};
22use tokio::sync::{mpsc, RwLock};
23use tonic::transport::Channel;
24use tonic::Request;
25use tracing::{debug, error, info};
26use uuid::Uuid;
27
28pub struct Worker {
30 config: WorkerConfig,
32
33 worker_id: Arc<RwLock<String>>,
35
36 client: Arc<RwLock<Option<CoordinatorServiceClient<Channel>>>>,
38
39 active_jobs: Arc<RwLock<HashMap<String, ActiveJob>>>,
41
42 state: Arc<WorkerState>,
44
45 shutdown: Arc<AtomicBool>,
47}
48
49#[derive(Debug, Clone)]
51pub struct WorkerConfig {
52 pub coordinator_addr: String,
54
55 pub hostname: String,
57
58 pub ip_address: String,
60
61 pub port: u32,
63
64 pub heartbeat_interval: Duration,
66
67 pub poll_interval: Duration,
69
70 pub max_concurrent_jobs: u32,
72
73 pub capabilities: WorkerCapabilities,
75
76 pub work_dir: PathBuf,
78
79 pub enable_gpu: bool,
81}
82
83impl Default for WorkerConfig {
84 fn default() -> Self {
85 Self {
86 coordinator_addr: "http://127.0.0.1:50051".to_string(),
87 hostname: hostname::get()
88 .ok()
89 .and_then(|h| h.into_string().ok())
90 .unwrap_or_else(|| "unknown".to_string()),
91 ip_address: "127.0.0.1".to_string(),
92 port: 50052,
93 heartbeat_interval: Duration::from_secs(30),
94 poll_interval: Duration::from_secs(5),
95 max_concurrent_jobs: 4,
96 capabilities: WorkerCapabilities::detect(),
97 work_dir: std::env::temp_dir(),
98 enable_gpu: false,
99 }
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct WorkerCapabilities {
106 pub cpu_cores: u32,
107 pub memory_bytes: u64,
108 pub gpu_devices: Vec<String>,
109 pub supported_codecs: Vec<String>,
110 pub supported_hwaccels: Vec<String>,
111 pub relative_speed: f32,
112}
113
114impl WorkerCapabilities {
115 #[must_use]
117 pub fn detect() -> Self {
118 let cpu_cores = num_cpus::get() as u32;
119 let memory_bytes = Self::detect_memory();
120 let gpu_devices = Self::detect_gpus();
121 let supported_codecs = Self::detect_codecs();
122 let supported_hwaccels = Self::detect_hwaccels();
123 let relative_speed = Self::benchmark_speed();
124
125 Self {
126 cpu_cores,
127 memory_bytes,
128 gpu_devices,
129 supported_codecs,
130 supported_hwaccels,
131 relative_speed,
132 }
133 }
134
135 fn detect_memory() -> u64 {
136 #[cfg(target_os = "linux")]
138 {
139 let mut sys = sysinfo::System::new();
140 sys.refresh_memory();
141 let total = sys.total_memory();
142 if total > 0 {
143 return total;
144 }
145 }
146 4_294_967_296 }
148
149 fn detect_gpus() -> Vec<String> {
150 Vec::new()
153 }
154
155 fn detect_codecs() -> Vec<String> {
156 vec![
158 "h264".to_string(),
159 "h265".to_string(),
160 "vp9".to_string(),
161 "av1".to_string(),
162 ]
163 }
164
165 fn detect_hwaccels() -> Vec<String> {
166 Vec::new()
168 }
169
170 fn benchmark_speed() -> f32 {
171 1.0
173 }
174}
175
176struct WorkerState {
178 status: Arc<RwLock<WorkerStatus>>,
179 metrics: LocalWorkerMetrics,
180}
181
182impl WorkerState {
183 fn new() -> Self {
184 Self {
185 status: Arc::new(RwLock::new(WorkerStatus::Idle)),
186 metrics: LocalWorkerMetrics::new(),
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193enum WorkerStatus {
194 Idle,
195 Busy,
196 Full,
197 Draining,
198 Error,
199}
200
201impl From<WorkerStatus> for i32 {
202 fn from(status: WorkerStatus) -> Self {
203 match status {
204 WorkerStatus::Idle => 0,
205 WorkerStatus::Busy => 1,
206 WorkerStatus::Full => 2,
207 WorkerStatus::Draining => 3,
208 WorkerStatus::Error => 4,
209 }
210 }
211}
212
213#[derive(Clone)]
215struct LocalWorkerMetrics {
216 cpu_usage: Arc<AtomicU32>,
217 memory_usage: Arc<AtomicU32>,
218 gpu_usage: Arc<AtomicU32>,
219 bytes_processed: Arc<AtomicU64>,
220 frames_encoded: Arc<AtomicU32>,
221}
222
223impl LocalWorkerMetrics {
224 fn new() -> Self {
225 Self {
226 cpu_usage: Arc::new(AtomicU32::new(0)),
227 memory_usage: Arc::new(AtomicU32::new(0)),
228 gpu_usage: Arc::new(AtomicU32::new(0)),
229 bytes_processed: Arc::new(AtomicU64::new(0)),
230 frames_encoded: Arc::new(AtomicU32::new(0)),
231 }
232 }
233
234 fn update_system_metrics(&self) {
235 #[cfg(target_os = "linux")]
237 {
238 let load = sysinfo::System::load_average();
239 let cpu_percent = (load.one * 100.0) as u32;
240 self.cpu_usage.store(cpu_percent, Ordering::Relaxed);
241
242 let mut sys = sysinfo::System::new();
243 sys.refresh_memory();
244 let total = sys.total_memory();
245 if total > 0 {
246 let used = sys.used_memory();
247 let mem_percent = (used * 100 / total) as u32;
248 self.memory_usage.store(mem_percent, Ordering::Relaxed);
249 }
250 }
251 }
252
253 fn to_proto(&self) -> pb::WorkerMetrics {
254 pb::WorkerMetrics {
255 cpu_usage: self.cpu_usage.load(Ordering::Relaxed) as f32 / 100.0,
256 memory_usage: self.memory_usage.load(Ordering::Relaxed) as f32 / 100.0,
257 gpu_usage: self.gpu_usage.load(Ordering::Relaxed) as f32 / 100.0,
258 bytes_processed: self.bytes_processed.load(Ordering::Relaxed),
259 frames_encoded: self.frames_encoded.load(Ordering::Relaxed),
260 }
261 }
262}
263
264#[allow(dead_code)]
266struct ActiveJob {
267 job_id: String,
268 task: EncodingTask,
269 progress: f32,
270 started_at: SystemTime,
271 cancel_tx: mpsc::Sender<()>,
272}
273
274impl Worker {
275 #[must_use]
277 pub fn new(config: WorkerConfig) -> Self {
278 Self {
279 config,
280 worker_id: Arc::new(RwLock::new(Uuid::new_v4().to_string())),
281 client: Arc::new(RwLock::new(None)),
282 active_jobs: Arc::new(RwLock::new(HashMap::new())),
283 state: Arc::new(WorkerState::new()),
284 shutdown: Arc::new(AtomicBool::new(false)),
285 }
286 }
287
288 pub async fn run(&self) -> Result<()> {
290 info!("Starting worker");
291
292 self.connect().await?;
294
295 self.register().await?;
297
298 let worker = self.clone_arc();
300 tokio::spawn(async move {
301 worker.heartbeat_loop().await;
302 });
303
304 let worker = self.clone_arc();
305 tokio::spawn(async move {
306 worker.job_poll_loop().await;
307 });
308
309 while !self.shutdown.load(Ordering::Relaxed) {
311 tokio::time::sleep(Duration::from_secs(1)).await;
312 }
313
314 self.unregister().await?;
316
317 Ok(())
318 }
319
320 fn clone_arc(&self) -> Arc<Self> {
321 Arc::new(Self {
324 config: self.config.clone(),
325 worker_id: self.worker_id.clone(),
326 client: self.client.clone(),
327 active_jobs: self.active_jobs.clone(),
328 state: Arc::new(WorkerState {
329 status: self.state.status.clone(),
330 metrics: LocalWorkerMetrics {
331 cpu_usage: self.state.metrics.cpu_usage.clone(),
332 memory_usage: self.state.metrics.memory_usage.clone(),
333 gpu_usage: self.state.metrics.gpu_usage.clone(),
334 bytes_processed: self.state.metrics.bytes_processed.clone(),
335 frames_encoded: self.state.metrics.frames_encoded.clone(),
336 },
337 }),
338 shutdown: self.shutdown.clone(),
339 })
340 }
341
342 async fn connect(&self) -> Result<()> {
344 info!(
345 "Connecting to coordinator at {}",
346 self.config.coordinator_addr
347 );
348
349 let client =
350 CoordinatorServiceClient::connect(self.config.coordinator_addr.clone()).await?;
351
352 let mut client_lock = self.client.write().await;
353 *client_lock = Some(client);
354
355 info!("Connected to coordinator");
356 Ok(())
357 }
358
359 async fn register(&self) -> Result<()> {
361 info!("Registering with coordinator");
362
363 let capabilities = pb::WorkerCapabilities {
364 cpu_cores: self.config.capabilities.cpu_cores,
365 memory_bytes: self.config.capabilities.memory_bytes,
366 gpu_devices: self.config.capabilities.gpu_devices.clone(),
367 supported_codecs: self.config.capabilities.supported_codecs.clone(),
368 supported_hwaccels: self.config.capabilities.supported_hwaccels.clone(),
369 relative_speed: self.config.capabilities.relative_speed,
370 max_concurrent_jobs: self.config.max_concurrent_jobs,
371 };
372
373 let registration = WorkerRegistration {
374 worker_id: self.worker_id.read().await.clone(),
375 hostname: self.config.hostname.clone(),
376 ip_address: self.config.ip_address.clone(),
377 port: self.config.port,
378 capabilities: Some(Box::new(capabilities)),
379 metadata: HashMap::new(),
380 };
381
382 let mut client = self.client.write().await;
383 if let Some(ref mut c) = *client {
384 let response = c
385 .register_worker(Request::new(registration))
386 .await?
387 .into_inner();
388
389 if response.success {
390 let mut worker_id = self.worker_id.write().await;
391 *worker_id = response.assigned_worker_id;
392 info!("Registered with ID: {}", *worker_id);
393 Ok(())
394 } else {
395 Err(DistributedError::Worker(response.message))
396 }
397 } else {
398 Err(DistributedError::Worker("Not connected".to_string()))
399 }
400 }
401
402 async fn unregister(&self) -> Result<()> {
404 info!("Unregistering from coordinator");
405
406 let unreg = WorkerUnregistration {
407 worker_id: self.worker_id.read().await.clone(),
408 reason: "Shutdown".to_string(),
409 };
410
411 let mut client = self.client.write().await;
412 if let Some(ref mut c) = *client {
413 c.unregister_worker(Request::new(unreg)).await?;
414 }
415
416 Ok(())
417 }
418
419 async fn heartbeat_loop(&self) {
421 let mut interval = tokio::time::interval(self.config.heartbeat_interval);
422
423 loop {
424 interval.tick().await;
425
426 if self.shutdown.load(Ordering::Relaxed) {
427 break;
428 }
429
430 if let Err(e) = self.send_heartbeat().await {
431 error!("Heartbeat failed: {}", e);
432 }
433 }
434 }
435
436 async fn send_heartbeat(&self) -> Result<()> {
438 debug!("Sending heartbeat");
439
440 self.state.metrics.update_system_metrics();
442
443 let active_jobs = self.active_jobs.read().await;
444 let active_job_ids: Vec<String> = active_jobs.keys().cloned().collect();
445
446 let status = *self.state.status.read().await;
447 let status_proto = pb::WorkerStatus {
448 state: i32::from(status),
449 active_jobs: active_jobs.len() as u32,
450 queued_jobs: 0,
451 };
452
453 let heartbeat = WorkerHeartbeat {
454 worker_id: self.worker_id.read().await.clone(),
455 status: Some(Box::new(status_proto)),
456 active_job_ids,
457 metrics: Some(Box::new(self.state.metrics.to_proto())),
458 };
459
460 let mut client = self.client.write().await;
461 if let Some(ref mut c) = *client {
462 let response = c.heartbeat(Request::new(heartbeat)).await?.into_inner();
463
464 if response.should_drain {
466 info!("Coordinator requested drain");
467 let mut status = self.state.status.write().await;
468 *status = WorkerStatus::Draining;
469 }
470
471 for job_id in response.jobs_to_cancel {
473 self.cancel_job(&job_id).await;
474 }
475 }
476
477 Ok(())
478 }
479
480 async fn job_poll_loop(&self) {
482 let mut interval = tokio::time::interval(self.config.poll_interval);
483
484 loop {
485 interval.tick().await;
486
487 if self.shutdown.load(Ordering::Relaxed) {
488 break;
489 }
490
491 let status = *self.state.status.read().await;
492 if status == WorkerStatus::Draining || status == WorkerStatus::Error {
493 continue;
494 }
495
496 let active_count = self.active_jobs.read().await.len();
497 if active_count >= self.config.max_concurrent_jobs as usize {
498 let mut status_lock = self.state.status.write().await;
500 *status_lock = WorkerStatus::Full;
501 continue;
502 }
503
504 if let Err(e) = self.request_and_execute_jobs().await {
506 error!("Job request failed: {}", e);
507 }
508 }
509 }
510
511 async fn request_and_execute_jobs(&self) -> Result<()> {
513 let active_count = self.active_jobs.read().await.len() as u32;
514 let max_jobs = self.config.max_concurrent_jobs.saturating_sub(active_count);
515
516 if max_jobs == 0 {
517 return Ok(());
518 }
519
520 let request = JobRequest {
521 worker_id: self.worker_id.read().await.clone(),
522 max_jobs,
523 preferred_codecs: self.config.capabilities.supported_codecs.clone(),
524 };
525
526 let mut client = self.client.write().await;
527 if let Some(ref mut c) = *client {
528 let response = c.request_job(Request::new(request)).await?.into_inner();
529
530 for job in response.jobs {
531 if let Some(task) = job.task {
532 info!("Received job: {}", job.job_id);
533 self.execute_job(job.job_id, *task).await;
534 }
535 }
536
537 drop(client);
539 let active_count = self.active_jobs.read().await.len();
540 let mut status = self.state.status.write().await;
541 *status = if active_count == 0 {
542 WorkerStatus::Idle
543 } else if active_count >= self.config.max_concurrent_jobs as usize {
544 WorkerStatus::Full
545 } else {
546 WorkerStatus::Busy
547 };
548 }
549
550 Ok(())
551 }
552
553 async fn execute_job(&self, job_id: String, task: EncodingTask) {
555 let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
556
557 let active_job = ActiveJob {
558 job_id: job_id.clone(),
559 task: task.clone(),
560 progress: 0.0,
561 started_at: SystemTime::now(),
562 cancel_tx,
563 };
564
565 self.active_jobs
566 .write()
567 .await
568 .insert(job_id.clone(), active_job);
569
570 let worker_id = self.worker_id.read().await.clone();
571 let client = self.client.clone();
572 let active_jobs = self.active_jobs.clone();
573 let metrics = self.state.metrics.clone();
574
575 tokio::spawn(async move {
576 let result = Self::run_encoding_task(
577 &task,
578 &job_id,
579 &worker_id,
580 client.clone(),
581 &mut cancel_rx,
582 metrics,
583 )
584 .await;
585
586 active_jobs.write().await.remove(&job_id);
588
589 let mut client_lock = client.write().await;
591 if let Some(ref mut c) = *client_lock {
592 match result {
593 Ok(output_info) => {
594 let result_msg = JobResult {
595 job_id: job_id.clone(),
596 worker_id: worker_id.clone(),
597 output_url: output_info.output_url,
598 output_size: output_info.output_size,
599 encoding_time: output_info.encoding_time,
600 metadata: Some(Box::new(ResultMetadata {
601 frames_encoded: output_info.frames_encoded,
602 average_bitrate: output_info.average_bitrate,
603 checksum: output_info.checksum,
604 extra_metadata: HashMap::new(),
605 })),
606 };
607
608 if let Err(e) = c.submit_result(Request::new(result_msg)).await {
609 error!("Failed to submit result for job {}: {}", job_id, e);
610 } else {
611 info!("Job {} completed successfully", job_id);
612 }
613 }
614 Err(e) => {
615 error!("Job {} failed: {}", job_id, e);
616
617 let failure = JobFailure {
618 job_id: job_id.clone(),
619 worker_id: worker_id.clone(),
620 error_message: e.to_string(),
621 error_code: "ENCODING_ERROR".to_string(),
622 is_transient: false,
623 };
624
625 let _ = c.report_failure(Request::new(failure)).await;
626 }
627 }
628 }
629 });
630 }
631
632 #[allow(clippy::too_many_arguments)]
634 async fn run_encoding_task(
635 task: &EncodingTask,
636 job_id: &str,
637 worker_id: &str,
638 client: Arc<RwLock<Option<CoordinatorServiceClient<Channel>>>>,
639 cancel_rx: &mut mpsc::Receiver<()>,
640 metrics: LocalWorkerMetrics,
641 ) -> Result<EncodingOutput> {
642 info!("Starting encoding task for job {}", job_id);
643
644 let total_frames = 1000u64;
647 let mut frames_encoded = 0u64;
648
649 for _ in 0..10 {
650 if cancel_rx.try_recv().is_ok() {
652 return Err(DistributedError::Job("Job cancelled".to_string()));
653 }
654
655 tokio::time::sleep(Duration::from_millis(100)).await;
657 frames_encoded += total_frames / 10;
658
659 let progress = frames_encoded as f32 / total_frames as f32;
660
661 let progress_report = ProgressReport {
663 job_id: job_id.to_string(),
664 worker_id: worker_id.to_string(),
665 progress,
666 frames_encoded,
667 bytes_written: frames_encoded * 1024,
668 encoding_speed: 30.0,
669 estimated_completion_timestamp: SystemTime::now()
670 .duration_since(UNIX_EPOCH)
671 .unwrap_or_default()
672 .as_secs() as i64
673 + 60,
674 };
675
676 let mut client_lock = client.write().await;
677 if let Some(ref mut c) = *client_lock {
678 let _ = c.report_progress(Request::new(progress_report)).await;
679 }
680 }
681
682 metrics
684 .frames_encoded
685 .fetch_add(total_frames as u32, Ordering::Relaxed);
686 metrics
687 .bytes_processed
688 .fetch_add(total_frames * 1024, Ordering::Relaxed);
689
690 Ok(EncodingOutput {
691 output_url: task.output_url.clone(),
692 output_size: total_frames * 1024,
693 encoding_time: 1.0,
694 frames_encoded: total_frames,
695 average_bitrate: 5000.0,
696 checksum: "abc123".to_string(),
697 })
698 }
699
700 async fn cancel_job(&self, job_id: &str) {
702 info!("Cancelling job {}", job_id);
703
704 if let Some(job) = self.active_jobs.write().await.remove(job_id) {
705 let _ = job.cancel_tx.send(()).await;
706 }
707 }
708
709 pub async fn shutdown(&self) -> Result<()> {
711 info!("Shutting down worker");
712 self.shutdown.store(true, Ordering::Relaxed);
713
714 let active_jobs = self.active_jobs.read().await;
716 for (_, job) in active_jobs.iter() {
717 let _ = job.cancel_tx.send(()).await;
718 }
719
720 Ok(())
721 }
722}
723
724struct EncodingOutput {
726 output_url: String,
727 output_size: u64,
728 encoding_time: f64,
729 frames_encoded: u64,
730 average_bitrate: f64,
731 checksum: String,
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_worker_config_default() {
740 let config = WorkerConfig::default();
741 assert_eq!(config.max_concurrent_jobs, 4);
742 assert!(config.capabilities.cpu_cores > 0);
743 }
744
745 #[test]
746 fn test_worker_capabilities_detection() {
747 let caps = WorkerCapabilities::detect();
748 assert!(caps.cpu_cores > 0);
749 assert!(caps.memory_bytes > 0);
750 assert!(!caps.supported_codecs.is_empty());
751 }
752
753 #[test]
754 fn test_worker_status_conversion() {
755 assert_eq!(i32::from(WorkerStatus::Idle), 0);
756 assert_eq!(i32::from(WorkerStatus::Busy), 1);
757 assert_eq!(i32::from(WorkerStatus::Full), 2);
758 }
759
760 #[test]
761 fn test_worker_metrics() {
762 let metrics = LocalWorkerMetrics::new();
763 assert_eq!(metrics.cpu_usage.load(Ordering::Relaxed), 0);
764 assert_eq!(metrics.frames_encoded.load(Ordering::Relaxed), 0);
765
766 metrics.frames_encoded.store(100, Ordering::Relaxed);
767 assert_eq!(metrics.frames_encoded.load(Ordering::Relaxed), 100);
768 }
769}