Skip to main content

pharos_runtime_core/
worker_pool.rs

1//! Bounded native worker-pool primitives for runtime request orchestration.
2//!
3//! The pool owns its channel and worker joins, rejects work after shutdown, and
4//! never uses an unbounded queue or an unsafe FFI bridge.
5
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex, mpsc};
8use std::thread::{self, JoinHandle};
9use std::time::Duration;
10
11const MAX_WORKERS: usize = 128;
12const MAX_QUEUE_CAPACITY: usize = 16_384;
13/// Approximate queue-memory accounting unit used for bounded work sizing.
14///
15/// # Examples
16///
17/// ```rust,ignore
18/// let _ = pharos_runtime_core::worker_pool::PHAROS_NATIVE_WORK_QUEUE_BYTES_PER_SLOT;
19/// ```
20///
21/// # Related
22///
23/// - [`crate::worker_pool`] for the surrounding module surface.
24pub const PHAROS_NATIVE_WORK_QUEUE_BYTES_PER_SLOT: usize = 4096;
25
26#[derive(Debug, Clone, Copy, Eq, PartialEq)]
27/// Errors returned by bounded worker-pool lifecycle operations.
28///
29/// # Variants
30///
31/// - `InvalidConfiguration`: Supported `WorkerPoolError` variant.
32/// - `Shutdown`: Supported `WorkerPoolError` variant.
33/// - `QueueFull`: Supported `WorkerPoolError` variant.
34/// - `Poisoned`: Supported `WorkerPoolError` variant.
35/// - `WorkerPanicked`: Supported `WorkerPoolError` variant.
36///
37/// # Examples
38///
39/// ```rust,ignore
40/// let _ = pharos_runtime_core::worker_pool::WorkerPoolError::InvalidConfiguration;
41/// ```
42///
43/// # Related
44///
45/// - [`crate::worker_pool`] for constructors, helpers, and neighboring runtime types.
46pub enum WorkerPoolError {
47    /// The requested size is outside the safe configured range.
48    InvalidConfiguration,
49    /// The queue has been closed or shutdown has been requested.
50    Shutdown,
51    /// The bounded queue is full and the caller must apply overload behavior.
52    QueueFull,
53    /// A lifecycle mutex was poisoned by a worker or caller panic.
54    Poisoned,
55    /// A worker panicked while joining.
56    WorkerPanicked,
57}
58
59/// A bounded worker pool that executes an owned callback for each item.
60///
61/// # Fields
62///
63/// - This type stores module-owned state for [`crate::worker_pool::WorkerPool`].
64///
65/// # Examples
66///
67/// ```rust,ignore
68/// let _ = std::mem::size_of::<pharos_runtime_core::worker_pool::WorkerPool>();
69/// ```
70///
71/// # Related
72///
73/// - [`crate::worker_pool`] for constructors, helpers, and neighboring runtime types.
74pub struct WorkerPool<T: Send + 'static> {
75    sender: Mutex<Option<mpsc::SyncSender<T>>>,
76    shutdown: Arc<AtomicBool>,
77    workers: Mutex<Vec<JoinHandle<()>>>,
78}
79
80impl<T: Send + 'static> WorkerPool<T> {
81    /// Creates a pool with a bounded queue and a shared item handler.
82    ///
83    /// # Parameters
84    ///
85    /// - `worker_count`: Input consumed by [`crate::worker_pool::new`].
86    /// - `queue_capacity`: Input consumed by [`crate::worker_pool::new`].
87    /// - `handler`: Input consumed by [`crate::worker_pool::new`].
88    ///
89    /// # Returns
90    ///
91    /// Returns the successful value produced by [`crate::worker_pool::new`] or an error when the operation fails.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error value when [`crate::worker_pool::new`] cannot complete successfully.
96    ///
97    /// # Performance
98    ///
99    /// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::worker_pool::new`].
100    ///
101    /// # Examples
102    ///
103    /// ```rust,ignore
104    /// let _result = pharos_runtime_core::worker_pool::new(todo!(), todo!(), todo!());
105    /// ```
106    ///
107    /// # Related
108    ///
109    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
110    pub fn new(
111        worker_count: usize,
112        queue_capacity: usize,
113        handler: Arc<dyn Fn(T) + Send + Sync + 'static>,
114    ) -> Result<Self, WorkerPoolError> {
115        if worker_count == 0
116            || worker_count > MAX_WORKERS
117            || queue_capacity == 0
118            || queue_capacity > MAX_QUEUE_CAPACITY
119        {
120            return Err(WorkerPoolError::InvalidConfiguration);
121        }
122        let (sender, receiver) = mpsc::sync_channel(queue_capacity);
123        let receiver = Arc::new(Mutex::new(receiver));
124        let shutdown = Arc::new(AtomicBool::new(false));
125        let mut workers = Vec::with_capacity(worker_count);
126        for index in 0..worker_count {
127            let receiver = Arc::clone(&receiver);
128            let shutdown = Arc::clone(&shutdown);
129            let handler = Arc::clone(&handler);
130            let worker = thread::Builder::new()
131                .name(format!("pharos-worker-{index}"))
132                .spawn(move || {
133                    loop {
134                        let item = match receiver.lock() {
135                            Ok(lock) => lock.recv_timeout(Duration::from_millis(100)),
136                            Err(_) => break,
137                        };
138                        match item {
139                            Ok(item) => handler(item),
140                            Err(mpsc::RecvTimeoutError::Timeout)
141                                if shutdown.load(Ordering::Acquire) =>
142                            {
143                                break;
144                            }
145                            Err(mpsc::RecvTimeoutError::Timeout) => continue,
146                            Err(mpsc::RecvTimeoutError::Disconnected) => break,
147                        }
148                    }
149                })
150                .map_err(|_| WorkerPoolError::Shutdown)?;
151            workers.push(worker);
152        }
153        Ok(Self {
154            sender: Mutex::new(Some(sender)),
155            shutdown,
156            workers: Mutex::new(workers),
157        })
158    }
159
160    /// Enqueues one item without allowing the queue to grow without bound.
161    ///
162    /// # Parameters
163    ///
164    /// - `item`: Input consumed by [`crate::worker_pool::enqueue`].
165    ///
166    /// # Returns
167    ///
168    /// Returns the successful value produced by [`crate::worker_pool::enqueue`] or an error when the operation fails.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error value when [`crate::worker_pool::enqueue`] cannot complete successfully.
173    ///
174    /// # Examples
175    ///
176    /// ```rust,ignore
177    /// let _result = pharos_runtime_core::worker_pool::enqueue(todo!());
178    /// ```
179    ///
180    /// # Related
181    ///
182    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
183    pub fn enqueue(&self, item: T) -> Result<(), WorkerPoolError> {
184        self.enqueue_with_rejection(item, |_| {})
185    }
186
187    /// Enqueues one item and returns ownership to a bounded overload handler when full.
188    ///
189    /// # Parameters
190    ///
191    /// - `item`: Input consumed by [`crate::worker_pool::enqueue_with_rejection`].
192    /// - `reject`: Input consumed by [`crate::worker_pool::enqueue_with_rejection`].
193    ///
194    /// # Returns
195    ///
196    /// Returns the successful value produced by [`crate::worker_pool::enqueue_with_rejection`] or an error when the operation fails.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error value when [`crate::worker_pool::enqueue_with_rejection`] cannot complete successfully.
201    ///
202    /// # Examples
203    ///
204    /// ```rust,ignore
205    /// let _result = pharos_runtime_core::worker_pool::enqueue_with_rejection(todo!(), todo!());
206    /// ```
207    ///
208    /// # Related
209    ///
210    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
211    pub fn enqueue_with_rejection<F>(&self, item: T, reject: F) -> Result<(), WorkerPoolError>
212    where
213        F: FnOnce(T),
214    {
215        if self.shutdown.load(Ordering::Acquire) {
216            reject(item);
217            return Err(WorkerPoolError::Shutdown);
218        }
219        let sender = self
220            .sender
221            .lock()
222            .map_err(|_| WorkerPoolError::Poisoned)?
223            .clone()
224            .ok_or(WorkerPoolError::Shutdown)?;
225        match sender.try_send(item) {
226            Ok(()) => Ok(()),
227            Err(mpsc::TrySendError::Full(item)) => {
228                reject(item);
229                Err(WorkerPoolError::QueueFull)
230            }
231            Err(mpsc::TrySendError::Disconnected(item)) => {
232                reject(item);
233                Err(WorkerPoolError::Shutdown)
234            }
235        }
236    }
237
238    /// Requests shutdown and closes the queue to new work.
239    ///
240    /// # Returns
241    ///
242    /// Returns the successful value produced by [`crate::worker_pool::request_shutdown`] or an error when the operation fails.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error value when [`crate::worker_pool::request_shutdown`] cannot complete successfully.
247    ///
248    /// # Examples
249    ///
250    /// ```rust,ignore
251    /// let _result = pharos_runtime_core::worker_pool::request_shutdown();
252    /// ```
253    ///
254    /// # Related
255    ///
256    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
257    pub fn request_shutdown(&self) -> Result<(), WorkerPoolError> {
258        self.shutdown.store(true, Ordering::Release);
259        let mut sender = self.sender.lock().map_err(|_| WorkerPoolError::Poisoned)?;
260        sender.take();
261        Ok(())
262    }
263
264    /// Joins all workers after shutdown has been requested.
265    ///
266    /// # Returns
267    ///
268    /// Returns the successful value produced by [`crate::worker_pool::join`] or an error when the operation fails.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error value when [`crate::worker_pool::join`] cannot complete successfully.
273    ///
274    /// # Performance
275    ///
276    /// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::worker_pool::join`].
277    ///
278    /// # Examples
279    ///
280    /// ```rust,ignore
281    /// let _result = pharos_runtime_core::worker_pool::join();
282    /// ```
283    ///
284    /// # Related
285    ///
286    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
287    pub fn join(&self) -> Result<(), WorkerPoolError> {
288        self.request_shutdown()?;
289        let mut workers = self.workers.lock().map_err(|_| WorkerPoolError::Poisoned)?;
290        let handles = std::mem::take(&mut *workers);
291        for handle in handles {
292            if handle.join().is_err() {
293                return Err(WorkerPoolError::WorkerPanicked);
294            }
295        }
296        Ok(())
297    }
298
299    /// Shuts down and joins the pool, releasing all worker handles.
300    ///
301    /// # Returns
302    ///
303    /// Returns the successful value produced by [`crate::worker_pool::destroy`] or an error when the operation fails.
304    ///
305    /// # Errors
306    ///
307    /// Returns an error value when [`crate::worker_pool::destroy`] cannot complete successfully.
308    ///
309    /// # Examples
310    ///
311    /// ```rust,ignore
312    /// let _result = pharos_runtime_core::worker_pool::destroy();
313    /// ```
314    ///
315    /// # Related
316    ///
317    /// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
318    pub fn destroy(self) -> Result<(), WorkerPoolError> {
319        self.join()
320    }
321}
322
323impl<T: Send + 'static> Drop for WorkerPool<T> {
324    /// Performs drop.
325    fn drop(&mut self) {
326        self.shutdown.store(true, Ordering::Release);
327        if let Ok(mut sender) = self.sender.lock() {
328            sender.take();
329        }
330        if let Ok(mut workers) = self.workers.lock() {
331            for handle in std::mem::take(&mut *workers) {
332                let _ = handle.join();
333            }
334        }
335    }
336}
337
338/// Executes one worker thread main-loop with bounded recv polling and cooperative shutdown.
339///
340/// # Parameters
341///
342/// - `receiver`: Input consumed by [`crate::worker_pool::worker_pool_thread_main`].
343/// - `shutdown`: Input consumed by [`crate::worker_pool::worker_pool_thread_main`].
344/// - `handler`: Input consumed by [`crate::worker_pool::worker_pool_thread_main`].
345///
346/// # Returns
347///
348/// Returns after [`crate::worker_pool::worker_pool_thread_main`] applies its in-place effect.
349///
350/// # Examples
351///
352/// ```rust,ignore
353/// let _result = pharos_runtime_core::worker_pool::worker_pool_thread_main(todo!(), todo!(), todo!());
354/// ```
355///
356/// # Related
357///
358/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
359pub fn worker_pool_thread_main<T: Send + 'static>(
360    receiver: Arc<Mutex<mpsc::Receiver<T>>>,
361    shutdown: Arc<AtomicBool>,
362    handler: Arc<dyn Fn(T) + Send + Sync + 'static>,
363) {
364    loop {
365        let item = match receiver.lock() {
366            Ok(lock) => lock.recv_timeout(Duration::from_millis(100)),
367            Err(_) => break,
368        };
369        match item {
370            Ok(item) => handler(item),
371            Err(mpsc::RecvTimeoutError::Timeout) if shutdown.load(Ordering::Acquire) => break,
372            Err(mpsc::RecvTimeoutError::Timeout) => continue,
373            Err(mpsc::RecvTimeoutError::Disconnected) => break,
374        }
375    }
376}
377
378/// Creates the native string-work worker pool with bounded resources.
379///
380/// # Parameters
381///
382/// - `worker_count`: Input consumed by [`crate::worker_pool::worker_pool_init`].
383/// - `queue_capacity`: Input consumed by [`crate::worker_pool::worker_pool_init`].
384/// - `handler`: Input consumed by [`crate::worker_pool::worker_pool_init`].
385///
386/// # Returns
387///
388/// Returns the successful value produced by [`crate::worker_pool::worker_pool_init`] or an error when the operation fails.
389///
390/// # Errors
391///
392/// Returns an error value when [`crate::worker_pool::worker_pool_init`] cannot complete successfully.
393///
394/// # Examples
395///
396/// ```rust,ignore
397/// let _result = pharos_runtime_core::worker_pool::worker_pool_init(todo!(), todo!(), todo!());
398/// ```
399///
400/// # Related
401///
402/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
403pub fn worker_pool_init(
404    worker_count: usize,
405    queue_capacity: usize,
406    handler: Arc<dyn Fn(String) + Send + Sync + 'static>,
407) -> Result<WorkerPool<String>, WorkerPoolError> {
408    WorkerPool::new(worker_count, queue_capacity, handler)
409}
410
411/// Enqueues one string request and fails when the bounded queue is closed.
412///
413/// # Parameters
414///
415/// - `pool`: Input consumed by [`crate::worker_pool::worker_pool_enqueue`].
416/// - `item`: Input consumed by [`crate::worker_pool::worker_pool_enqueue`].
417///
418/// # Returns
419///
420/// Returns the successful value produced by [`crate::worker_pool::worker_pool_enqueue`] or an error when the operation fails.
421///
422/// # Errors
423///
424/// Returns an error value when [`crate::worker_pool::worker_pool_enqueue`] cannot complete successfully.
425///
426/// # Examples
427///
428/// ```rust,ignore
429/// let _result = pharos_runtime_core::worker_pool::worker_pool_enqueue(todo!(), todo!());
430/// ```
431///
432/// # Related
433///
434/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
435pub fn worker_pool_enqueue(pool: &WorkerPool<String>, item: String) -> Result<(), WorkerPoolError> {
436    pool.enqueue(item)
437}
438
439/// Requests shutdown of a string worker pool.
440///
441/// # Parameters
442///
443/// - `pool`: Input consumed by [`crate::worker_pool::worker_pool_request_shutdown`].
444///
445/// # Returns
446///
447/// Returns the successful value produced by [`crate::worker_pool::worker_pool_request_shutdown`] or an error when the operation fails.
448///
449/// # Errors
450///
451/// Returns an error value when [`crate::worker_pool::worker_pool_request_shutdown`] cannot complete successfully.
452///
453/// # Examples
454///
455/// ```rust,ignore
456/// let _result = pharos_runtime_core::worker_pool::worker_pool_request_shutdown(todo!());
457/// ```
458///
459/// # Related
460///
461/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
462pub fn worker_pool_request_shutdown(pool: &WorkerPool<String>) -> Result<(), WorkerPoolError> {
463    pool.request_shutdown()
464}
465
466/// Joins every worker after shutdown.
467///
468/// # Parameters
469///
470/// - `pool`: Input consumed by [`crate::worker_pool::worker_pool_join`].
471///
472/// # Returns
473///
474/// Returns the successful value produced by [`crate::worker_pool::worker_pool_join`] or an error when the operation fails.
475///
476/// # Errors
477///
478/// Returns an error value when [`crate::worker_pool::worker_pool_join`] cannot complete successfully.
479///
480/// # Examples
481///
482/// ```rust,ignore
483/// let _result = pharos_runtime_core::worker_pool::worker_pool_join(todo!());
484/// ```
485///
486/// # Related
487///
488/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
489pub fn worker_pool_join(pool: &WorkerPool<String>) -> Result<(), WorkerPoolError> {
490    pool.join()
491}
492
493/// Destroys a string worker pool after joining its workers.
494///
495/// # Parameters
496///
497/// - `pool`: Input consumed by [`crate::worker_pool::worker_pool_destroy`].
498///
499/// # Returns
500///
501/// Returns the successful value produced by [`crate::worker_pool::worker_pool_destroy`] or an error when the operation fails.
502///
503/// # Errors
504///
505/// Returns an error value when [`crate::worker_pool::worker_pool_destroy`] cannot complete successfully.
506///
507/// # Examples
508///
509/// ```rust,ignore
510/// let _result = pharos_runtime_core::worker_pool::worker_pool_destroy(todo!());
511/// ```
512///
513/// # Related
514///
515/// - [`crate::worker_pool`] for adjacent helpers that share the same runtime surface.
516pub fn worker_pool_destroy(pool: WorkerPool<String>) -> Result<(), WorkerPoolError> {
517    pool.destroy()
518}
519
520#[cfg(test)]
521mod tests {
522    use super::{WorkerPool, WorkerPoolError};
523    use std::sync::atomic::{AtomicUsize, Ordering};
524    use std::sync::{Arc, Barrier};
525
526    /// Confirms a saturated pool rejects immediately and returns the item to the overload handler.
527    #[test]
528    fn rejects_work_when_the_bounded_queue_is_full() {
529        let entered = Arc::new(Barrier::new(2));
530        let release = Arc::new(Barrier::new(2));
531        let worker_entered = Arc::clone(&entered);
532        let worker_release = Arc::clone(&release);
533        let handled = Arc::new(AtomicUsize::new(0));
534        let worker_handled = Arc::clone(&handled);
535        let pool = WorkerPool::new(
536            1,
537            1,
538            Arc::new(move |_item: usize| {
539                if worker_handled.fetch_add(1, Ordering::SeqCst) == 0 {
540                    worker_entered.wait();
541                    worker_release.wait();
542                }
543            }),
544        )
545        .expect("create worker pool");
546        pool.enqueue(1).expect("enqueue active work");
547        entered.wait();
548        pool.enqueue(2).expect("fill queue");
549        let mut rejected = None;
550        let result = pool.enqueue_with_rejection(3, |item| rejected = Some(item));
551        assert_eq!(result, Err(WorkerPoolError::QueueFull));
552        assert_eq!(rejected, Some(3));
553        release.wait();
554        pool.destroy().expect("drain worker pool");
555    }
556
557    /// Confirms one slow task does not block an independent worker beyond the latency budget.
558    #[test]
559    fn serves_fast_work_while_another_worker_is_slow() {
560        let (completed, receiver) = std::sync::mpsc::channel();
561        let pool = WorkerPool::new(
562            2,
563            2,
564            Arc::new(move |slow: bool| {
565                if slow {
566                    std::thread::sleep(std::time::Duration::from_millis(200));
567                } else {
568                    let _ = completed.send(());
569                }
570            }),
571        )
572        .expect("create concurrent worker pool");
573        let started = std::time::Instant::now();
574        pool.enqueue(true).expect("enqueue slow work");
575        pool.enqueue(false).expect("enqueue fast work");
576        receiver
577            .recv_timeout(std::time::Duration::from_millis(100))
578            .expect("fast work latency budget");
579        assert!(started.elapsed() < std::time::Duration::from_millis(150));
580        pool.destroy().expect("drain concurrent worker pool");
581    }
582}