Skip to main content

subx_core/core/parallel/
worker.rs

1//! Worker pool and worker definitions for parallel processing
2use super::task::{Task, TaskResult};
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use tokio::task::JoinHandle;
6use uuid::Uuid;
7
8/// Pool managing active workers
9pub struct WorkerPool {
10    workers: Arc<Mutex<HashMap<Uuid, WorkerInfo>>>,
11    max_workers: usize,
12    reporter: Arc<dyn crate::core::report::Reporter>,
13}
14
15#[derive(Debug)]
16struct WorkerInfo {
17    handle: JoinHandle<TaskResult>,
18    task_id: String,
19    start_time: std::time::Instant,
20    worker_type: WorkerType,
21}
22
23/// Type of work performed by a worker thread.
24///
25/// Categorizes workers based on their primary resource usage pattern
26/// to enable optimal scheduling and resource allocation.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum WorkerType {
29    /// Workers that primarily consume CPU resources
30    CpuIntensive,
31    /// Workers that primarily perform I/O operations
32    IoIntensive,
33    /// Workers that perform a mix of CPU and I/O operations
34    Mixed,
35}
36
37impl WorkerPool {
38    /// Creates a new worker pool with the specified maximum number of workers.
39    ///
40    /// The pool reports through a [`crate::core::report::NoopReporter`]
41    /// unless a reporter is attached with [`WorkerPool::with_reporter`].
42    ///
43    /// # Arguments
44    ///
45    /// * `max_workers` - The maximum number of concurrent workers allowed
46    pub fn new(max_workers: usize) -> Self {
47        Self {
48            workers: Arc::new(Mutex::new(HashMap::new())),
49            max_workers,
50            reporter: crate::core::report::noop(),
51        }
52    }
53
54    /// Attach a reporting sink, consuming and returning the pool.
55    ///
56    /// # Arguments
57    ///
58    /// * `reporter` - Sink for progress chatter emitted while the pool
59    ///   drains its workers.
60    pub fn with_reporter(mut self, reporter: Arc<dyn crate::core::report::Reporter>) -> Self {
61        self.reporter = reporter;
62        self
63    }
64
65    /// Clone the attached reporting sink for use across `await` points.
66    fn reporter(&self) -> Arc<dyn crate::core::report::Reporter> {
67        Arc::clone(&self.reporter)
68    }
69
70    /// Execute a task by spawning a worker
71    pub async fn execute(&self, task: Box<dyn Task + Send + Sync>) -> Result<TaskResult, String> {
72        let worker_id = Uuid::now_v7();
73        let task_id = task.task_id();
74        let worker_type = self.determine_worker_type(task.task_type());
75
76        {
77            let workers = self.workers.lock().unwrap();
78            if workers.len() >= self.max_workers {
79                return Err("Worker pool is full".to_string());
80            }
81        }
82
83        let handle = tokio::spawn(async move { task.execute().await });
84
85        {
86            let mut workers = self.workers.lock().unwrap();
87            workers.insert(
88                worker_id,
89                WorkerInfo {
90                    handle,
91                    task_id: task_id.clone(),
92                    start_time: std::time::Instant::now(),
93                    worker_type,
94                },
95            );
96        }
97
98        // For simplicity, return immediately indicating submission
99        Ok(TaskResult::Success("Task submitted".to_string()))
100    }
101
102    fn determine_worker_type(&self, task_type: &str) -> WorkerType {
103        match task_type {
104            "convert" => WorkerType::CpuIntensive,
105            "sync" => WorkerType::Mixed,
106            "match" => WorkerType::IoIntensive,
107            "validate" => WorkerType::IoIntensive,
108            _ => WorkerType::Mixed,
109        }
110    }
111
112    /// Number of active workers
113    pub fn get_active_count(&self) -> usize {
114        self.workers.lock().unwrap().len()
115    }
116
117    /// Maximum capacity of worker pool
118    pub fn get_capacity(&self) -> usize {
119        self.max_workers
120    }
121
122    /// Statistics about current workers
123    pub fn get_worker_stats(&self) -> WorkerStats {
124        let workers = self.workers.lock().unwrap();
125        let mut cpu = 0;
126        let mut io = 0;
127        let mut mixed = 0;
128        for w in workers.values() {
129            match w.worker_type {
130                WorkerType::CpuIntensive => cpu += 1,
131                WorkerType::IoIntensive => io += 1,
132                WorkerType::Mixed => mixed += 1,
133            }
134        }
135        WorkerStats {
136            total_active: workers.len(),
137            cpu_intensive_count: cpu,
138            io_intensive_count: io,
139            mixed_count: mixed,
140            max_capacity: self.max_workers,
141        }
142    }
143
144    /// Shutdown and wait for all workers
145    pub async fn shutdown(&self) {
146        let reporter = self.reporter();
147        let workers = { std::mem::take(&mut *self.workers.lock().unwrap()) };
148        for (id, info) in workers {
149            // Progress chatter on the drain path — the reporter decides
150            // whether it reaches the terminal.
151            reporter.progress(&crate::core::report::ProgressEvent::Message(&format!(
152                "Waiting for worker {id} to complete task {}",
153                info.task_id
154            )));
155            let _ = info.handle.await;
156        }
157    }
158
159    /// List active worker infos
160    pub fn list_active_workers(&self) -> Vec<ActiveWorkerInfo> {
161        let workers = self.workers.lock().unwrap();
162        workers
163            .iter()
164            .map(|(id, info)| ActiveWorkerInfo {
165                worker_id: *id,
166                task_id: info.task_id.clone(),
167                worker_type: info.worker_type.clone(),
168                runtime: info.start_time.elapsed(),
169            })
170            .collect()
171    }
172}
173
174impl Clone for WorkerPool {
175    fn clone(&self) -> Self {
176        Self {
177            workers: Arc::clone(&self.workers),
178            max_workers: self.max_workers,
179            reporter: Arc::clone(&self.reporter),
180        }
181    }
182}
183
184/// Statistics about the current state of the worker pool.
185///
186/// Provides insights into worker utilization and capacity across
187/// different worker types.
188#[derive(Debug, Clone)]
189pub struct WorkerStats {
190    /// Total number of currently active workers
191    pub total_active: usize,
192    /// Number of active CPU-intensive workers
193    pub cpu_intensive_count: usize,
194    /// Number of active I/O-intensive workers
195    pub io_intensive_count: usize,
196    /// Number of active mixed-type workers
197    pub mixed_count: usize,
198    /// Maximum number of workers allowed in the pool
199    pub max_capacity: usize,
200}
201
202/// Information about an active worker in the pool.
203///
204/// Contains runtime information about a worker currently executing a task.
205#[derive(Debug, Clone)]
206pub struct ActiveWorkerInfo {
207    /// Unique identifier for the worker
208    pub worker_id: Uuid,
209    /// Identifier of the task being executed
210    pub task_id: String,
211    /// Type of work this worker performs
212    pub worker_type: WorkerType,
213    /// How long the worker has been running the current task
214    pub runtime: std::time::Duration,
215}
216
217/// Represents an individual worker for monitoring
218pub struct Worker {
219    id: Uuid,
220    status: WorkerStatus,
221}
222
223/// Current status of a worker in the pool.
224///
225/// Tracks the state of individual workers from creation through execution
226/// and potential error conditions.
227#[derive(Debug, Clone)]
228pub enum WorkerStatus {
229    /// Worker is available and waiting for tasks
230    Idle,
231    /// Worker is executing a task (contains task ID)
232    Busy(String),
233    /// Worker has been stopped and is no longer available
234    Stopped,
235    /// Worker encountered an error (contains error message)
236    Error(String),
237}
238
239impl Worker {
240    /// Creates a new worker with a unique ID and idle status.
241    pub fn new() -> Self {
242        Self {
243            id: Uuid::now_v7(),
244            status: WorkerStatus::Idle,
245        }
246    }
247
248    /// Returns the unique identifier of this worker.
249    pub fn id(&self) -> Uuid {
250        self.id
251    }
252
253    /// Returns the current status of this worker.
254    pub fn status(&self) -> &WorkerStatus {
255        &self.status
256    }
257
258    /// Updates the status of this worker.
259    ///
260    /// # Arguments
261    ///
262    /// * `status` - The new status to set for this worker
263    pub fn set_status(&mut self, status: WorkerStatus) {
264        self.status = status;
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[tokio::test]
273    async fn test_worker_pool_capacity() {
274        let pool = WorkerPool::new(2);
275        assert_eq!(pool.get_capacity(), 2);
276        assert_eq!(pool.get_active_count(), 0);
277        let stats = pool.get_worker_stats();
278        assert_eq!(stats.max_capacity, 2);
279        assert_eq!(stats.total_active, 0);
280    }
281
282    #[tokio::test]
283    async fn test_execute_and_active_count() {
284        use crate::core::parallel::task::{Task, TaskResult};
285        use async_trait::async_trait;
286
287        #[derive(Clone)]
288        struct DummyTask {
289            id: String,
290            tp: &'static str,
291        }
292
293        #[async_trait]
294        impl Task for DummyTask {
295            async fn execute(&self) -> TaskResult {
296                TaskResult::Success(self.id.clone())
297            }
298            fn task_type(&self) -> &'static str {
299                self.tp
300            }
301            fn task_id(&self) -> String {
302                self.id.clone()
303            }
304        }
305
306        let pool = WorkerPool::new(1);
307        let task = DummyTask {
308            id: "t1".into(),
309            tp: "convert",
310        };
311        let res = pool.execute(Box::new(task.clone())).await;
312        assert!(matches!(res, Ok(TaskResult::Success(_))));
313        assert_eq!(pool.get_active_count(), 1);
314    }
315
316    #[tokio::test]
317    async fn test_reject_when_full() {
318        use crate::core::parallel::task::{Task, TaskResult};
319        use async_trait::async_trait;
320
321        #[derive(Clone)]
322        struct DummyTask;
323
324        #[async_trait]
325        impl Task for DummyTask {
326            async fn execute(&self) -> TaskResult {
327                TaskResult::Success("".into())
328            }
329            fn task_type(&self) -> &'static str {
330                "match"
331            }
332            fn task_id(&self) -> String {
333                "".into()
334            }
335        }
336
337        let pool = WorkerPool::new(1);
338        let _ = pool.execute(Box::new(DummyTask)).await;
339        let err = pool.execute(Box::new(DummyTask)).await;
340        assert!(err.is_err());
341    }
342
343    #[tokio::test]
344    async fn test_list_active_workers_and_stats() {
345        use super::WorkerType;
346        use crate::core::parallel::task::{Task, TaskResult};
347        use async_trait::async_trait;
348
349        #[derive(Clone)]
350        struct DummyTask2;
351
352        #[async_trait]
353        impl Task for DummyTask2 {
354            async fn execute(&self) -> TaskResult {
355                TaskResult::Success("".into())
356            }
357            fn task_type(&self) -> &'static str {
358                "sync"
359            }
360            fn task_id(&self) -> String {
361                "tok2".into()
362            }
363        }
364
365        let pool = WorkerPool::new(2);
366        let _ = pool.execute(Box::new(DummyTask2)).await;
367        let workers = pool.list_active_workers();
368        assert_eq!(workers.len(), 1);
369        let info = &workers[0];
370        assert_eq!(info.task_id, "tok2");
371        assert_eq!(info.worker_type, WorkerType::Mixed);
372        let stats = pool.get_worker_stats();
373        assert_eq!(stats.total_active, 1);
374    }
375
376    /// Test worker pool with multiple concurrent tasks
377    #[tokio::test]
378    async fn test_worker_job_distribution() {
379        use crate::core::parallel::task::{Task, TaskResult};
380        use async_trait::async_trait;
381        use std::sync::Arc;
382        use std::sync::atomic::{AtomicUsize, Ordering};
383
384        #[derive(Clone)]
385        struct CountingTask {
386            id: String,
387            counter: Arc<AtomicUsize>,
388        }
389
390        #[async_trait]
391        impl Task for CountingTask {
392            async fn execute(&self) -> TaskResult {
393                self.counter.fetch_add(1, Ordering::SeqCst);
394                tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
395                TaskResult::Success(format!("task-{}", self.id))
396            }
397            fn task_type(&self) -> &'static str {
398                "convert"
399            }
400            fn task_id(&self) -> String {
401                self.id.clone()
402            }
403        }
404
405        let pool = WorkerPool::new(4);
406        let counter = Arc::new(AtomicUsize::new(0));
407        let mut handles = Vec::new();
408
409        // Submit tasks with delay to avoid overwhelming the pool
410        for i in 0..4 {
411            // Only submit as many tasks as pool capacity
412            let task = CountingTask {
413                id: format!("task-{}", i),
414                counter: Arc::clone(&counter),
415            };
416
417            // Execute tasks concurrently
418            let pool_clone = pool.clone();
419            let handle = tokio::spawn(async move { pool_clone.execute(Box::new(task)).await });
420            handles.push(handle);
421        }
422
423        // Wait for all submissions
424        for handle in handles {
425            let result = handle.await.unwrap();
426            assert!(result.is_ok(), "Task submission should succeed");
427        }
428
429        // Wait a bit for tasks to complete
430        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
431
432        // Verify load balancing - all submitted tasks should have been processed
433        let final_count = counter.load(Ordering::SeqCst);
434        assert_eq!(final_count, 4, "All 4 tasks should have been executed");
435    }
436
437    /// Test error recovery mechanism with failing tasks
438    #[tokio::test]
439    async fn test_worker_error_recovery() {
440        use crate::core::parallel::task::{Task, TaskResult};
441        use async_trait::async_trait;
442
443        #[derive(Clone)]
444        struct FailingTask {
445            id: String,
446            should_fail: bool,
447        }
448
449        #[async_trait]
450        impl Task for FailingTask {
451            async fn execute(&self) -> TaskResult {
452                if self.should_fail {
453                    TaskResult::Failed("Intentional failure".to_string())
454                } else {
455                    TaskResult::Success(format!("success-{}", self.id))
456                }
457            }
458            fn task_type(&self) -> &'static str {
459                "sync"
460            }
461            fn task_id(&self) -> String {
462                self.id.clone()
463            }
464        }
465
466        let pool = WorkerPool::new(2);
467
468        // Test successful task
469        let success_task = FailingTask {
470            id: "success".to_string(),
471            should_fail: false,
472        };
473        let result = pool.execute(Box::new(success_task)).await;
474        assert!(result.is_ok(), "Successful task should be submitted");
475
476        // Test failing task (note: execute() only indicates submission success)
477        let fail_task = FailingTask {
478            id: "fail".to_string(),
479            should_fail: true,
480        };
481        let result = pool.execute(Box::new(fail_task)).await;
482        assert!(
483            result.is_ok(),
484            "Failing task should still be submitted successfully"
485        );
486
487        // Pool should handle the internal failure gracefully
488        assert!(
489            pool.get_active_count() <= 2,
490            "Active count should be within limits"
491        );
492    }
493
494    /// Test parallel processing performance comparison
495    #[tokio::test]
496    async fn test_parallel_processing_performance() {
497        use crate::core::parallel::task::{Task, TaskResult};
498        use async_trait::async_trait;
499        use std::time::Instant;
500
501        #[derive(Clone)]
502        struct CpuIntensiveTask {
503            id: String,
504            duration_ms: u64,
505        }
506
507        #[async_trait]
508        impl Task for CpuIntensiveTask {
509            async fn execute(&self) -> TaskResult {
510                // Simulate CPU-intensive work
511                tokio::time::sleep(tokio::time::Duration::from_millis(self.duration_ms)).await;
512                TaskResult::Success(format!("completed-{}", self.id))
513            }
514            fn task_type(&self) -> &'static str {
515                "convert"
516            }
517            fn task_id(&self) -> String {
518                self.id.clone()
519            }
520        }
521
522        // Test sequential processing (with smaller pool to avoid conflicts)
523        let sequential_pool = WorkerPool::new(1);
524        let start = Instant::now();
525
526        for i in 0..2 {
527            // Reduce number of tasks
528            let task = CpuIntensiveTask {
529                id: format!("seq-{}", i),
530                duration_ms: 10, // Reduce duration
531            };
532            if let Err(e) = sequential_pool.execute(Box::new(task)).await {
533                println!("Sequential task {} failed: {}", i, e);
534                // Don't panic on pool full, just continue
535            }
536        }
537        let sequential_time = start.elapsed();
538
539        // Test parallel processing with minimal concurrency
540        let parallel_pool = WorkerPool::new(2); // Smaller pool
541        let start = Instant::now();
542
543        // Use only one parallel task to avoid pool overflow
544        let task = CpuIntensiveTask {
545            id: "par-0".to_string(),
546            duration_ms: 10,
547        };
548        if let Err(e) = parallel_pool.execute(Box::new(task)).await {
549            println!("Parallel task failed: {}", e);
550        }
551        let parallel_time = start.elapsed();
552
553        // Note: This test measures submission time, not execution time
554        // In a real scenario, we'd need more sophisticated measurement
555        println!("Sequential submission time: {:?}", sequential_time);
556        println!("Parallel submission time: {:?}", parallel_time);
557
558        // Parallel submission should be faster or at least comparable
559        assert!(
560            parallel_time <= sequential_time * 2,
561            "Parallel submission should not be significantly slower"
562        );
563    }
564
565    /// Test resource management and worker type determination
566    #[tokio::test]
567    async fn test_resource_management() {
568        let pool = WorkerPool::new(3);
569
570        // Test worker type determination
571        assert_eq!(
572            pool.determine_worker_type("convert"),
573            WorkerType::CpuIntensive
574        );
575        assert_eq!(pool.determine_worker_type("sync"), WorkerType::Mixed);
576        assert_eq!(pool.determine_worker_type("match"), WorkerType::IoIntensive);
577        assert_eq!(
578            pool.determine_worker_type("validate"),
579            WorkerType::IoIntensive
580        );
581        assert_eq!(pool.determine_worker_type("unknown"), WorkerType::Mixed);
582
583        // Test initial stats
584        let stats = pool.get_worker_stats();
585        assert_eq!(stats.total_active, 0);
586        assert_eq!(stats.max_capacity, 3);
587        assert_eq!(stats.cpu_intensive_count, 0);
588        assert_eq!(stats.io_intensive_count, 0);
589        assert_eq!(stats.mixed_count, 0);
590
591        // Test capacity management
592        assert_eq!(pool.get_capacity(), 3);
593        assert_eq!(pool.get_active_count(), 0);
594    }
595
596    /// Test worker pool shutdown mechanism
597    #[tokio::test]
598    async fn test_worker_pool_shutdown() {
599        use crate::core::parallel::task::{Task, TaskResult};
600        use async_trait::async_trait;
601
602        #[derive(Clone)]
603        struct SlowTask {
604            id: String,
605        }
606
607        #[async_trait]
608        impl Task for SlowTask {
609            async fn execute(&self) -> TaskResult {
610                tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
611                TaskResult::Success(format!("slow-{}", self.id))
612            }
613            fn task_type(&self) -> &'static str {
614                "mixed"
615            }
616            fn task_id(&self) -> String {
617                self.id.clone()
618            }
619        }
620
621        let pool = WorkerPool::new(2);
622
623        // Submit some slow tasks
624        for i in 0..2 {
625            let task = SlowTask {
626                id: format!("slow-{}", i),
627            };
628            pool.execute(Box::new(task)).await.unwrap();
629        }
630
631        // Verify tasks are active
632        assert!(pool.get_active_count() <= 2);
633
634        // Test shutdown - should wait for completion
635        let start = std::time::Instant::now();
636        pool.shutdown().await;
637        let shutdown_time = start.elapsed();
638
639        // Shutdown should take some time waiting for tasks
640        assert!(shutdown_time >= std::time::Duration::from_millis(30));
641
642        // After shutdown, no workers should be active
643        assert_eq!(pool.get_active_count(), 0);
644    }
645
646    /// Test active worker information tracking
647    #[tokio::test]
648    async fn test_active_worker_tracking() {
649        use crate::core::parallel::task::{Task, TaskResult};
650        use async_trait::async_trait;
651
652        #[derive(Clone)]
653        struct TrackableTask {
654            id: String,
655            task_type: &'static str,
656        }
657
658        #[async_trait]
659        impl Task for TrackableTask {
660            async fn execute(&self) -> TaskResult {
661                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
662                TaskResult::Success(format!("tracked-{}", self.id))
663            }
664            fn task_type(&self) -> &'static str {
665                self.task_type
666            }
667            fn task_id(&self) -> String {
668                self.id.clone()
669            }
670        }
671
672        let pool = WorkerPool::new(3);
673
674        // Submit different types of tasks
675        let tasks = vec![
676            ("cpu-task", "convert"),
677            ("io-task", "match"),
678            ("mixed-task", "sync"),
679        ];
680
681        for (id, task_type) in tasks {
682            let task = TrackableTask {
683                id: id.to_string(),
684                task_type,
685            };
686            pool.execute(Box::new(task)).await.unwrap();
687        }
688
689        // Check active workers
690        let active_workers = pool.list_active_workers();
691        assert!(active_workers.len() <= 3, "Should not exceed pool capacity");
692
693        // Verify worker information is tracked
694        for worker in &active_workers {
695            assert!(!worker.task_id.is_empty(), "Task ID should be set");
696            assert!(matches!(
697                worker.worker_type,
698                WorkerType::CpuIntensive | WorkerType::IoIntensive | WorkerType::Mixed
699            ));
700            assert!(
701                worker.runtime.as_millis() < u128::MAX,
702                "Runtime should be valid"
703            );
704        }
705
706        // Check worker stats
707        let stats = pool.get_worker_stats();
708        assert!(stats.total_active <= 3);
709        assert_eq!(stats.max_capacity, 3);
710
711        // Wait for tasks to complete
712        tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
713    }
714
715    #[test]
716    fn worker_id_is_uuidv7() {
717        let w = Worker::new();
718        assert_eq!(w.id().get_version_num(), 7);
719    }
720
721    #[test]
722    fn consecutive_workers_have_distinct_ids() {
723        let a = Worker::new();
724        let b = Worker::new();
725        assert_ne!(a.id(), b.id());
726    }
727
728    #[tokio::test]
729    async fn worker_pool_execute_dispatches_uuidv7_worker_id() {
730        use crate::core::parallel::task::{Task, TaskResult};
731        use async_trait::async_trait;
732
733        struct DummyTask;
734
735        #[async_trait]
736        impl Task for DummyTask {
737            async fn execute(&self) -> TaskResult {
738                TaskResult::Success("done".into())
739            }
740            fn task_type(&self) -> &'static str {
741                "match"
742            }
743            fn task_id(&self) -> String {
744                "dummy".into()
745            }
746        }
747
748        let pool = WorkerPool::new(1);
749        let res = pool.execute(Box::new(DummyTask)).await;
750        assert!(matches!(res, Ok(TaskResult::Success(_))));
751
752        let workers = pool.list_active_workers();
753        assert_eq!(workers.len(), 1);
754        assert_eq!(workers[0].worker_id.get_version_num(), 7);
755    }
756}