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    panic::{catch_unwind, AssertUnwindSafe},
9    pin::Pin,
10    sync::{
11        atomic::{AtomicUsize, Ordering},
12        Arc, OnceLock,
13    },
14    task::{ready, Context, Poll},
15    thread,
16    time::Instant,
17};
18use tokio::sync::{oneshot, AcquireError, OwnedSemaphorePermit, Semaphore};
19
20/// RPC Tracing call guard semaphore.
21///
22/// This is used to restrict the number of concurrent RPC requests to tracing methods like
23/// `debug_traceTransaction` as well as `eth_getProof` because they can consume a lot of
24/// memory and CPU.
25///
26/// This types serves as an entry guard for the [`BlockingTaskPool`] and is used to rate limit
27/// parallel blocking tasks in the pool.
28#[derive(Clone, Debug)]
29pub struct BlockingTaskGuard(Arc<Semaphore>);
30
31impl BlockingTaskGuard {
32    /// Create a new `BlockingTaskGuard` with the given maximum number of blocking tasks in
33    /// parallel.
34    pub fn new(max_blocking_tasks: usize) -> Self {
35        Self(Arc::new(Semaphore::new(max_blocking_tasks)))
36    }
37
38    /// See also [`Semaphore::acquire_owned`]
39    pub async fn acquire_owned(self) -> Result<OwnedSemaphorePermit, AcquireError> {
40        self.0.acquire_owned().await
41    }
42
43    /// See also [`Semaphore::acquire_many_owned`]
44    pub async fn acquire_many_owned(self, n: u32) -> Result<OwnedSemaphorePermit, AcquireError> {
45        self.0.acquire_many_owned(n).await
46    }
47}
48
49/// Used to execute blocking tasks on a rayon threadpool from within a tokio runtime.
50///
51/// This is a dedicated threadpool for blocking tasks which are CPU bound.
52/// RPC calls that perform blocking IO (disk lookups) are not executed on this pool but on the tokio
53/// runtime's blocking pool, which performs poorly with CPU bound tasks (see
54/// <https://ryhl.io/blog/async-what-is-blocking/>). Once the tokio blocking
55/// pool is saturated it is converted into a queue, blocking tasks could then interfere with the
56/// queue and block other RPC calls.
57///
58/// See also [tokio-docs] for more information.
59///
60/// [tokio-docs]: https://docs.rs/tokio/latest/tokio/index.html#cpu-bound-tasks-and-blocking-code
61#[derive(Clone, Debug)]
62pub struct BlockingTaskPool {
63    pool: Arc<rayon::ThreadPool>,
64}
65
66impl BlockingTaskPool {
67    /// Create a new `BlockingTaskPool` with the given threadpool.
68    pub fn new(pool: rayon::ThreadPool) -> Self {
69        Self { pool: Arc::new(pool) }
70    }
71
72    /// Convenience function to start building a new threadpool.
73    pub fn builder() -> rayon::ThreadPoolBuilder {
74        rayon::ThreadPoolBuilder::new()
75    }
76
77    /// Convenience function to build a new threadpool with the default configuration.
78    ///
79    /// Uses [`rayon::ThreadPoolBuilder::build`](rayon::ThreadPoolBuilder::build) defaults.
80    /// If a different stack size or other parameters are needed, they can be configured via
81    /// [`rayon::ThreadPoolBuilder`] returned by [`Self::builder`].
82    pub fn build() -> Result<Self, rayon::ThreadPoolBuildError> {
83        Self::builder().build().map(Self::new)
84    }
85
86    /// Asynchronous wrapper around Rayon's
87    /// [`ThreadPool::spawn`](rayon::ThreadPool::spawn).
88    ///
89    /// Runs a function on the configured threadpool, returning a future that resolves with the
90    /// function's return value.
91    ///
92    /// If the function panics, the future will resolve to an error.
93    pub fn spawn<F, R>(&self, func: F) -> BlockingTaskHandle<R>
94    where
95        F: FnOnce() -> R + Send + 'static,
96        R: Send + 'static,
97    {
98        let (tx, rx) = oneshot::channel();
99
100        self.pool.spawn(move || {
101            let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
102        });
103
104        BlockingTaskHandle { rx }
105    }
106
107    /// Asynchronous wrapper around Rayon's
108    /// [`ThreadPool::spawn_fifo`](rayon::ThreadPool::spawn_fifo).
109    ///
110    /// Runs a function on the configured threadpool, returning a future that resolves with the
111    /// function's return value.
112    ///
113    /// If the function panics, the future will resolve to an error.
114    pub fn spawn_fifo<F, R>(&self, func: F) -> BlockingTaskHandle<R>
115    where
116        F: FnOnce() -> R + Send + 'static,
117        R: Send + 'static,
118    {
119        let (tx, rx) = oneshot::channel();
120
121        self.pool.spawn_fifo(move || {
122            let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
123        });
124
125        BlockingTaskHandle { rx }
126    }
127}
128
129/// Async handle for a blocking task running in a Rayon thread pool.
130///
131/// ## Panics
132///
133/// If polled from outside a tokio runtime.
134#[derive(Debug)]
135#[must_use = "futures do nothing unless you `.await` or poll them"]
136#[pin_project::pin_project]
137pub struct BlockingTaskHandle<T> {
138    #[pin]
139    pub(crate) rx: oneshot::Receiver<thread::Result<T>>,
140}
141
142impl<T> Future for BlockingTaskHandle<T> {
143    type Output = thread::Result<T>;
144
145    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
146        match ready!(self.project().rx.poll(cx)) {
147            Ok(res) => Poll::Ready(res),
148            Err(_) => Poll::Ready(Err(Box::<TokioBlockingTaskError>::default())),
149        }
150    }
151}
152
153/// An error returned when the Tokio channel is dropped while awaiting a result.
154///
155/// This should only happen
156#[derive(Debug, Default, thiserror::Error)]
157#[error("tokio channel dropped while awaiting result")]
158#[non_exhaustive]
159pub struct TokioBlockingTaskError;
160
161thread_local! {
162    static WORKER: RefCell<Worker> = const { RefCell::new(Worker::new()) };
163}
164
165/// A rayon thread pool with per-thread [`Worker`] state.
166///
167/// Each thread in the pool has its own [`Worker`] that can hold arbitrary state via
168/// [`Worker::init`]. The state is thread-local and accessible during [`install`](Self::install)
169/// calls.
170///
171/// Worker access is backed by a thread-local [`RefCell`]. Keep [`with_worker`](Self::with_worker)
172/// and [`with_worker_mut`](Self::with_worker_mut) closures short and non-yielding: if Rayon runs
173/// another job on the same thread while a borrow is active, re-entrant worker access can panic.
174///
175/// The pool supports multiple init/clear cycles, allowing reuse of the same threads with
176/// different state configurations.
177///
178/// The underlying rayon pool is created lazily on first access.
179#[derive(Debug)]
180pub struct WorkerPool {
181    pool: OnceLock<rayon::ThreadPool>,
182    metrics: OnceLock<WorkerPoolMetrics>,
183    num_threads: usize,
184    thread_name_prefix: &'static str,
185}
186
187impl WorkerPool {
188    /// Creates a new lazy `WorkerPool` with the given number of threads and a thread name prefix.
189    ///
190    /// The underlying rayon pool is not created until the first method that requires it is called.
191    /// Thread names follow the pattern `"{prefix}-{index:02}"`.
192    pub const fn new(num_threads: usize, thread_name_prefix: &'static str) -> Self {
193        Self { pool: OnceLock::new(), metrics: OnceLock::new(), num_threads, thread_name_prefix }
194    }
195
196    /// Returns a reference to the underlying rayon pool, creating it on first access.
197    fn pool(&self) -> &rayon::ThreadPool {
198        self.pool.get_or_init(|| {
199            let prefix = self.thread_name_prefix;
200            build_pool_with_panic_handler(
201                rayon::ThreadPoolBuilder::new()
202                    .num_threads(self.num_threads)
203                    .thread_name(move |i| format!("{prefix}-{i:02}")),
204            )
205            .unwrap_or_else(|err| panic!("failed to build {prefix} worker pool: {err}"))
206        })
207    }
208
209    /// Returns metrics for this worker pool.
210    fn metrics(&self) -> &WorkerPoolMetrics {
211        self.metrics.get_or_init(|| WorkerPoolMetrics::new(self.thread_name_prefix))
212    }
213
214    /// Returns `true` if the underlying rayon pool has been initialized.
215    pub fn is_initialized(&self) -> bool {
216        self.pool.get().is_some()
217    }
218
219    /// Returns the number of threads the pool is configured with, without creating it.
220    pub const fn num_threads(&self) -> usize {
221        self.num_threads
222    }
223
224    /// Returns the total number of threads in the underlying rayon pool.
225    pub fn current_num_threads(&self) -> usize {
226        self.pool().current_num_threads()
227    }
228
229    /// Initializes per-thread [`Worker`] state on every thread in the pool.
230    pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
231        self.broadcast(self.pool().current_num_threads(), |worker| {
232            worker.init::<T>(&f);
233        });
234    }
235
236    /// Runs a closure on `num_threads` threads in the pool, giving mutable access to each
237    /// thread's [`Worker`].
238    ///
239    /// Use this to initialize or re-initialize per-thread state via [`Worker::init`].
240    /// Only `num_threads` threads execute the closure; the rest skip it.
241    pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
242        if num_threads >= self.pool().current_num_threads() {
243            // Fast path: run on every thread, no atomic coordination needed.
244            self.pool().broadcast(|_| {
245                WORKER.with_borrow_mut(|worker| f(worker));
246            });
247        } else {
248            let remaining = AtomicUsize::new(num_threads);
249            self.pool().broadcast(|_| {
250                // Atomically claim a slot; threads that can't decrement skip the closure.
251                let mut current = remaining.load(Ordering::Relaxed);
252                loop {
253                    if current == 0 {
254                        return;
255                    }
256                    match remaining.compare_exchange_weak(
257                        current,
258                        current - 1,
259                        Ordering::Relaxed,
260                        Ordering::Relaxed,
261                    ) {
262                        Ok(_) => break,
263                        Err(actual) => current = actual,
264                    }
265                }
266                WORKER.with_borrow_mut(|worker| f(worker));
267            });
268        }
269    }
270
271    /// Clears the state on every thread in the pool.
272    pub fn clear(&self) {
273        self.pool().broadcast(|_| {
274            WORKER.with_borrow_mut(Worker::clear);
275        });
276    }
277
278    /// Runs a closure on the pool with access to the calling thread's [`Worker`].
279    ///
280    /// All rayon parallelism (e.g. `par_iter`) spawned inside the closure executes on this pool.
281    /// Each thread can access its own [`Worker`] via the provided reference or through additional
282    /// [`WorkerPool::with_worker`] calls.
283    pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
284        let pool = self.pool();
285        let metrics = self.metrics().clone();
286        let queued_at = Instant::now();
287
288        pool.install(move || {
289            let started_at = Instant::now();
290            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
291            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
292            WORKER.with_borrow(|worker| f(worker))
293        })
294    }
295
296    /// Runs a closure on the pool without worker state access.
297    ///
298    /// Like [`install`](Self::install) but for closures that don't need per-thread [`Worker`]
299    /// state.
300    pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
301        let pool = self.pool();
302        let metrics = self.metrics().clone();
303        let queued_at = Instant::now();
304
305        pool.install(move || {
306            let started_at = Instant::now();
307            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
308            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
309            f()
310        })
311    }
312
313    /// Runs a closure on this pool, waiting for its result.
314    ///
315    /// Unlike [`install_fn`](Self::install_fn), this always queues the closure onto this pool
316    /// when called from another rayon pool. This avoids Rayon running the closure on the caller's
317    /// worker through its cross-pool install path.
318    pub fn spawn_and_wait<R: Send + 'static>(&self, f: impl FnOnce() -> R + Send + 'static) -> R {
319        let pool = self.pool();
320        if pool.current_thread_index().is_some() {
321            return f()
322        }
323
324        let (tx, rx) = std::sync::mpsc::sync_channel(1);
325        self.spawn(move || {
326            let _ = tx.send(catch_unwind(AssertUnwindSafe(f)));
327        });
328
329        match rx.recv().expect("worker pool exited before completing task") {
330            Ok(result) => result,
331            Err(panic) => std::panic::resume_unwind(panic),
332        }
333    }
334
335    /// Spawns a closure on the pool.
336    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
337        let pool = self.pool();
338        let metrics = self.metrics().clone();
339        let queued_at = Instant::now();
340
341        pool.spawn(move || {
342            let started_at = Instant::now();
343            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
344            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
345            f();
346        });
347    }
348
349    /// Executes `f` on this pool using [`rayon::in_place_scope`], which converts the calling
350    /// thread into a worker for the duration — tasks spawned inside the scope run on the pool
351    /// and the call blocks until all of them complete.
352    pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
353        self.pool().in_place_scope(f)
354    }
355
356    /// Accesses the current thread's [`Worker`] from within a pool closure.
357    ///
358    /// This is useful for accessing the worker from inside `par_iter` where the initial `&Worker`
359    /// reference from `install` belongs to a different thread.
360    ///
361    /// This borrows the thread-local worker for the entire duration of `f`. Do not yield to Rayon
362    /// from inside `f` if another job could call [`with_worker_mut`](Self::with_worker_mut) on the
363    /// same thread. Yield points include parallel iterators, `rayon::join`, scopes, and waiting in
364    /// `ThreadPool::install` on a different Rayon pool. A cross-pool `install` may run another job
365    /// from the caller's pool on the same thread while it waits.
366    ///
367    /// Prefer copying or cloning the required worker state in `f`, returning from this method to
368    /// release the borrow, and only then performing work that may yield.
369    ///
370    /// # Panics
371    ///
372    /// Panics if the current thread's worker is already mutably borrowed, including by a
373    /// re-entrant [`with_worker_mut`](Self::with_worker_mut) call.
374    pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
375        WORKER.with_borrow(|worker| f(worker))
376    }
377
378    /// Mutably accesses the current thread's [`Worker`] from within a pool closure.
379    ///
380    /// This exclusively borrows the thread-local worker for the entire duration of `f`. The borrow
381    /// is not re-entrant: if Rayon runs another job on the same thread before `f` returns, any call
382    /// to [`with_worker`](Self::with_worker) or `with_worker_mut` from that job will panic.
383    ///
384    /// Do not call operations that can yield to Rayon from inside `f` when re-entrant worker access
385    /// is possible. This includes parallel iterators, `rayon::join`, scopes, and waiting in
386    /// `ThreadPool::install` on a different Rayon pool. In particular, a cross-pool `install`
387    /// cooperatively runs jobs from the caller's pool while waiting for the target pool.
388    ///
389    /// Prefer computing updates before entering `with_worker_mut`, then use this closure only to
390    /// apply the update to the worker state.
391    ///
392    /// # Panics
393    ///
394    /// Panics if the current thread's worker is already borrowed, including by a re-entrant
395    /// `with_worker` or `with_worker_mut` call.
396    pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
397        WORKER.with_borrow_mut(|worker| f(worker))
398    }
399}
400
401/// Records a worker pool job's run time when the job finishes or unwinds.
402struct RecordWorkerPoolJobDurationOnDrop {
403    metrics: WorkerPoolMetrics,
404    started_at: Instant,
405}
406
407impl RecordWorkerPoolJobDurationOnDrop {
408    const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
409        Self { metrics, started_at }
410    }
411}
412
413impl Drop for RecordWorkerPoolJobDurationOnDrop {
414    fn drop(&mut self) {
415        self.metrics.record_job_duration(self.started_at.elapsed());
416    }
417}
418
419/// Builds a rayon thread pool with a panic handler that prevents aborting the process.
420///
421/// Rust's default panic hook already logs the panic message and backtrace to stderr, so the handler
422/// itself is intentionally a no-op.
423pub fn build_pool_with_panic_handler(
424    builder: rayon::ThreadPoolBuilder,
425) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
426    builder.panic_handler(|_| {}).build()
427}
428
429/// Per-thread state container for a [`WorkerPool`].
430///
431/// Holds a type-erased `Box<dyn Any>` that can be initialized and accessed with concrete types
432/// via [`init`](Self::init) and [`get`](Self::get).
433#[derive(Debug, Default)]
434pub struct Worker {
435    state: Option<Box<dyn Any>>,
436}
437
438impl Worker {
439    /// Creates a new empty `Worker`.
440    const fn new() -> Self {
441        Self { state: None }
442    }
443
444    /// Initializes the worker state.
445    ///
446    /// If state of type `T` already exists, passes `Some(&mut T)` to the closure so resources
447    /// can be reused. On first init, passes `None`.
448    pub fn init<T: 'static>(&mut self, f: impl FnOnce(Option<&mut T>) -> T) {
449        let existing =
450            self.state.take().and_then(|mut b| b.downcast_mut::<T>().is_some().then_some(b));
451
452        let new_state = match existing {
453            Some(mut boxed) => {
454                let r = boxed.downcast_mut::<T>().expect("type checked above");
455                *r = f(Some(r));
456                boxed
457            }
458            None => Box::new(f(None)),
459        };
460
461        self.state = Some(new_state);
462    }
463
464    /// Returns a reference to the state, downcasted to `T`.
465    ///
466    /// # Panics
467    ///
468    /// Panics if the worker has not been initialized or if the type does not match.
469    pub fn get<T: 'static>(&self) -> &T {
470        self.state
471            .as_ref()
472            .expect("worker not initialized")
473            .downcast_ref::<T>()
474            .expect("worker state type mismatch")
475    }
476
477    /// Returns a mutable reference to the state, downcasted to `T`.
478    ///
479    /// # Panics
480    ///
481    /// Panics if the worker has not been initialized or if the type does not match.
482    pub fn get_mut<T: 'static>(&mut self) -> &mut T {
483        self.state
484            .as_mut()
485            .expect("worker not initialized")
486            .downcast_mut::<T>()
487            .expect("worker state type mismatch")
488    }
489
490    /// Returns a mutable reference to the state, initializing it with `f` on first access.
491    ///
492    /// # Panics
493    ///
494    /// Panics if the state was previously initialized with a different type.
495    pub fn get_or_init<T: 'static>(&mut self, f: impl FnOnce() -> T) -> &mut T {
496        self.state
497            .get_or_insert_with(|| Box::new(f()))
498            .downcast_mut::<T>()
499            .expect("worker state type mismatch")
500    }
501
502    /// Clears the worker state, dropping the contained value.
503    pub fn clear(&mut self) {
504        self.state = None;
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[tokio::test]
513    async fn blocking_pool() {
514        let pool = BlockingTaskPool::build().unwrap();
515        let res = pool.spawn(move || 5);
516        let res = res.await.unwrap();
517        assert_eq!(res, 5);
518    }
519
520    #[tokio::test]
521    async fn blocking_pool_panic() {
522        let pool = BlockingTaskPool::build().unwrap();
523        let res = pool.spawn(move || -> i32 {
524            panic!();
525        });
526        let res = res.await;
527        assert!(res.is_err());
528    }
529
530    #[test]
531    fn worker_pool_init_and_access() {
532        let pool = WorkerPool::new(2, "test");
533
534        pool.broadcast(2, |worker| {
535            worker.init::<Vec<u8>>(|_| vec![1, 2, 3]);
536        });
537
538        let sum: u8 = pool.install(|worker| {
539            let v = worker.get::<Vec<u8>>();
540            v.iter().sum()
541        });
542        assert_eq!(sum, 6);
543
544        pool.clear();
545    }
546
547    #[test]
548    fn worker_pool_reinit_reuses_resources() {
549        let pool = WorkerPool::new(1, "test");
550
551        pool.broadcast(1, |worker| {
552            worker.init::<Vec<u8>>(|existing| {
553                assert!(existing.is_none());
554                vec![1, 2, 3]
555            });
556        });
557
558        pool.broadcast(1, |worker| {
559            worker.init::<Vec<u8>>(|existing| {
560                let v = existing.expect("should have existing state");
561                assert_eq!(v, &mut vec![1, 2, 3]);
562                v.push(4);
563                std::mem::take(v)
564            });
565        });
566
567        let len = pool.install(|worker| worker.get::<Vec<u8>>().len());
568        assert_eq!(len, 4);
569
570        pool.clear();
571    }
572
573    #[test]
574    fn worker_pool_clear_and_reinit() {
575        let pool = WorkerPool::new(1, "test");
576
577        pool.broadcast(1, |worker| {
578            worker.init::<u64>(|_| 42);
579        });
580        let val = pool.install(|worker| *worker.get::<u64>());
581        assert_eq!(val, 42);
582
583        pool.clear();
584
585        pool.broadcast(1, |worker| {
586            worker.init::<String>(|_| "hello".to_string());
587        });
588        let val = pool.install(|worker| worker.get::<String>().clone());
589        assert_eq!(val, "hello");
590
591        pool.clear();
592    }
593
594    #[test]
595    fn worker_pool_par_iter_with_worker() {
596        use rayon::prelude::*;
597
598        let pool = WorkerPool::new(2, "test");
599
600        pool.broadcast(2, |worker| {
601            worker.init::<u64>(|_| 10);
602        });
603
604        let results: Vec<u64> = pool.install(|_| {
605            (0u64..4)
606                .into_par_iter()
607                .map(|i| WorkerPool::with_worker(|w| i + *w.get::<u64>()))
608                .collect()
609        });
610        assert_eq!(results, vec![10, 11, 12, 13]);
611
612        pool.clear();
613    }
614
615    #[test]
616    fn worker_pool_spawn_and_wait_runs_on_target_pool() {
617        let caller_pool = WorkerPool::new(1, "caller");
618        let target_pool = Arc::new(WorkerPool::new(1, "target"));
619
620        let thread_name = caller_pool.install_fn({
621            let target_pool = Arc::clone(&target_pool);
622            move || {
623                WorkerPool::with_worker_mut(|_| {
624                    target_pool.spawn_and_wait(|| thread::current().name().unwrap().to_owned())
625                })
626            }
627        });
628
629        assert_eq!(thread_name, "target-00");
630    }
631}