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}