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/// The pool supports multiple init/clear cycles, allowing reuse of the same threads with
172/// different state configurations.
173///
174/// The underlying rayon pool is created lazily on first access.
175#[derive(Debug)]
176pub struct WorkerPool {
177    pool: OnceLock<rayon::ThreadPool>,
178    metrics: OnceLock<WorkerPoolMetrics>,
179    num_threads: usize,
180    thread_name_prefix: &'static str,
181}
182
183impl WorkerPool {
184    /// Creates a new lazy `WorkerPool` with the given number of threads and a thread name prefix.
185    ///
186    /// The underlying rayon pool is not created until the first method that requires it is called.
187    /// Thread names follow the pattern `"{prefix}-{index:02}"`.
188    pub const fn new(num_threads: usize, thread_name_prefix: &'static str) -> Self {
189        Self { pool: OnceLock::new(), metrics: OnceLock::new(), num_threads, thread_name_prefix }
190    }
191
192    /// Returns a reference to the underlying rayon pool, creating it on first access.
193    fn pool(&self) -> &rayon::ThreadPool {
194        self.pool.get_or_init(|| {
195            let prefix = self.thread_name_prefix;
196            build_pool_with_panic_handler(
197                rayon::ThreadPoolBuilder::new()
198                    .num_threads(self.num_threads)
199                    .thread_name(move |i| format!("{prefix}-{i:02}")),
200            )
201            .unwrap_or_else(|err| panic!("failed to build {prefix} worker pool: {err}"))
202        })
203    }
204
205    /// Returns metrics for this worker pool.
206    fn metrics(&self) -> &WorkerPoolMetrics {
207        self.metrics.get_or_init(|| WorkerPoolMetrics::new(self.thread_name_prefix))
208    }
209
210    /// Returns `true` if the underlying rayon pool has been initialized.
211    pub fn is_initialized(&self) -> bool {
212        self.pool.get().is_some()
213    }
214
215    /// Returns the total number of threads in the underlying rayon pool.
216    pub fn current_num_threads(&self) -> usize {
217        self.pool().current_num_threads()
218    }
219
220    /// Initializes per-thread [`Worker`] state on every thread in the pool.
221    pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
222        self.broadcast(self.pool().current_num_threads(), |worker| {
223            worker.init::<T>(&f);
224        });
225    }
226
227    /// Runs a closure on `num_threads` threads in the pool, giving mutable access to each
228    /// thread's [`Worker`].
229    ///
230    /// Use this to initialize or re-initialize per-thread state via [`Worker::init`].
231    /// Only `num_threads` threads execute the closure; the rest skip it.
232    pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
233        if num_threads >= self.pool().current_num_threads() {
234            // Fast path: run on every thread, no atomic coordination needed.
235            self.pool().broadcast(|_| {
236                WORKER.with_borrow_mut(|worker| f(worker));
237            });
238        } else {
239            let remaining = AtomicUsize::new(num_threads);
240            self.pool().broadcast(|_| {
241                // Atomically claim a slot; threads that can't decrement skip the closure.
242                let mut current = remaining.load(Ordering::Relaxed);
243                loop {
244                    if current == 0 {
245                        return;
246                    }
247                    match remaining.compare_exchange_weak(
248                        current,
249                        current - 1,
250                        Ordering::Relaxed,
251                        Ordering::Relaxed,
252                    ) {
253                        Ok(_) => break,
254                        Err(actual) => current = actual,
255                    }
256                }
257                WORKER.with_borrow_mut(|worker| f(worker));
258            });
259        }
260    }
261
262    /// Clears the state on every thread in the pool.
263    pub fn clear(&self) {
264        self.pool().broadcast(|_| {
265            WORKER.with_borrow_mut(Worker::clear);
266        });
267    }
268
269    /// Runs a closure on the pool with access to the calling thread's [`Worker`].
270    ///
271    /// All rayon parallelism (e.g. `par_iter`) spawned inside the closure executes on this pool.
272    /// Each thread can access its own [`Worker`] via the provided reference or through additional
273    /// [`WorkerPool::with_worker`] calls.
274    pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
275        let pool = self.pool();
276        let metrics = self.metrics().clone();
277        let queued_at = Instant::now();
278
279        pool.install(move || {
280            let started_at = Instant::now();
281            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
282            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
283            WORKER.with_borrow(|worker| f(worker))
284        })
285    }
286
287    /// Runs a closure on the pool without worker state access.
288    ///
289    /// Like [`install`](Self::install) but for closures that don't need per-thread [`Worker`]
290    /// state.
291    pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
292        let pool = self.pool();
293        let metrics = self.metrics().clone();
294        let queued_at = Instant::now();
295
296        pool.install(move || {
297            let started_at = Instant::now();
298            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
299            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
300            f()
301        })
302    }
303
304    /// Spawns a closure on the pool.
305    pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
306        let pool = self.pool();
307        let metrics = self.metrics().clone();
308        let queued_at = Instant::now();
309
310        pool.spawn(move || {
311            let started_at = Instant::now();
312            metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
313            let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
314            f();
315        });
316    }
317
318    /// Executes `f` on this pool using [`rayon::in_place_scope`], which converts the calling
319    /// thread into a worker for the duration — tasks spawned inside the scope run on the pool
320    /// and the call blocks until all of them complete.
321    pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
322        self.pool().in_place_scope(f)
323    }
324
325    /// Access the current thread's [`Worker`] from within an [`install`](Self::install) closure.
326    ///
327    /// This is useful for accessing the worker from inside `par_iter` where the initial `&Worker`
328    /// reference from `install` belongs to a different thread.
329    pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
330        WORKER.with_borrow(|worker| f(worker))
331    }
332
333    /// Mutably access the current thread's [`Worker`] from within a pool closure.
334    pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
335        WORKER.with_borrow_mut(|worker| f(worker))
336    }
337}
338
339/// Records a worker pool job's run time when the job finishes or unwinds.
340struct RecordWorkerPoolJobDurationOnDrop {
341    metrics: WorkerPoolMetrics,
342    started_at: Instant,
343}
344
345impl RecordWorkerPoolJobDurationOnDrop {
346    const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
347        Self { metrics, started_at }
348    }
349}
350
351impl Drop for RecordWorkerPoolJobDurationOnDrop {
352    fn drop(&mut self) {
353        self.metrics.record_job_duration(self.started_at.elapsed());
354    }
355}
356
357/// Builds a rayon thread pool with a panic handler that prevents aborting the process.
358///
359/// Rust's default panic hook already logs the panic message and backtrace to stderr, so the handler
360/// itself is intentionally a no-op.
361pub fn build_pool_with_panic_handler(
362    builder: rayon::ThreadPoolBuilder,
363) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
364    builder.panic_handler(|_| {}).build()
365}
366
367/// Per-thread state container for a [`WorkerPool`].
368///
369/// Holds a type-erased `Box<dyn Any>` that can be initialized and accessed with concrete types
370/// via [`init`](Self::init) and [`get`](Self::get).
371#[derive(Debug, Default)]
372pub struct Worker {
373    state: Option<Box<dyn Any>>,
374}
375
376impl Worker {
377    /// Creates a new empty `Worker`.
378    const fn new() -> Self {
379        Self { state: None }
380    }
381
382    /// Initializes the worker state.
383    ///
384    /// If state of type `T` already exists, passes `Some(&mut T)` to the closure so resources
385    /// can be reused. On first init, passes `None`.
386    pub fn init<T: 'static>(&mut self, f: impl FnOnce(Option<&mut T>) -> T) {
387        let existing =
388            self.state.take().and_then(|mut b| b.downcast_mut::<T>().is_some().then_some(b));
389
390        let new_state = match existing {
391            Some(mut boxed) => {
392                let r = boxed.downcast_mut::<T>().expect("type checked above");
393                *r = f(Some(r));
394                boxed
395            }
396            None => Box::new(f(None)),
397        };
398
399        self.state = Some(new_state);
400    }
401
402    /// Returns a reference to the state, downcasted to `T`.
403    ///
404    /// # Panics
405    ///
406    /// Panics if the worker has not been initialized or if the type does not match.
407    pub fn get<T: 'static>(&self) -> &T {
408        self.state
409            .as_ref()
410            .expect("worker not initialized")
411            .downcast_ref::<T>()
412            .expect("worker state type mismatch")
413    }
414
415    /// Returns a mutable reference to the state, downcasted to `T`.
416    ///
417    /// # Panics
418    ///
419    /// Panics if the worker has not been initialized or if the type does not match.
420    pub fn get_mut<T: 'static>(&mut self) -> &mut T {
421        self.state
422            .as_mut()
423            .expect("worker not initialized")
424            .downcast_mut::<T>()
425            .expect("worker state type mismatch")
426    }
427
428    /// Returns a mutable reference to the state, initializing it with `f` on first access.
429    ///
430    /// # Panics
431    ///
432    /// Panics if the state was previously initialized with a different type.
433    pub fn get_or_init<T: 'static>(&mut self, f: impl FnOnce() -> T) -> &mut T {
434        self.state
435            .get_or_insert_with(|| Box::new(f()))
436            .downcast_mut::<T>()
437            .expect("worker state type mismatch")
438    }
439
440    /// Clears the worker state, dropping the contained value.
441    pub fn clear(&mut self) {
442        self.state = None;
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[tokio::test]
451    async fn blocking_pool() {
452        let pool = BlockingTaskPool::build().unwrap();
453        let res = pool.spawn(move || 5);
454        let res = res.await.unwrap();
455        assert_eq!(res, 5);
456    }
457
458    #[tokio::test]
459    async fn blocking_pool_panic() {
460        let pool = BlockingTaskPool::build().unwrap();
461        let res = pool.spawn(move || -> i32 {
462            panic!();
463        });
464        let res = res.await;
465        assert!(res.is_err());
466    }
467
468    #[test]
469    fn worker_pool_init_and_access() {
470        let pool = WorkerPool::new(2, "test");
471
472        pool.broadcast(2, |worker| {
473            worker.init::<Vec<u8>>(|_| vec![1, 2, 3]);
474        });
475
476        let sum: u8 = pool.install(|worker| {
477            let v = worker.get::<Vec<u8>>();
478            v.iter().sum()
479        });
480        assert_eq!(sum, 6);
481
482        pool.clear();
483    }
484
485    #[test]
486    fn worker_pool_reinit_reuses_resources() {
487        let pool = WorkerPool::new(1, "test");
488
489        pool.broadcast(1, |worker| {
490            worker.init::<Vec<u8>>(|existing| {
491                assert!(existing.is_none());
492                vec![1, 2, 3]
493            });
494        });
495
496        pool.broadcast(1, |worker| {
497            worker.init::<Vec<u8>>(|existing| {
498                let v = existing.expect("should have existing state");
499                assert_eq!(v, &mut vec![1, 2, 3]);
500                v.push(4);
501                std::mem::take(v)
502            });
503        });
504
505        let len = pool.install(|worker| worker.get::<Vec<u8>>().len());
506        assert_eq!(len, 4);
507
508        pool.clear();
509    }
510
511    #[test]
512    fn worker_pool_clear_and_reinit() {
513        let pool = WorkerPool::new(1, "test");
514
515        pool.broadcast(1, |worker| {
516            worker.init::<u64>(|_| 42);
517        });
518        let val = pool.install(|worker| *worker.get::<u64>());
519        assert_eq!(val, 42);
520
521        pool.clear();
522
523        pool.broadcast(1, |worker| {
524            worker.init::<String>(|_| "hello".to_string());
525        });
526        let val = pool.install(|worker| worker.get::<String>().clone());
527        assert_eq!(val, "hello");
528
529        pool.clear();
530    }
531
532    #[test]
533    fn worker_pool_par_iter_with_worker() {
534        use rayon::prelude::*;
535
536        let pool = WorkerPool::new(2, "test");
537
538        pool.broadcast(2, |worker| {
539            worker.init::<u64>(|_| 10);
540        });
541
542        let results: Vec<u64> = pool.install(|_| {
543            (0u64..4)
544                .into_par_iter()
545                .map(|i| WorkerPool::with_worker(|w| i + *w.get::<u64>()))
546                .collect()
547        });
548        assert_eq!(results, vec![10, 11, 12, 13]);
549
550        pool.clear();
551    }
552}