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}