Skip to main content

foxtive_supervisor/
task_pool.rs

1//! Task pool and load balancing support
2//!
3//! This module provides the ability to create pools of worker tasks that can
4//! distribute workload across multiple instances for better performance and reliability.
5
6use crate::Supervisor;
7use crate::contracts::SupervisedTask;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::info;
11
12/// Load balancing strategy for task pools
13#[derive(Debug, Clone, Copy, Default)]
14pub enum LoadBalancingStrategy {
15    /// Round-robin: Distribute tasks evenly in order
16    #[default]
17    RoundRobin,
18    /// Random: Select a random worker from the pool
19    Random,
20    /// Least loaded: Select the worker with fewest active tasks (requires tracking)
21    LeastLoaded,
22}
23
24/// A pool of worker tasks for load balancing
25pub struct TaskPool {
26    /// Pool identifier
27    pub id: String,
28    /// Number of workers in the pool
29    pub pool_size: usize,
30    /// Load balancing strategy
31    pub strategy: LoadBalancingStrategy,
32    /// Current round-robin index
33    current_index: Arc<RwLock<usize>>,
34}
35
36impl TaskPool {
37    /// Create a new task pool
38    pub fn new(id: impl Into<String>, pool_size: usize, strategy: LoadBalancingStrategy) -> Self {
39        Self {
40            id: id.into(),
41            pool_size,
42            strategy,
43            current_index: Arc::new(RwLock::new(0)),
44        }
45    }
46
47    /// Get the next worker index based on the load balancing strategy
48    pub async fn get_next_worker(&self) -> usize {
49        match self.strategy {
50            LoadBalancingStrategy::RoundRobin => {
51                let mut index = self.current_index.write().await;
52                let worker = *index;
53                *index = (*index + 1) % self.pool_size;
54                worker
55            }
56            LoadBalancingStrategy::Random => {
57                // Note: rand is a non-optional dependency because it's used here (always compiled)
58                // and in supervision.rs for jitter (feature-gated under 'cron')
59                rand::random_range(0..self.pool_size)
60            }
61            LoadBalancingStrategy::LeastLoaded => {
62                // For now, fall back to round-robin
63                // In a real implementation, we'd track active tasks per worker
64                let mut index = self.current_index.write().await;
65                let worker = *index;
66                *index = (*index + 1) % self.pool_size;
67                worker
68            }
69        }
70    }
71
72    /// Build a supervisor with pooled workers
73    pub fn build_pool<T, F>(&self, task_factory: F) -> Supervisor
74    where
75        T: SupervisedTask + 'static,
76        F: Fn(usize) -> T,
77    {
78        let mut supervisor = Supervisor::new();
79
80        for i in 0..self.pool_size {
81            let task = task_factory(i);
82            supervisor = supervisor.add(task);
83        }
84
85        info!(pool_id = %self.id, size = self.pool_size, "Created task pool");
86        supervisor
87    }
88
89    /// Get pool information
90    pub fn info(&self) -> PoolInfo {
91        PoolInfo {
92            id: self.id.clone(),
93            pool_size: self.pool_size,
94            strategy: self.strategy,
95        }
96    }
97}
98
99/// Information about a task pool
100#[derive(Debug, Clone)]
101pub struct PoolInfo {
102    pub id: String,
103    pub pool_size: usize,
104    pub strategy: LoadBalancingStrategy,
105}
106
107/// Builder for creating task pools
108pub struct TaskPoolBuilder {
109    id: String,
110    pool_size: usize,
111    strategy: LoadBalancingStrategy,
112}
113
114impl TaskPoolBuilder {
115    /// Create a new task pool builder
116    pub fn new(id: impl Into<String>) -> Self {
117        Self {
118            id: id.into(),
119            pool_size: 1,
120            strategy: LoadBalancingStrategy::default(),
121        }
122    }
123
124    /// Set the pool size
125    pub fn with_size(mut self, size: usize) -> Self {
126        self.pool_size = size.max(1);
127        self
128    }
129
130    /// Set the load balancing strategy
131    pub fn with_strategy(mut self, strategy: LoadBalancingStrategy) -> Self {
132        self.strategy = strategy;
133        self
134    }
135
136    /// Build the task pool
137    pub fn build(self) -> TaskPool {
138        TaskPool::new(self.id, self.pool_size, self.strategy)
139    }
140
141    /// Build and immediately create a supervisor with pooled workers
142    pub fn build_and_create<T, F>(self, task_factory: F) -> Supervisor
143    where
144        T: SupervisedTask + 'static,
145        F: Fn(usize) -> T,
146    {
147        let pool = self.build();
148        pool.build_pool(task_factory)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[tokio::test]
157    async fn test_round_robin_distribution() {
158        let pool = TaskPool::new("test-pool", 3, LoadBalancingStrategy::RoundRobin);
159
160        // Should cycle through 0, 1, 2, 0, 1, 2...
161        assert_eq!(pool.get_next_worker().await, 0);
162        assert_eq!(pool.get_next_worker().await, 1);
163        assert_eq!(pool.get_next_worker().await, 2);
164        assert_eq!(pool.get_next_worker().await, 0);
165        assert_eq!(pool.get_next_worker().await, 1);
166    }
167
168    #[tokio::test]
169    async fn test_random_distribution() {
170        let pool = TaskPool::new("test-pool", 5, LoadBalancingStrategy::Random);
171
172        // All results should be within bounds
173        for _ in 0..20 {
174            let worker = pool.get_next_worker().await;
175            assert!(worker < 5);
176        }
177    }
178
179    #[tokio::test]
180    async fn test_pool_builder() {
181        let pool = TaskPoolBuilder::new("worker-pool")
182            .with_size(4)
183            .with_strategy(LoadBalancingStrategy::RoundRobin)
184            .build();
185
186        assert_eq!(pool.id, "worker-pool");
187        assert_eq!(pool.pool_size, 4);
188        assert!(matches!(pool.strategy, LoadBalancingStrategy::RoundRobin));
189    }
190
191    #[tokio::test]
192    async fn test_pool_info() {
193        let pool = TaskPool::new("info-test", 3, LoadBalancingStrategy::Random);
194        let info = pool.info();
195
196        assert_eq!(info.id, "info-test");
197        assert_eq!(info.pool_size, 3);
198        assert!(matches!(info.strategy, LoadBalancingStrategy::Random));
199    }
200}