Skip to main content

reth_tasks/
pool.rs

1//! Additional helpers for executing tracing calls
2
3use crate::metrics::WorkerPoolMetrics;
4use std::{
5    any::Any,
6    cell::RefCell,
7    future::Future,
8    ops::Deref,
9    panic::{catch_unwind, AssertUnwindSafe},
10    pin::Pin,
11    sync::{
12        atomic::{AtomicUsize, Ordering},
13        Arc, OnceLock, Weak,
14    },
15    task::{ready, Context, Poll},
16    thread,
17    time::Instant,
18};
19use tokio::sync::{oneshot, AcquireError, OwnedSemaphorePermit, Semaphore};
20
21/// RPC Tracing call guard semaphore.
22///
23/// This is used to restrict the number of concurrent RPC requests to tracing methods like
24/// `debug_traceTransaction` as well as `eth_getProof` because they can consume a lot of
25/// memory and CPU.
26///
27/// This types serves as an entry guard for the [`BlockingTaskPool`] and is used to rate limit
28/// parallel blocking tasks in the pool.
29#[derive(Clone, Debug)]
30pub struct BlockingTaskGuard(Arc<Semaphore>);
31
32impl BlockingTaskGuard {
33    /// Create a new `BlockingTaskGuard` with the given maximum number of blocking tasks in
34    /// parallel.
35    pub fn new(max_blocking_tasks: usize) -> Self {
36        Self(Arc::new(Semaphore::new(max_blocking_tasks)))
37    }
38
39    /// See also [`Semaphore::acquire_owned`]
40    pub async fn acquire_owned(self) -> Result<OwnedSemaphorePermit, AcquireError> {
41        self.0.acquire_owned().await
42    }
43
44    /// See also [`Semaphore::acquire_many_owned`]
45    pub async fn acquire_many_owned(self, n: u32) -> Result<OwnedSemaphorePermit, AcquireError> {
46        self.0.acquire_many_owned(n).await
47    }
48}
49
50/// Used to execute blocking tasks on a rayon threadpool from within a tokio runtime.
51///
52/// This is a dedicated threadpool for blocking tasks which are CPU bound.
53/// RPC calls that perform blocking IO (disk lookups) are not executed on this pool but on the tokio
54/// runtime's blocking pool, which performs poorly with CPU bound tasks (see
55/// <https://ryhl.io/blog/async-what-is-blocking/>). Once the tokio blocking
56/// pool is saturated it is converted into a queue, blocking tasks could then interfere with the
57/// queue and block other RPC calls.
58///
59/// See also [tokio-docs] for more information.
60///
61/// [tokio-docs]: https://docs.rs/tokio/latest/tokio/index.html#cpu-bound-tasks-and-blocking-code
62#[derive(Clone, Debug)]
63pub struct BlockingTaskPool {
64    pool: Arc<rayon::ThreadPool>,
65}
66
67impl BlockingTaskPool {
68    /// Create a new `BlockingTaskPool` with the given threadpool.
69    pub fn new(pool: rayon::ThreadPool) -> Self {
70        Self { pool: Arc::new(pool) }
71    }
72
73    /// Convenience function to start building a new threadpool.
74    pub fn builder() -> rayon::ThreadPoolBuilder {
75        rayon::ThreadPoolBuilder::new()
76    }
77
78    /// Convenience function to build a new threadpool with the default configuration.
79    ///
80    /// Uses [`rayon::ThreadPoolBuilder::build`](rayon::ThreadPoolBuilder::build) defaults.
81    /// If a different stack size or other parameters are needed, they can be configured via
82    /// [`rayon::ThreadPoolBuilder`] returned by [`Self::builder`].
83    pub fn build() -> Result<Self, rayon::ThreadPoolBuildError> {
84        Self::builder().build().map(Self::new)
85    }
86
87    /// Asynchronous wrapper around Rayon's
88    /// [`ThreadPool::spawn`](rayon::ThreadPool::spawn).
89    ///
90    /// Runs a function on the configured threadpool, returning a future that resolves with the
91    /// function's return value.
92    ///
93    /// If the function panics, the future will resolve to an error.
94    pub fn spawn<F, R>(&self, func: F) -> BlockingTaskHandle<R>
95    where
96        F: FnOnce() -> R + Send + 'static,
97        R: Send + 'static,
98    {
99        let (tx, rx) = oneshot::channel();
100
101        self.pool.spawn(move || {
102            let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
103        });
104
105        BlockingTaskHandle { rx }
106    }
107
108    /// Asynchronous wrapper around Rayon's
109    /// [`ThreadPool::spawn_fifo`](rayon::ThreadPool::spawn_fifo).
110    ///
111    /// Runs a function on the configured threadpool, returning a future that resolves with the
112    /// function's return value.
113    ///
114    /// If the function panics, the future will resolve to an error.
115    pub fn spawn_fifo<F, R>(&self, func: F) -> BlockingTaskHandle<R>
116    where
117        F: FnOnce() -> R + Send + 'static,
118        R: Send + 'static,
119    {
120        let (tx, rx) = oneshot::channel();
121
122        self.pool.spawn_fifo(move || {
123            let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
124        });
125
126        BlockingTaskHandle { rx }
127    }
128}
129
130/// Async handle for a blocking task running in a Rayon thread pool.
131///
132/// ## Panics
133///
134/// If polled from outside a tokio runtime.
135#[derive(Debug)]
136#[must_use = "futures do nothing unless you `.await` or poll them"]
137#[pin_project::pin_project]
138pub struct BlockingTaskHandle<T> {
139    #[pin]
140    pub(crate) rx: oneshot::Receiver<thread::Result<T>>,
141}
142
143impl<T> Future for BlockingTaskHandle<T> {
144    type Output = thread::Result<T>;
145
146    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
147        match ready!(self.project().rx.poll(cx)) {
148            Ok(res) => Poll::Ready(res),
149            Err(_) => Poll::Ready(Err(Box::<TokioBlockingTaskError>::default())),
150        }
151    }
152}
153
154/// An error returned when the Tokio channel is dropped while awaiting a result.
155///
156/// This should only happen
157#[derive(Debug, Default, thiserror::Error)]
158#[error("tokio channel dropped while awaiting result")]
159#[non_exhaustive]
160pub struct TokioBlockingTaskError;
161
162thread_local! {
163    static WORKER: RefCell<Worker> = const { RefCell::new(Worker::new()) };
164}
165
166/// A rayon thread pool with per-thread [`Worker`] state.
167///
168/// Each thread in the pool has its own [`Worker`] that can hold arbitrary state via
169/// [`WorkerStatefulPool::init`]. Access state through its owner from jobs executing on the pool,
170/// for example inside [`install_fn`](Self::install_fn).
171///
172/// Worker access is backed by a thread-local [`RefCell`]. Keep [`with_worker`](Self::with_worker)
173/// and [`with_worker_mut`](Self::with_worker_mut) closures short and non-yielding: if Rayon runs
174/// another job on the same thread while a borrow is active, re-entrant worker access can panic.
175///
176/// Independent [`WorkerStatefulPool`] owners can reuse the same threads with different state
177/// configurations; each owner clears its state when dropped.
178///
179/// The underlying rayon pool is created lazily on first access.
180#[derive(Debug)]
181pub struct WorkerPool {
182    pool: OnceLock<rayon::ThreadPool>,
183    metrics: OnceLock<WorkerPoolMetrics>,
184    num_threads: usize,
185    thread_name_prefix: &'static str,
186}
187
188impl WorkerPool {
189    /// Creates a new lazy `WorkerPool` with the given number of threads and a thread name prefix.
190    ///
191    /// The underlying rayon pool is not created until the first method that requires it is called.
192    /// Thread names follow the pattern `"{prefix}-{index:02}"`.
193    pub const fn new(num_threads: usize, thread_name_prefix: &'static str) -> Self {
194        Self { pool: OnceLock::new(), metrics: OnceLock::new(), num_threads, thread_name_prefix }
195    }
196
197    /// Returns a reference to the underlying rayon pool, creating it on first access.
198    fn pool(&self) -> &rayon::ThreadPool {
199        self.pool.get_or_init(|| {
200            let prefix = self.thread_name_prefix;
201            build_pool_with_panic_handler(
202                rayon::ThreadPoolBuilder::new()
203                    .num_threads(self.num_threads)
204                    .thread_name(move |i| format!("{prefix}-{i:02}")),
205            )
206            .unwrap_or_else(|err| panic!("failed to build {prefix} worker pool: {err}"))
207        })
208    }
209
210    /// Returns metrics for this worker pool.
211    fn metrics(&self) -> &WorkerPoolMetrics {
212        self.metrics.get_or_init(|| WorkerPoolMetrics::new(self.thread_name_prefix))
213    }
214
215    /// Returns `true` if the underlying rayon pool has been initialized.
216    pub fn is_initialized(&self) -> bool {
217        self.pool.get().is_some()
218    }
219
220    /// Returns the number of threads the pool is configured with, without creating it.
221    pub const fn num_threads(&self) -> usize {
222        self.num_threads
223    }
224
225    /// Returns the total number of threads in the underlying rayon pool.
226    pub fn current_num_threads(&self) -> usize {
227        self.pool().current_num_threads()
228    }
229
230    /// Runs a closure on `num_threads` threads in the pool, giving mutable access to each
231    /// thread's [`Worker`].
232    ///
233    /// The closure runs with exclusive access to each selected thread's worker.
234    /// Only `num_threads` threads execute the closure; the rest skip it.
235    pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
236        if num_threads >= self.pool().current_num_threads() {
237            // Fast path: run on every thread, no atomic coordination needed.
238            self.pool().broadcast(|_| {
239                WORKER.with_borrow_mut(|worker| f(worker));
240            });
241        } else {
242            let remaining = AtomicUsize::new(num_threads);
243            self.pool().broadcast(|_| {
244                // Atomically claim a slot; threads that can't decrement skip the closure.
245                let mut current = remaining.load(Ordering::Relaxed);
246                loop {
247                    if current == 0 {
248                        return;
249                    }
250                    match remaining.compare_exchange_weak(
251                        current,
252                        current - 1,
253                        Ordering::Relaxed,
254                        Ordering::Relaxed,
255                    ) {
256                        Ok(_) => break,
257                        Err(actual) => current = actual,
258                    }
259                }
260                WORKER.with_borrow_mut(|worker| f(worker));
261            });
262        }
263    }
264
265    /// Runs a closure on the pool with access to the calling thread's [`Worker`].
266    ///
267    /// All rayon parallelism (e.g. `par_iter`) spawned inside the closure executes on this pool.
268    /// Each thread can access its own [`Worker`] via the provided reference or through additional
269    /// [`WorkerPool::with_worker`] calls.
270    pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
271        let pool = self.pool();
272        let metrics = self.metrics().clone();
273        let queued_at = Instant::now();
274
275        pool.install(move || {
276            let started_at = Instant::now();
277            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
278            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
279            WORKER.with_borrow(|worker| f(worker))
280        })
281    }
282
283    /// Runs a closure on the pool without worker state access.
284    ///
285    /// Like [`install`](Self::install) but for closures that don't need per-thread [`Worker`]
286    /// state.
287    pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
288        let pool = self.pool();
289        let metrics = self.metrics().clone();
290        let queued_at = Instant::now();
291
292        pool.install(move || {
293            let started_at = Instant::now();
294            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
295            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
296            f()
297        })
298    }
299
300    /// Runs a closure on this pool, waiting for its result.
301    ///
302    /// Unlike [`install_fn`](Self::install_fn), this always queues the closure onto this pool
303    /// when called from another rayon pool. This avoids Rayon running the closure on the caller's
304    /// worker through its cross-pool install path.
305    pub fn spawn_and_wait<R: Send + 'static>(&self, f: impl FnOnce() -> R + Send + 'static) -> R {
306        let pool = self.pool();
307        if pool.current_thread_index().is_some() {
308            return f()
309        }
310
311        let (tx, rx) = std::sync::mpsc::sync_channel(1);
312        self.spawn(move || {
313            let _ = tx.send(catch_unwind(AssertUnwindSafe(f)));
314        });
315
316        match rx.recv().expect("worker pool exited before completing task") {
317            Ok(result) => result,
318            Err(panic) => std::panic::resume_unwind(panic),
319        }
320    }
321
322    /// Spawns a closure on the pool.
323    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
324        let pool = self.pool();
325        let metrics = self.metrics().clone();
326        let queued_at = Instant::now();
327
328        pool.spawn(move || {
329            let started_at = Instant::now();
330            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
331            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
332            f();
333        });
334    }
335
336    /// Executes `f` on this pool using [`rayon::in_place_scope`], which converts the calling
337    /// thread into a worker for the duration — tasks spawned inside the scope run on the pool
338    /// and the call blocks until all of them complete.
339    pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
340        self.pool().in_place_scope(f)
341    }
342
343    /// Accesses the current thread's [`Worker`] from within a pool closure.
344    ///
345    /// This is useful for accessing the worker from inside `par_iter` where the initial `&Worker`
346    /// reference from `install` belongs to a different thread.
347    ///
348    /// This borrows the thread-local worker for the entire duration of `f`. Do not yield to Rayon
349    /// from inside `f` if another job could call [`with_worker_mut`](Self::with_worker_mut) on the
350    /// same thread. Yield points include parallel iterators, `rayon::join`, scopes, and waiting in
351    /// `ThreadPool::install` on a different Rayon pool. A cross-pool `install` may run another job
352    /// from the caller's pool on the same thread while it waits.
353    ///
354    /// Prefer copying or cloning the required worker state in `f`, returning from this method to
355    /// release the borrow, and only then performing work that may yield.
356    ///
357    /// # Panics
358    ///
359    /// Panics if the current thread's worker is already mutably borrowed, including by a
360    /// re-entrant [`with_worker_mut`](Self::with_worker_mut) call.
361    pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
362        WORKER.with_borrow(|worker| f(worker))
363    }
364
365    /// Mutably accesses the current thread's [`Worker`] from within a pool closure.
366    ///
367    /// This exclusively borrows the thread-local worker for the entire duration of `f`. The borrow
368    /// is not re-entrant: if Rayon runs another job on the same thread before `f` returns, any call
369    /// to [`with_worker`](Self::with_worker) or `with_worker_mut` from that job will panic.
370    ///
371    /// Do not call operations that can yield to Rayon from inside `f` when re-entrant worker access
372    /// is possible. This includes parallel iterators, `rayon::join`, scopes, and waiting in
373    /// `ThreadPool::install` on a different Rayon pool. In particular, a cross-pool `install`
374    /// cooperatively runs jobs from the caller's pool while waiting for the target pool.
375    ///
376    /// Prefer computing updates before entering `with_worker_mut`, then use this closure only to
377    /// apply the update to the worker state.
378    ///
379    /// # Panics
380    ///
381    /// Panics if the current thread's worker is already borrowed, including by a re-entrant
382    /// `with_worker` or `with_worker_mut` call.
383    pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
384        WORKER.with_borrow_mut(|worker| f(worker))
385    }
386}
387
388/// Records a worker pool job's run time when the job finishes or unwinds.
389struct RecordWorkerPoolJobDurationOnDrop {
390    metrics: WorkerPoolMetrics,
391    started_at: Instant,
392}
393
394impl RecordWorkerPoolJobDurationOnDrop {
395    const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
396        Self { metrics, started_at }
397    }
398}
399
400impl Drop for RecordWorkerPoolJobDurationOnDrop {
401    fn drop(&mut self) {
402        self.metrics.record_job_duration(self.started_at.elapsed());
403    }
404}
405
406/// Builds a rayon thread pool with a panic handler that prevents aborting the process.
407///
408/// Rust's default panic hook already logs the panic message and backtrace to stderr, so the handler
409/// itself is intentionally a no-op.
410pub fn build_pool_with_panic_handler(
411    builder: rayon::ThreadPoolBuilder,
412) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
413    builder.panic_handler(|_| {}).build()
414}
415
416/// Per-thread state container for a [`WorkerPool`].
417///
418/// Holds a type-erased `Box<dyn Any>` that can be initialized and accessed with concrete types
419/// through [`WorkerStatefulPool`].
420#[derive(Debug, Default)]
421pub struct Worker {
422    state: Option<(Weak<WorkerStatefulPool>, Box<dyn Any>)>,
423}
424
425impl Worker {
426    /// Creates a new empty `Worker`.
427    const fn new() -> Self {
428        Self { state: None }
429    }
430
431    /// Initializes the worker state.
432    ///
433    /// If state of type `T` already exists for `stateful_pool`, passes `Some(&mut T)` to the
434    /// closure so resources can be reused. Otherwise, drops the previous state and passes
435    /// `None`.
436    fn init<T: 'static>(
437        &mut self,
438        stateful_pool: &Weak<WorkerStatefulPool>,
439        f: impl FnOnce(Option<&mut T>) -> T,
440    ) {
441        let existing = self.state.take().and_then(|(owner, boxed)| {
442            (Weak::ptr_eq(&owner, stateful_pool) && boxed.is::<T>()).then_some(boxed)
443        });
444
445        let new_state = match existing {
446            Some(mut boxed) => {
447                let r = boxed.downcast_mut::<T>().expect("type checked above");
448                *r = f(Some(r));
449                boxed
450            }
451            None => Box::new(f(None)),
452        };
453
454        self.state = Some((stateful_pool.clone(), new_state));
455    }
456
457    /// Returns a reference to the state for `stateful_pool`, downcasted to `T`.
458    ///
459    /// # Panics
460    ///
461    /// Panics if the worker has not been initialized or if the run or type does not match.
462    fn get<T: 'static>(&self, stateful_pool: &Weak<WorkerStatefulPool>) -> &T {
463        let (owner, state) = self.state.as_ref().expect("worker not initialized");
464        assert!(Weak::ptr_eq(owner, stateful_pool), "worker state run mismatch");
465        state.downcast_ref::<T>().expect("worker state type mismatch")
466    }
467
468    /// Returns a mutable reference to the state for `stateful_pool`, downcasted to `T`.
469    ///
470    /// # Panics
471    ///
472    /// Panics if the worker has not been initialized or if the run or type does not match.
473    fn get_mut<T: 'static>(&mut self, stateful_pool: &Weak<WorkerStatefulPool>) -> &mut T {
474        let (owner, state) = self.state.as_mut().expect("worker not initialized");
475        assert!(Weak::ptr_eq(owner, stateful_pool), "worker state run mismatch");
476        state.downcast_mut::<T>().expect("worker state type mismatch")
477    }
478
479    /// Returns mutable state for `stateful_pool`, creating it with `f` if absent.
480    ///
481    /// Reuses existing state without calling `f` when both its run and type match.
482    /// Otherwise, drops the previous state before calling `f` to create its replacement.
483    /// If `f` panics, the worker remains uninitialized.
484    fn get_or_init<T: 'static>(
485        &mut self,
486        stateful_pool: &Weak<WorkerStatefulPool>,
487        f: impl FnOnce() -> T,
488    ) -> &mut T {
489        if !self
490            .state
491            .as_ref()
492            .is_some_and(|(owner, state)| Weak::ptr_eq(owner, stateful_pool) && state.is::<T>())
493        {
494            self.state = None;
495            self.state = Some((stateful_pool.clone(), Box::new(f())));
496        }
497        self.get_mut(stateful_pool)
498    }
499
500    /// Drops the worker state only if it belongs to `stateful_pool`.
501    fn clear(&mut self, stateful_pool: &Weak<WorkerStatefulPool>) {
502        if self.state.as_ref().is_some_and(|(owner, _)| Weak::ptr_eq(owner, stateful_pool)) {
503            self.state = None;
504        }
505    }
506}
507
508/// Owns one run's thread-local state on a shared [`WorkerPool`].
509///
510/// Share this value with jobs through an [`Arc`]. Dropping the last handle clears this run's
511/// state on every pool thread, leaving other runs' state untouched. Worker slots retain only
512/// a weak reference to this owner and do not keep it alive. Dereferences to the underlying
513/// pool for scheduling jobs; each job accessing state must retain an owner handle.
514///
515/// Access closures borrow the current thread's worker and must not yield to Rayon or recursively
516/// access worker state. Use them only in jobs executing on this pool. Cleanup broadcasts to the
517/// pool and blocks until complete; the last handle must be dropped outside worker access closures.
518#[derive(Debug)]
519pub struct WorkerStatefulPool {
520    pool: Arc<WorkerPool>,
521    owner: Weak<Self>,
522}
523
524impl WorkerStatefulPool {
525    /// Creates a shared state owner without initializing the pool or its workers.
526    pub fn new(pool: Arc<WorkerPool>) -> Arc<Self> {
527        Arc::new_cyclic(|owner| Self { pool, owner: owner.clone() })
528    }
529
530    /// Initializes this run's state on every pool thread.
531    ///
532    /// Offers existing state to `f` only when its run and type match. Otherwise, drops the previous
533    /// state before calling `f` with `None`.
534    pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
535        self.pool.broadcast(self.pool.current_num_threads(), |worker| {
536            worker.init(&self.owner, &f);
537        });
538    }
539
540    /// Reads this run's state on the current worker thread.
541    ///
542    /// # Panics
543    ///
544    /// Panics if the worker is uninitialized, its run or type differs, or it is mutably borrowed.
545    pub fn get<T: 'static, R>(&self, f: impl FnOnce(&T) -> R) -> R {
546        WorkerPool::with_worker(|worker| f(worker.get(&self.owner)))
547    }
548
549    /// Mutably accesses this run's state, initializing it on the current worker if needed.
550    ///
551    /// Reuses state without calling `init` when both its run and type match. Otherwise, drops the
552    /// previous state before calling `init`. If `init` panics, the worker remains uninitialized.
553    ///
554    /// # Panics
555    ///
556    /// Panics if the current worker is already borrowed.
557    pub fn get_or_init<T: 'static, R>(
558        &self,
559        init: impl FnOnce() -> T,
560        f: impl FnOnce(&mut T) -> R,
561    ) -> R {
562        WorkerPool::with_worker_mut(|worker| f(worker.get_or_init(&self.owner, init)))
563    }
564}
565
566impl Deref for WorkerStatefulPool {
567    type Target = WorkerPool;
568
569    fn deref(&self) -> &Self::Target {
570        &self.pool
571    }
572}
573
574impl Drop for WorkerStatefulPool {
575    fn drop(&mut self) {
576        if let Some(pool) = self.pool.pool.get() {
577            pool.broadcast(|_| {
578                WORKER.with_borrow_mut(|worker| worker.clear(&self.owner));
579            });
580        }
581    }
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587    use rayon::prelude::*;
588    use std::time::Duration;
589
590    #[tokio::test]
591    async fn blocking_pool() {
592        let pool = BlockingTaskPool::build().unwrap();
593        let res = pool.spawn(move || 5);
594        let res = res.await.unwrap();
595        assert_eq!(res, 5);
596    }
597
598    #[tokio::test]
599    async fn blocking_pool_panic() {
600        let pool = BlockingTaskPool::build().unwrap();
601        let res = pool.spawn(move || -> i32 {
602            panic!();
603        });
604        let res = res.await;
605        assert!(res.is_err());
606    }
607
608    #[test]
609    fn worker_pool_init_and_access() {
610        let pool = Arc::new(WorkerPool::new(2, "test"));
611        let state = WorkerStatefulPool::new(Arc::clone(&pool));
612        state.init(|_| vec![1u8, 2, 3]);
613        let sum = pool.install_fn(|| state.get(|v: &Vec<u8>| v.iter().sum::<u8>()));
614        assert_eq!(sum, 6);
615        drop(state);
616        pool.broadcast(2, |worker| assert!(worker.state.is_none()));
617    }
618
619    #[test]
620    fn worker_pool_reinit_reuses_resources() {
621        let pool = Arc::new(WorkerPool::new(1, "test"));
622        let state = WorkerStatefulPool::new(Arc::clone(&pool));
623        state.init(|existing: Option<&mut Vec<u8>>| {
624            assert!(existing.is_none());
625            vec![1, 2, 3]
626        });
627        state.init(|existing: Option<&mut Vec<u8>>| {
628            let v = existing.expect("should have existing state");
629            assert_eq!(v, &mut vec![1, 2, 3]);
630            v.push(4);
631            std::mem::take(v)
632        });
633        let len = pool.install_fn(|| state.get(Vec::<u8>::len));
634        assert_eq!(len, 4);
635    }
636
637    #[test]
638    fn worker_pool_clear_and_reinit() {
639        let pool = Arc::new(WorkerPool::new(1, "test"));
640        let state = WorkerStatefulPool::new(Arc::clone(&pool));
641        state.init(|_| 42u64);
642        assert_eq!(pool.install_fn(|| state.get(|v: &u64| *v)), 42);
643        drop(state);
644        let state = WorkerStatefulPool::new(Arc::clone(&pool));
645        state.init(|_| String::from("hello"));
646        assert_eq!(pool.install_fn(|| state.get(String::clone)), "hello");
647    }
648
649    #[test]
650    fn worker_pool_par_iter_with_worker() {
651        let pool = Arc::new(WorkerPool::new(2, "test"));
652        let state = WorkerStatefulPool::new(Arc::clone(&pool));
653        state.init(|_| 10u64);
654        let results = pool.install_fn(|| {
655            (0u64..4).into_par_iter().map(|i| state.get(|v: &u64| i + *v)).collect::<Vec<_>>()
656        });
657        assert_eq!(results, vec![10, 11, 12, 13]);
658    }
659
660    #[test]
661    fn worker_pool_spawn_and_wait_runs_on_target_pool() {
662        let caller_pool = WorkerPool::new(1, "caller");
663        let target_pool = Arc::new(WorkerPool::new(1, "target"));
664
665        let thread_name = caller_pool.install_fn({
666            let target_pool = Arc::clone(&target_pool);
667            move || {
668                WorkerPool::with_worker_mut(|_| {
669                    target_pool.spawn_and_wait(|| thread::current().name().unwrap().to_owned())
670                })
671            }
672        });
673
674        assert_eq!(thread_name, "target-00");
675    }
676
677    #[test]
678    fn worker_get_or_init_reuses_only_matching_run_and_type() {
679        let mut worker = Worker::new();
680        let first = stateful_pool();
681        let second = stateful_pool();
682
683        worker.get_or_init(&first.owner, || vec![1u8]).push(2);
684        assert_eq!(
685            worker.get_or_init::<Vec<u8>>(&first.owner.clone(), || panic!("must reuse")),
686            &[1, 2]
687        );
688        assert_eq!(worker.get_or_init(&second.owner, || vec![3u8]), &[3]);
689        assert_eq!(worker.get_or_init(&second.owner, || String::from("new type")), "new type");
690        assert_eq!(*worker.get_or_init(&first.owner, || 42u64), 42);
691    }
692
693    #[test]
694    fn worker_init_reuses_only_matching_run_and_type() {
695        let mut worker = Worker::new();
696        let first = stateful_pool();
697        let second = stateful_pool();
698        worker.init(&first.owner, |_| 1u64);
699        worker.init(&second.owner, |existing: Option<&mut u64>| {
700            assert!(existing.is_none());
701            2
702        });
703        worker.init(&second.owner, |existing: Option<&mut String>| {
704            assert!(existing.is_none());
705            String::from("new type")
706        });
707        assert_eq!(worker.get::<String>(&second.owner), "new type");
708    }
709
710    #[test]
711    fn worker_pool_late_cleanup_preserves_other_runs() {
712        let pool = Arc::new(WorkerPool::new(2, "run-cleanup"));
713        let first = WorkerStatefulPool::new(Arc::clone(&pool));
714        let second = WorkerStatefulPool::new(Arc::clone(&pool));
715        first.init(|_| 1u64);
716        pool.broadcast(1, |worker| {
717            assert_eq!(*worker.get_or_init(&second.owner, || 2u64), 2);
718        });
719        drop(first);
720        let retained = AtomicUsize::new(0);
721        pool.broadcast(2, |worker| {
722            if let Some((owner, state)) = &worker.state {
723                assert!(Weak::ptr_eq(owner, &second.owner));
724                assert_eq!(state.downcast_ref::<u64>(), Some(&2));
725                retained.fetch_add(1, Ordering::Relaxed);
726            }
727        });
728        assert_eq!(retained.load(Ordering::Relaxed), 1);
729        drop(second);
730        pool.broadcast(2, |worker| assert!(worker.state.is_none()));
731    }
732
733    #[test]
734    #[should_panic(expected = "worker state run mismatch")]
735    fn worker_get_rejects_another_run() {
736        let mut worker = Worker::new();
737        worker.init(&stateful_pool().owner, |_| 1u64);
738        worker.get::<u64>(&stateful_pool().owner);
739    }
740
741    #[test]
742    #[should_panic(expected = "worker state run mismatch")]
743    fn worker_get_mut_rejects_another_run() {
744        let mut worker = Worker::new();
745        worker.init(&stateful_pool().owner, |_| 1u64);
746        worker.get_mut::<u64>(&stateful_pool().owner);
747    }
748
749    #[test]
750    fn worker_replacement_drops_previous_state_before_initialization() {
751        let mut worker = Worker::new();
752        let previous_run = stateful_pool();
753        let next_run = stateful_pool();
754        let resource = Arc::new(());
755
756        for owner in [&previous_run.owner, &next_run.owner] {
757            worker.init(&previous_run.owner, |_| Arc::clone(&resource));
758            assert_eq!(Arc::strong_count(&resource), 2);
759            assert_eq!(
760                *worker.get_or_init(owner, || {
761                    assert_eq!(Arc::strong_count(&resource), 1);
762                    42u64
763                }),
764                42,
765            );
766        }
767    }
768
769    #[test]
770    fn worker_failed_replacement_leaves_state_uninitialized() {
771        let mut worker = Worker::new();
772        let previous_run = stateful_pool();
773        let next_run = stateful_pool();
774        worker.init(&previous_run.owner, |_| 1u64);
775
776        let panic = catch_unwind(AssertUnwindSafe(|| {
777            worker.get_or_init::<u64>(&next_run.owner, || panic!("initialization failed"));
778        }))
779        .unwrap_err();
780        assert_eq!(panic.downcast_ref::<&str>(), Some(&"initialization failed"));
781        assert!(worker.state.is_none());
782        assert_eq!(*worker.get_or_init(&next_run.owner, || 2u64), 2);
783    }
784
785    #[test]
786    fn stateful_pool_drop_does_not_initialize_unused_pool() {
787        let pool = Arc::new(WorkerPool::new(1, "unused-state"));
788        drop(WorkerStatefulPool::new(Arc::clone(&pool)));
789        assert!(!pool.is_initialized());
790    }
791
792    #[test]
793    fn stateful_pool_last_job_handle_clears_state() {
794        let pool = Arc::new(WorkerPool::new(2, "owned-state"));
795        let state = WorkerStatefulPool::new(Arc::clone(&pool));
796        let owner = Arc::downgrade(&state);
797        let resource = Arc::new(());
798        state.init(|_| Arc::clone(&resource));
799        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
800        let (release_tx, release_rx) = std::sync::mpsc::channel();
801        let (done_tx, done_rx) = std::sync::mpsc::channel();
802        let job_state = Arc::clone(&state);
803        state.spawn(move || {
804            ready_tx.send(()).unwrap();
805            release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
806            job_state.get(|value: &Arc<()>| assert_eq!(Arc::strong_count(value), 3));
807            drop(job_state);
808            done_tx.send(()).unwrap();
809        });
810        ready_rx.recv_timeout(Duration::from_secs(5)).unwrap();
811        drop(state);
812        assert!(owner.upgrade().is_some());
813        assert_eq!(Arc::strong_count(&resource), 3);
814        release_tx.send(()).unwrap();
815        done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
816        assert!(owner.upgrade().is_none());
817        assert_eq!(Arc::strong_count(&resource), 1);
818        pool.broadcast(2, |worker| assert!(worker.state.is_none()));
819    }
820
821    #[test]
822    fn stateful_pool_clears_after_job_panics() {
823        let pool = Arc::new(WorkerPool::new(1, "unwind-state"));
824        let state = WorkerStatefulPool::new(Arc::clone(&pool));
825        let resource = Arc::new(());
826        state.init(|_| Arc::clone(&resource));
827        let panic = catch_unwind(AssertUnwindSafe(|| {
828            pool.install_fn(move || {
829                state.get_or_init(|| Arc::new(()), |_| panic!("job failed"));
830            });
831        }))
832        .unwrap_err();
833        assert_eq!(panic.downcast_ref::<&str>(), Some(&"job failed"));
834        assert_eq!(Arc::strong_count(&resource), 1);
835        pool.broadcast(1, |worker| assert!(worker.state.is_none()));
836    }
837
838    #[test]
839    fn stateful_pool_clears_after_unwrapping_owner() {
840        let pool = Arc::new(WorkerPool::new(1, "unwrapped-state"));
841        let state = WorkerStatefulPool::new(Arc::clone(&pool));
842        let resource = Arc::new(());
843        state.init(|_| Arc::clone(&resource));
844        let state = Arc::try_unwrap(state).unwrap();
845        assert_eq!(Arc::strong_count(&resource), 2);
846        drop(state);
847        assert_eq!(Arc::strong_count(&resource), 1);
848        pool.broadcast(1, |worker| assert!(worker.state.is_none()));
849    }
850
851    fn stateful_pool() -> Arc<WorkerStatefulPool> {
852        WorkerStatefulPool::new(Arc::new(WorkerPool::new(1, "state-test")))
853    }
854}