Skip to main content

oximedia_distributed/
worker.rs

1//! Worker node implementation for distributed encoding.
2//!
3//! Workers:
4//! - Register with the coordinator
5//! - Send periodic heartbeats
6//! - Request and execute encoding jobs
7//! - Report progress and results
8//! - Handle local encoding execution
9
10use 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
28/// Worker node
29pub struct Worker {
30    /// Worker configuration
31    config: WorkerConfig,
32
33    /// Worker ID (assigned by coordinator or self-generated)
34    worker_id: Arc<RwLock<String>>,
35
36    /// gRPC client
37    client: Arc<RwLock<Option<CoordinatorServiceClient<Channel>>>>,
38
39    /// Active jobs
40    active_jobs: Arc<RwLock<HashMap<String, ActiveJob>>>,
41
42    /// Worker state
43    state: Arc<WorkerState>,
44
45    /// Shutdown signal
46    shutdown: Arc<AtomicBool>,
47}
48
49/// Worker configuration
50#[derive(Debug, Clone)]
51pub struct WorkerConfig {
52    /// Coordinator address
53    pub coordinator_addr: String,
54
55    /// Worker hostname
56    pub hostname: String,
57
58    /// Worker IP address
59    pub ip_address: String,
60
61    /// Worker listening port
62    pub port: u32,
63
64    /// Heartbeat interval
65    pub heartbeat_interval: Duration,
66
67    /// Job poll interval
68    pub poll_interval: Duration,
69
70    /// Maximum concurrent jobs
71    pub max_concurrent_jobs: u32,
72
73    /// Worker capabilities
74    pub capabilities: WorkerCapabilities,
75
76    /// Working directory for temporary files
77    pub work_dir: PathBuf,
78
79    /// Enable GPU acceleration
80    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/// Worker capabilities detection
104#[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    /// Detect system capabilities
116    #[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        // Simplified memory detection
137        #[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 // Default 4GB
147    }
148
149    fn detect_gpus() -> Vec<String> {
150        // Simplified GPU detection
151        // In production, use proper GPU detection libraries
152        Vec::new()
153    }
154
155    fn detect_codecs() -> Vec<String> {
156        // Return common codecs
157        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        // Detect hardware acceleration support
167        Vec::new()
168    }
169
170    fn benchmark_speed() -> f32 {
171        // Simple benchmark, return baseline speed
172        1.0
173    }
174}
175
176/// Worker internal state
177struct 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/// Worker status
192#[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/// Worker metrics tracking
214#[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        // Update CPU and memory usage
236        #[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/// Active job tracking
265#[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    /// Create a new worker with the given configuration
276    #[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    /// Start the worker
289    pub async fn run(&self) -> Result<()> {
290        info!("Starting worker");
291
292        // Connect to coordinator
293        self.connect().await?;
294
295        // Register with coordinator
296        self.register().await?;
297
298        // Start background tasks
299        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        // Wait for shutdown
310        while !self.shutdown.load(Ordering::Relaxed) {
311            tokio::time::sleep(Duration::from_secs(1)).await;
312        }
313
314        // Unregister
315        self.unregister().await?;
316
317        Ok(())
318    }
319
320    fn clone_arc(&self) -> Arc<Self> {
321        // Helper to create Arc clone
322        // In real implementation, Worker would be wrapped in Arc
323        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    /// Connect to coordinator
343    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    /// Register with coordinator
360    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    /// Unregister from coordinator
403    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    /// Heartbeat loop
420    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    /// Send heartbeat to coordinator
437    async fn send_heartbeat(&self) -> Result<()> {
438        debug!("Sending heartbeat");
439
440        // Update metrics
441        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            // Handle coordinator commands
465            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            // Handle job cancellations
472            for job_id in response.jobs_to_cancel {
473                self.cancel_job(&job_id).await;
474            }
475        }
476
477        Ok(())
478    }
479
480    /// Job polling loop
481    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                // Update status to Full
499                let mut status_lock = self.state.status.write().await;
500                *status_lock = WorkerStatus::Full;
501                continue;
502            }
503
504            // Request jobs
505            if let Err(e) = self.request_and_execute_jobs().await {
506                error!("Job request failed: {}", e);
507            }
508        }
509    }
510
511    /// Request and execute jobs from coordinator
512    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            // Update status
538            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    /// Execute an encoding job
554    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            // Remove from active jobs
587            active_jobs.write().await.remove(&job_id);
588
589            // Report result
590            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    /// Run the actual encoding task
633    #[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        // Simulate encoding work
645        // In production, this would call actual encoding functions
646        let total_frames = 1000u64;
647        let mut frames_encoded = 0u64;
648
649        for _ in 0..10 {
650            // Check for cancellation
651            if cancel_rx.try_recv().is_ok() {
652                return Err(DistributedError::Job("Job cancelled".to_string()));
653            }
654
655            // Simulate encoding work
656            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            // Report progress
662            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        // Update metrics
683        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    /// Cancel a job
701    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    /// Shutdown the worker
710    pub async fn shutdown(&self) -> Result<()> {
711        info!("Shutting down worker");
712        self.shutdown.store(true, Ordering::Relaxed);
713
714        // Cancel all active jobs
715        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
724/// Encoding output information
725struct 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}