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 total number of threads in the underlying rayon pool.
220    pub fn current_num_threads(&self) -> usize {
221        self.pool().current_num_threads()
222    }
223
224    /// Initializes per-thread [`Worker`] state on every thread in the pool.
225    pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
226        self.broadcast(self.pool().current_num_threads(), |worker| {
227            worker.init::<T>(&f);
228        });
229    }
230
231    /// Runs a closure on `num_threads` threads in the pool, giving mutable access to each
232    /// thread's [`Worker`].
233    ///
234    /// Use this to initialize or re-initialize per-thread state via [`Worker::init`].
235    /// Only `num_threads` threads execute the closure; the rest skip it.
236    pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
237        if num_threads >= self.pool().current_num_threads() {
238            // Fast path: run on every thread, no atomic coordination needed.
239            self.pool().broadcast(|_| {
240                WORKER.with_borrow_mut(|worker| f(worker));
241            });
242        } else {
243            let remaining = AtomicUsize::new(num_threads);
244            self.pool().broadcast(|_| {
245                // Atomically claim a slot; threads that can't decrement skip the closure.
246                let mut current = remaining.load(Ordering::Relaxed);
247                loop {
248                    if current == 0 {
249                        return;
250                    }
251                    match remaining.compare_exchange_weak(
252                        current,
253                        current - 1,
254                        Ordering::Relaxed,
255                        Ordering::Relaxed,
256                    ) {
257                        Ok(_) => break,
258                        Err(actual) => current = actual,
259                    }
260                }
261                WORKER.with_borrow_mut(|worker| f(worker));
262            });
263        }
264    }
265
266    /// Clears the state on every thread in the pool.
267    pub fn clear(&self) {
268        self.pool().broadcast(|_| {
269            WORKER.with_borrow_mut(Worker::clear);
270        });
271    }
272
273    /// Runs a closure on the pool with access to the calling thread's [`Worker`].
274    ///
275    /// All rayon parallelism (e.g. `par_iter`) spawned inside the closure executes on this pool.
276    /// Each thread can access its own [`Worker`] via the provided reference or through additional
277    /// [`WorkerPool::with_worker`] calls.
278    pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
279        let pool = self.pool();
280        let metrics = self.metrics().clone();
281        let queued_at = Instant::now();
282
283        pool.install(move || {
284            let started_at = Instant::now();
285            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
286            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
287            WORKER.with_borrow(|worker| f(worker))
288        })
289    }
290
291    /// Runs a closure on the pool without worker state access.
292    ///
293    /// Like [`install`](Self::install) but for closures that don't need per-thread [`Worker`]
294    /// state.
295    pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
296        let pool = self.pool();
297        let metrics = self.metrics().clone();
298        let queued_at = Instant::now();
299
300        pool.install(move || {
301            let started_at = Instant::now();
302            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
303            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
304            f()
305        })
306    }
307
308    /// Runs a closure on this pool, waiting for its result.
309    ///
310    /// Unlike [`install_fn`](Self::install_fn), this always queues the closure onto this pool
311    /// when called from another rayon pool. This avoids Rayon running the closure on the caller's
312    /// worker through its cross-pool install path.
313    pub fn spawn_and_wait<R: Send + 'static>(&self, f: impl FnOnce() -> R + Send + 'static) -> R {
314        let pool = self.pool();
315        if pool.current_thread_index().is_some() {
316            return f()
317        }
318
319        let (tx, rx) = std::sync::mpsc::sync_channel(1);
320        self.spawn(move || {
321            let _ = tx.send(catch_unwind(AssertUnwindSafe(f)));
322        });
323
324        match rx.recv().expect("worker pool exited before completing task") {
325            Ok(result) => result,
326            Err(panic) => std::panic::resume_unwind(panic),
327        }
328    }
329
330    /// Spawns a closure on the pool.
331    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
332        let pool = self.pool();
333        let metrics = self.metrics().clone();
334        let queued_at = Instant::now();
335
336        pool.spawn(move || {
337            let started_at = Instant::now();
338            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
339            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
340            f();
341        });
342    }
343
344    /// Executes `f` on this pool using [`rayon::in_place_scope`], which converts the calling
345    /// thread into a worker for the duration — tasks spawned inside the scope run on the pool
346    /// and the call blocks until all of them complete.
347    pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
348        self.pool().in_place_scope(f)
349    }
350
351    /// Accesses the current thread's [`Worker`] from within a pool closure.
352    ///
353    /// This is useful for accessing the worker from inside `par_iter` where the initial `&Worker`
354    /// reference from `install` belongs to a different thread.
355    ///
356    /// This borrows the thread-local worker for the entire duration of `f`. Do not yield to Rayon
357    /// from inside `f` if another job could call [`with_worker_mut`](Self::with_worker_mut) on the
358    /// same thread. Yield points include parallel iterators, `rayon::join`, scopes, and waiting in
359    /// `ThreadPool::install` on a different Rayon pool. A cross-pool `install` may run another job
360    /// from the caller's pool on the same thread while it waits.
361    ///
362    /// Prefer copying or cloning the required worker state in `f`, returning from this method to
363    /// release the borrow, and only then performing work that may yield.
364    ///
365    /// # Panics
366    ///
367    /// Panics if the current thread's worker is already mutably borrowed, including by a
368    /// re-entrant [`with_worker_mut`](Self::with_worker_mut) call.
369    pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
370        WORKER.with_borrow(|worker| f(worker))
371    }
372
373    /// Mutably accesses the current thread's [`Worker`] from within a pool closure.
374    ///
375    /// This exclusively borrows the thread-local worker for the entire duration of `f`. The borrow
376    /// is not re-entrant: if Rayon runs another job on the same thread before `f` returns, any call
377    /// to [`with_worker`](Self::with_worker) or `with_worker_mut` from that job will panic.
378    ///
379    /// Do not call operations that can yield to Rayon from inside `f` when re-entrant worker access
380    /// is possible. This includes parallel iterators, `rayon::join`, scopes, and waiting in
381    /// `ThreadPool::install` on a different Rayon pool. In particular, a cross-pool `install`
382    /// cooperatively runs jobs from the caller's pool while waiting for the target pool.
383    ///
384    /// Prefer computing updates before entering `with_worker_mut`, then use this closure only to
385    /// apply the update to the worker state.
386    ///
387    /// # Panics
388    ///
389    /// Panics if the current thread's worker is already borrowed, including by a re-entrant
390    /// `with_worker` or `with_worker_mut` call.
391    pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
392        WORKER.with_borrow_mut(|worker| f(worker))
393    }
394}
395
396/// Records a worker pool job's run time when the job finishes or unwinds.
397struct RecordWorkerPoolJobDurationOnDrop {
398    metrics: WorkerPoolMetrics,
399    started_at: Instant,
400}
401
402impl RecordWorkerPoolJobDurationOnDrop {
403    const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
404        Self { metrics, started_at }
405    }
406}
407
408impl Drop for RecordWorkerPoolJobDurationOnDrop {
409    fn drop(&mut self) {
410        self.metrics.record_job_duration(self.started_at.elapsed());
411    }
412}
413
414/// Builds a rayon thread pool with a panic handler that prevents aborting the process.
415///
416/// Rust's default panic hook already logs the panic message and backtrace to stderr, so the handler
417/// itself is intentionally a no-op.
418pub fn build_pool_with_panic_handler(
419    builder: rayon::ThreadPoolBuilder,
420) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
421    builder.panic_handler(|_| {}).build()
422}
423
424/// Per-thread state container for a [`WorkerPool`].
425///
426/// Holds a type-erased `Box<dyn Any>` that can be initialized and accessed with concrete types
427/// via [`init`](Self::init) and [`get`](Self::get).
428#[derive(Debug, Default)]
429pub struct Worker {
430    state: Option<Box<dyn Any>>,
431}
432
433impl Worker {
434    /// Creates a new empty `Worker`.
435    const fn new() -> Self {
436        Self { state: None }
437    }
438
439    /// Initializes the worker state.
440    ///
441    /// If state of type `T` already exists, passes `Some(&mut T)` to the closure so resources
442    /// can be reused. On first init, passes `None`.
443    pub fn init<T: 'static>(&mut self, f: impl FnOnce(Option<&mut T>) -> T) {
444        let existing =
445            self.state.take().and_then(|mut b| b.downcast_mut::<T>().is_some().then_some(b));
446
447        let new_state = match existing {
448            Some(mut boxed) => {
449                let r = boxed.downcast_mut::<T>().expect("type checked above");
450                *r = f(Some(r));
451                boxed
452            }
453            None => Box::new(f(None)),
454        };
455
456        self.state = Some(new_state);
457    }
458
459    /// Returns a reference to the state, downcasted to `T`.
460    ///
461    /// # Panics
462    ///
463    /// Panics if the worker has not been initialized or if the type does not match.
464    pub fn get<T: 'static>(&self) -> &T {
465        self.state
466            .as_ref()
467            .expect("worker not initialized")
468            .downcast_ref::<T>()
469            .expect("worker state type mismatch")
470    }
471
472    /// Returns a mutable reference to the state, downcasted to `T`.
473    ///
474    /// # Panics
475    ///
476    /// Panics if the worker has not been initialized or if the type does not match.
477    pub fn get_mut<T: 'static>(&mut self) -> &mut T {
478        self.state
479            .as_mut()
480            .expect("worker not initialized")
481            .downcast_mut::<T>()
482            .expect("worker state type mismatch")
483    }
484
485    /// Returns a mutable reference to the state, initializing it with `f` on first access.
486    ///
487    /// # Panics
488    ///
489    /// Panics if the state was previously initialized with a different type.
490    pub fn get_or_init<T: 'static>(&mut self, f: impl FnOnce() -> T) -> &mut T {
491        self.state
492            .get_or_insert_with(|| Box::new(f()))
493            .downcast_mut::<T>()
494            .expect("worker state type mismatch")
495    }
496
497    /// Clears the worker state, dropping the contained value.
498    pub fn clear(&mut self) {
499        self.state = None;
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[tokio::test]
508    async fn blocking_pool() {
509        let pool = BlockingTaskPool::build().unwrap();
510        let res = pool.spawn(move || 5);
511        let res = res.await.unwrap();
512        assert_eq!(res, 5);
513    }
514
515    #[tokio::test]
516    async fn blocking_pool_panic() {
517        let pool = BlockingTaskPool::build().unwrap();
518        let res = pool.spawn(move || -> i32 {
519            panic!();
520        });
521        let res = res.await;
522        assert!(res.is_err());
523    }
524
525    #[test]
526    fn worker_pool_init_and_access() {
527        let pool = WorkerPool::new(2, "test");
528
529        pool.broadcast(2, |worker| {
530            worker.init::<Vec<u8>>(|_| vec![1, 2, 3]);
531        });
532
533        let sum: u8 = pool.install(|worker| {
534            let v = worker.get::<Vec<u8>>();
535            v.iter().sum()
536        });
537        assert_eq!(sum, 6);
538
539        pool.clear();
540    }
541
542    #[test]
543    fn worker_pool_reinit_reuses_resources() {
544        let pool = WorkerPool::new(1, "test");
545
546        pool.broadcast(1, |worker| {
547            worker.init::<Vec<u8>>(|existing| {
548                assert!(existing.is_none());
549                vec![1, 2, 3]
550            });
551        });
552
553        pool.broadcast(1, |worker| {
554            worker.init::<Vec<u8>>(|existing| {
555                let v = existing.expect("should have existing state");
556                assert_eq!(v, &mut vec![1, 2, 3]);
557                v.push(4);
558                std::mem::take(v)
559            });
560        });
561
562        let len = pool.install(|worker| worker.get::<Vec<u8>>().len());
563        assert_eq!(len, 4);
564
565        pool.clear();
566    }
567
568    #[test]
569    fn worker_pool_clear_and_reinit() {
570        let pool = WorkerPool::new(1, "test");
571
572        pool.broadcast(1, |worker| {
573            worker.init::<u64>(|_| 42);
574        });
575        let val = pool.install(|worker| *worker.get::<u64>());
576        assert_eq!(val, 42);
577
578        pool.clear();
579
580        pool.broadcast(1, |worker| {
581            worker.init::<String>(|_| "hello".to_string());
582        });
583        let val = pool.install(|worker| worker.get::<String>().clone());
584        assert_eq!(val, "hello");
585
586        pool.clear();
587    }
588
589    #[test]
590    fn worker_pool_par_iter_with_worker() {
591        use rayon::prelude::*;
592
593        let pool = WorkerPool::new(2, "test");
594
595        pool.broadcast(2, |worker| {
596            worker.init::<u64>(|_| 10);
597        });
598
599        let results: Vec<u64> = pool.install(|_| {
600            (0u64..4)
601                .into_par_iter()
602                .map(|i| WorkerPool::with_worker(|w| i + *w.get::<u64>()))
603                .collect()
604        });
605        assert_eq!(results, vec![10, 11, 12, 13]);
606
607        pool.clear();
608    }
609
610    #[test]
611    fn worker_pool_spawn_and_wait_runs_on_target_pool() {
612        let caller_pool = WorkerPool::new(1, "caller");
613        let target_pool = Arc::new(WorkerPool::new(1, "target"));
614
615        let thread_name = caller_pool.install_fn({
616            let target_pool = Arc::clone(&target_pool);
617            move || {
618                WorkerPool::with_worker_mut(|_| {
619                    target_pool.spawn_and_wait(|| thread::current().name().unwrap().to_owned())
620                })
621            }
622        });
623
624        assert_eq!(thread_name, "target-00");
625    }
626}