Skip to main content

reth_tasks/
runtime.rs

1//! Centralized management of async and parallel execution.
2//!
3//! This module provides [`Runtime`], a cheaply cloneable handle that manages:
4//! - Tokio runtime (either owned or attached)
5//! - Task spawning with shutdown awareness and panic monitoring
6//! - Dedicated rayon thread pools for different workloads (with `rayon` feature)
7//! - [`BlockingTaskGuard`] for rate-limiting expensive operations (with `rayon` feature)
8
9#[cfg(feature = "rayon")]
10use crate::pool::{build_pool_with_panic_handler, BlockingTaskGuard, BlockingTaskPool, WorkerPool};
11use crate::{
12    metrics::{IncCounterOnDrop, TaskExecutorMetrics},
13    shutdown::{GracefulShutdown, GracefulShutdownGuard, Shutdown},
14    worker_map::WorkerMap,
15    PanickedTaskError, TaskEvent, TaskManager,
16};
17use futures_util::{future::select, Future, FutureExt, TryFutureExt};
18#[cfg(feature = "rayon")]
19use std::{num::NonZeroUsize, thread::available_parallelism};
20use std::{
21    pin::pin,
22    sync::{
23        atomic::{AtomicUsize, Ordering},
24        Arc, Mutex,
25    },
26    thread,
27    time::{Duration, Instant},
28};
29use tokio::{runtime::Handle, sync::mpsc::UnboundedSender, task::JoinHandle};
30use tracing::{debug, error};
31use tracing_futures::Instrument;
32
33use tokio::runtime::Runtime as TokioRuntime;
34
35/// Default thread keep-alive duration for the tokio runtime.
36pub const DEFAULT_THREAD_KEEP_ALIVE: Duration = Duration::from_secs(15);
37
38/// Default reserved CPU cores for OS and other processes.
39pub const DEFAULT_RESERVED_CPU_CORES: usize = 2;
40
41/// Default number of threads for the storage I/O pool.
42pub const DEFAULT_STORAGE_POOL_THREADS: usize = 16;
43
44/// Default number of threads for the state trie overlay worker pool.
45#[cfg(feature = "rayon")]
46pub const DEFAULT_STATE_TRIE_OVERLAY_WORKER_THREADS: usize = 4;
47
48/// Default maximum number of concurrent blocking tasks (for RPC tracing guard).
49pub const DEFAULT_MAX_BLOCKING_TASKS: usize = 512;
50
51/// Configuration for the tokio runtime.
52#[derive(Debug, Clone)]
53pub enum TokioConfig {
54    /// Build and own a new tokio runtime.
55    Owned {
56        /// Number of worker threads. If `None`, uses tokio's default (number of CPU cores).
57        worker_threads: Option<usize>,
58        /// How long to keep worker threads alive when idle.
59        thread_keep_alive: Duration,
60        /// Thread name prefix.
61        thread_name: &'static str,
62    },
63    /// Attach to an existing tokio runtime handle.
64    ExistingHandle(Handle),
65}
66
67impl Default for TokioConfig {
68    fn default() -> Self {
69        Self::Owned {
70            worker_threads: None,
71            thread_keep_alive: DEFAULT_THREAD_KEEP_ALIVE,
72            thread_name: "tokio-rt",
73        }
74    }
75}
76
77impl TokioConfig {
78    /// Create a config that attaches to an existing runtime handle.
79    pub const fn existing_handle(handle: Handle) -> Self {
80        Self::ExistingHandle(handle)
81    }
82
83    /// Create a config for an owned runtime with the specified number of worker threads.
84    pub const fn with_worker_threads(worker_threads: usize) -> Self {
85        Self::Owned {
86            worker_threads: Some(worker_threads),
87            thread_keep_alive: DEFAULT_THREAD_KEEP_ALIVE,
88            thread_name: "tokio-rt",
89        }
90    }
91}
92
93/// Configuration for the rayon thread pools.
94#[derive(Debug, Clone)]
95#[cfg(feature = "rayon")]
96pub struct RayonConfig {
97    /// Number of threads for the general CPU pool.
98    /// If `None`, derived from available parallelism minus reserved cores.
99    pub cpu_threads: Option<usize>,
100    /// Number of CPU cores to reserve for OS and other processes.
101    pub reserved_cpu_cores: usize,
102    /// Number of threads for the RPC blocking pool (trace calls, `eth_getProof`, etc.).
103    /// If `None`, uses the same as `cpu_threads`.
104    pub rpc_threads: Option<usize>,
105    /// Number of threads for the storage I/O pool (static file, `RocksDB` writes in
106    /// `save_blocks`). If `None`, uses [`DEFAULT_STORAGE_POOL_THREADS`].
107    pub storage_threads: Option<usize>,
108    /// Maximum number of concurrent blocking tasks for the RPC guard semaphore.
109    pub max_blocking_tasks: usize,
110    /// Number of threads for the proof storage worker pool (trie storage proof workers).
111    /// If `None`, derived from available parallelism.
112    pub proof_storage_worker_threads: Option<usize>,
113    /// Number of threads for the proof account worker pool (trie account proof workers).
114    /// If `None`, derived from available parallelism.
115    pub proof_account_worker_threads: Option<usize>,
116    /// Number of threads for the prewarming pool (execution prewarming workers).
117    /// If `None`, derived from available parallelism.
118    pub prewarming_threads: Option<usize>,
119    /// Number of threads for the BAL streaming pool (BAL hashed state streaming).
120    /// If `None`, derived from available parallelism.
121    pub bal_streaming_threads: Option<usize>,
122    /// Number of threads for the state trie overlay worker pool.
123    /// If `None`, uses [`DEFAULT_STATE_TRIE_OVERLAY_WORKER_THREADS`].
124    pub state_trie_overlay_worker_threads: Option<usize>,
125}
126
127#[cfg(feature = "rayon")]
128impl Default for RayonConfig {
129    fn default() -> Self {
130        Self {
131            cpu_threads: None,
132            reserved_cpu_cores: DEFAULT_RESERVED_CPU_CORES,
133            rpc_threads: None,
134            storage_threads: None,
135            max_blocking_tasks: DEFAULT_MAX_BLOCKING_TASKS,
136            proof_storage_worker_threads: None,
137            proof_account_worker_threads: None,
138            prewarming_threads: None,
139            bal_streaming_threads: None,
140            state_trie_overlay_worker_threads: None,
141        }
142    }
143}
144
145#[cfg(feature = "rayon")]
146impl RayonConfig {
147    /// Set the number of reserved CPU cores.
148    pub const fn with_reserved_cpu_cores(mut self, reserved_cpu_cores: usize) -> Self {
149        self.reserved_cpu_cores = reserved_cpu_cores;
150        self
151    }
152
153    /// Set the maximum number of concurrent blocking tasks.
154    pub const fn with_max_blocking_tasks(mut self, max_blocking_tasks: usize) -> Self {
155        self.max_blocking_tasks = max_blocking_tasks;
156        self
157    }
158
159    /// Set the number of threads for the RPC blocking pool.
160    pub const fn with_rpc_threads(mut self, rpc_threads: usize) -> Self {
161        self.rpc_threads = Some(rpc_threads);
162        self
163    }
164
165    /// Set the number of threads for the storage I/O pool.
166    pub const fn with_storage_threads(mut self, storage_threads: usize) -> Self {
167        self.storage_threads = Some(storage_threads);
168        self
169    }
170
171    /// Set the number of threads for the proof storage worker pool.
172    pub const fn with_proof_storage_worker_threads(
173        mut self,
174        proof_storage_worker_threads: usize,
175    ) -> Self {
176        self.proof_storage_worker_threads = Some(proof_storage_worker_threads);
177        self
178    }
179
180    /// Set the number of threads for the proof account worker pool.
181    pub const fn with_proof_account_worker_threads(
182        mut self,
183        proof_account_worker_threads: usize,
184    ) -> Self {
185        self.proof_account_worker_threads = Some(proof_account_worker_threads);
186        self
187    }
188
189    /// Set the number of threads for the prewarming pool.
190    pub const fn with_prewarming_threads(mut self, prewarming_threads: usize) -> Self {
191        self.prewarming_threads = Some(prewarming_threads);
192        self
193    }
194
195    /// Set the number of threads for the BAL streaming pool.
196    pub const fn with_bal_streaming_threads(mut self, bal_streaming_threads: usize) -> Self {
197        self.bal_streaming_threads = Some(bal_streaming_threads);
198        self
199    }
200
201    /// Set the number of threads for the state trie overlay worker pool.
202    pub const fn with_state_trie_overlay_worker_threads(
203        mut self,
204        state_trie_overlay_worker_threads: usize,
205    ) -> Self {
206        self.state_trie_overlay_worker_threads = Some(state_trie_overlay_worker_threads);
207        self
208    }
209
210    /// Compute the default number of threads based on available parallelism.
211    fn default_thread_count(&self) -> usize {
212        // TODO: reserved_cpu_cores is currently ignored because subtracting from thread pool
213        // sizes doesn't actually reserve CPU cores for other processes.
214        let _ = self.reserved_cpu_cores;
215        self.cpu_threads.unwrap_or_else(|| available_parallelism().map_or(1, NonZeroUsize::get))
216    }
217}
218
219/// Configuration for building a [`Runtime`].
220#[derive(Debug, Clone, Default)]
221pub struct RuntimeConfig {
222    /// Tokio runtime configuration.
223    pub tokio: TokioConfig,
224    /// Rayon thread pool configuration.
225    #[cfg(feature = "rayon")]
226    pub rayon: RayonConfig,
227}
228
229impl RuntimeConfig {
230    /// Set the tokio configuration.
231    pub fn with_tokio(mut self, tokio: TokioConfig) -> Self {
232        self.tokio = tokio;
233        self
234    }
235
236    /// Set the rayon configuration.
237    #[cfg(feature = "rayon")]
238    pub const fn with_rayon(mut self, rayon: RayonConfig) -> Self {
239        self.rayon = rayon;
240        self
241    }
242}
243
244/// Error returned when [`RuntimeBuilder::build`] fails.
245#[derive(Debug, thiserror::Error)]
246pub enum RuntimeBuildError {
247    /// Failed to build the tokio runtime.
248    #[error("Failed to build tokio runtime: {0}")]
249    TokioBuild(#[from] std::io::Error),
250    /// Failed to build a rayon thread pool.
251    #[cfg(feature = "rayon")]
252    #[error("Failed to build rayon thread pool: {0}")]
253    RayonBuild(#[from] rayon::ThreadPoolBuildError),
254}
255
256// ── RuntimeInner ──────────────────────────────────────────────────────
257
258struct RuntimeInner {
259    /// Owned tokio runtime, taken during shutdown even while other handles remain alive.
260    tokio_runtime: Mutex<Option<TokioRuntime>>,
261    /// Handle to the tokio runtime.
262    handle: Handle,
263    /// Receiver of the shutdown signal.
264    on_shutdown: Shutdown,
265    /// Sender half for sending task events to the [`TaskManager`].
266    task_events_tx: UnboundedSender<TaskEvent>,
267    /// Task executor metrics.
268    metrics: TaskExecutorMetrics,
269    /// How many [`GracefulShutdown`] tasks are currently active.
270    graceful_tasks: Arc<AtomicUsize>,
271    /// General-purpose rayon CPU pool.
272    #[cfg(feature = "rayon")]
273    cpu_pool: rayon::ThreadPool,
274    /// RPC blocking pool.
275    #[cfg(feature = "rayon")]
276    rpc_pool: BlockingTaskPool,
277    /// Storage I/O pool.
278    #[cfg(feature = "rayon")]
279    storage_pool: rayon::ThreadPool,
280    /// Rate limiter for expensive RPC operations.
281    #[cfg(feature = "rayon")]
282    blocking_guard: BlockingTaskGuard,
283    /// Proof storage worker pool (trie storage proof computation).
284    #[cfg(feature = "rayon")]
285    proof_storage_worker_pool: WorkerPool,
286    /// Proof account worker pool (trie account proof computation).
287    #[cfg(feature = "rayon")]
288    proof_account_worker_pool: WorkerPool,
289    /// Prewarming pool (execution prewarming workers).
290    #[cfg(feature = "rayon")]
291    prewarming_pool: WorkerPool,
292    /// BAL streaming pool (BAL hashed state streaming).
293    #[cfg(feature = "rayon")]
294    bal_streaming_pool: WorkerPool,
295    /// State trie overlay worker pool.
296    #[cfg(feature = "rayon")]
297    state_trie_overlay_worker_pool: Arc<WorkerPool>,
298    /// Named single-thread worker map. Each unique name gets a dedicated OS thread
299    /// that is reused across all tasks submitted under that name.
300    worker_map: WorkerMap,
301    /// Handle to the spawned [`TaskManager`] background task.
302    /// The task monitors critical tasks for panics and fires the shutdown signal.
303    /// Can be taken via [`Runtime::take_task_manager_handle`] to poll for panic errors.
304    task_manager_handle: Mutex<Option<JoinHandle<Result<(), PanickedTaskError>>>>,
305}
306
307impl Drop for RuntimeInner {
308    fn drop(&mut self) {
309        // The last handle can belong to an async task, including one running on this runtime.
310        // Without explicit shutdown, let blocking tasks finish in the background in that case.
311        if Handle::try_current().is_ok() &&
312            let Some(runtime) = self.tokio_runtime.get_mut().unwrap().take()
313        {
314            runtime.shutdown_background();
315        }
316    }
317}
318
319// ── Runtime ───────────────────────────────────────────────────────────
320
321/// A cheaply cloneable handle to the runtime resources.
322///
323/// Wraps an `Arc<RuntimeInner>` and provides access to:
324/// - The tokio [`Handle`]
325/// - Task spawning with shutdown awareness and panic monitoring
326/// - Rayon thread pools (with `rayon` feature)
327#[derive(Clone)]
328pub struct Runtime(Arc<RuntimeInner>);
329
330impl std::fmt::Debug for Runtime {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("Runtime").field("handle", &self.0.handle).finish()
333    }
334}
335
336// ── Pool accessors ────────────────────────────────────────────────────
337
338impl Runtime {
339    /// Takes the [`TaskManager`] handle out of this runtime, if one is stored.
340    ///
341    /// The handle resolves with `Err(PanickedTaskError)` if a critical task panicked,
342    /// or `Ok(())` if shutdown was requested. If not taken, the background task still
343    /// runs and logs panics at `debug!` level.
344    pub fn take_task_manager_handle(&self) -> Option<JoinHandle<Result<(), PanickedTaskError>>> {
345        self.0.task_manager_handle.lock().unwrap().take()
346    }
347
348    /// Returns the tokio runtime [`Handle`].
349    pub fn handle(&self) -> &Handle {
350        &self.0.handle
351    }
352
353    /// Get the general-purpose rayon CPU thread pool.
354    #[cfg(feature = "rayon")]
355    pub fn cpu_pool(&self) -> &rayon::ThreadPool {
356        &self.0.cpu_pool
357    }
358
359    /// Get the RPC blocking task pool.
360    #[cfg(feature = "rayon")]
361    pub fn rpc_pool(&self) -> &BlockingTaskPool {
362        &self.0.rpc_pool
363    }
364
365    /// Get the storage I/O pool.
366    #[cfg(feature = "rayon")]
367    pub fn storage_pool(&self) -> &rayon::ThreadPool {
368        &self.0.storage_pool
369    }
370
371    /// Get a clone of the [`BlockingTaskGuard`].
372    #[cfg(feature = "rayon")]
373    pub fn blocking_guard(&self) -> BlockingTaskGuard {
374        self.0.blocking_guard.clone()
375    }
376
377    /// Get the proof storage worker pool.
378    #[cfg(feature = "rayon")]
379    pub fn proof_storage_worker_pool(&self) -> &WorkerPool {
380        &self.0.proof_storage_worker_pool
381    }
382
383    /// Get the proof account worker pool.
384    #[cfg(feature = "rayon")]
385    pub fn proof_account_worker_pool(&self) -> &WorkerPool {
386        &self.0.proof_account_worker_pool
387    }
388
389    /// Get the prewarming pool.
390    #[cfg(feature = "rayon")]
391    pub fn prewarming_pool(&self) -> &WorkerPool {
392        &self.0.prewarming_pool
393    }
394
395    /// Get the BAL streaming pool.
396    #[cfg(feature = "rayon")]
397    pub fn bal_streaming_pool(&self) -> &WorkerPool {
398        &self.0.bal_streaming_pool
399    }
400
401    /// Get the state trie overlay worker pool.
402    #[cfg(feature = "rayon")]
403    pub fn state_trie_overlay_worker_pool(&self) -> Arc<WorkerPool> {
404        Arc::clone(&self.0.state_trie_overlay_worker_pool)
405    }
406}
407
408// ── Test helpers ──────────────────────────────────────────────────────
409
410impl Runtime {
411    /// Creates a lightweight [`Runtime`] for tests with minimal thread pools.
412    ///
413    /// If called from within a tokio runtime (e.g. `#[tokio::test]`), attaches to the existing
414    /// handle to avoid shutdown panics when the test runtime is dropped.
415    pub fn test() -> Self {
416        let config = match Handle::try_current() {
417            Ok(handle) => Self::test_config().with_tokio(TokioConfig::existing_handle(handle)),
418            Err(_) => Self::test_config(),
419        };
420        RuntimeBuilder::new(config).build().expect("failed to build test Runtime")
421    }
422
423    const fn test_config() -> RuntimeConfig {
424        RuntimeConfig {
425            tokio: TokioConfig::Owned {
426                worker_threads: Some(2),
427                thread_keep_alive: DEFAULT_THREAD_KEEP_ALIVE,
428                thread_name: "tokio-test",
429            },
430            #[cfg(feature = "rayon")]
431            rayon: RayonConfig {
432                cpu_threads: Some(2),
433                reserved_cpu_cores: 0,
434                rpc_threads: Some(2),
435                storage_threads: Some(2),
436                max_blocking_tasks: 16,
437                proof_storage_worker_threads: Some(2),
438                proof_account_worker_threads: Some(2),
439                prewarming_threads: Some(2),
440                bal_streaming_threads: Some(2),
441                state_trie_overlay_worker_threads: Some(2),
442            },
443        }
444    }
445}
446
447// ── Spawn methods ─────────────────────────────────────────────────────
448
449/// Determines how a task is spawned.
450enum TaskKind {
451    /// Spawn the task to the default executor [`Handle::spawn`].
452    Default,
453    /// Spawn the task to the blocking executor [`Handle::spawn_blocking`].
454    Blocking,
455}
456
457impl Runtime {
458    /// Returns the receiver of the shutdown signal.
459    pub fn on_shutdown_signal(&self) -> &Shutdown {
460        &self.0.on_shutdown
461    }
462
463    /// Spawns a future on the tokio runtime depending on the [`TaskKind`].
464    fn spawn_on_rt<F>(&self, fut: F, task_kind: TaskKind) -> JoinHandle<()>
465    where
466        F: Future<Output = ()> + Send + 'static,
467    {
468        match task_kind {
469            TaskKind::Default => self.0.handle.spawn(fut),
470            TaskKind::Blocking => {
471                let handle = self.0.handle.clone();
472                self.0.handle.spawn_blocking(move || handle.block_on(fut))
473            }
474        }
475    }
476
477    /// Spawns a regular task depending on the given [`TaskKind`].
478    fn spawn_task_as<F>(&self, fut: F, task_kind: TaskKind) -> JoinHandle<()>
479    where
480        F: Future<Output = ()> + Send + 'static,
481    {
482        match task_kind {
483            TaskKind::Default => self.0.metrics.inc_regular_tasks(),
484            TaskKind::Blocking => self.0.metrics.inc_regular_blocking_tasks(),
485        }
486        let on_shutdown = self.0.on_shutdown.clone();
487
488        let finished_counter = match task_kind {
489            TaskKind::Default => self.0.metrics.finished_regular_tasks_total.clone(),
490            TaskKind::Blocking => self.0.metrics.finished_regular_blocking_tasks_total.clone(),
491        };
492
493        let task = {
494            async move {
495                let _inc_counter_on_drop = IncCounterOnDrop::new(finished_counter);
496                let fut = pin!(fut);
497                let _ = select(on_shutdown, fut).await;
498            }
499        }
500        .in_current_span();
501
502        self.spawn_on_rt(task, task_kind)
503    }
504
505    /// Spawns the task onto the runtime.
506    /// The given future resolves as soon as the [Shutdown] signal is received.
507    ///
508    /// See also [`Handle::spawn`].
509    pub fn spawn_task<F>(&self, fut: F) -> JoinHandle<()>
510    where
511        F: Future<Output = ()> + Send + 'static,
512    {
513        self.spawn_task_as(fut, TaskKind::Default)
514    }
515
516    /// Spawns a blocking task onto the runtime.
517    /// The given future resolves as soon as the [Shutdown] signal is received.
518    ///
519    /// See also [`Handle::spawn_blocking`].
520    pub fn spawn_blocking_task<F>(&self, fut: F) -> JoinHandle<()>
521    where
522        F: Future<Output = ()> + Send + 'static,
523    {
524        self.spawn_task_as(fut, TaskKind::Blocking)
525    }
526
527    /// Spawns a blocking closure directly on the tokio runtime, bypassing shutdown
528    /// awareness. Useful for raw CPU-bound work.
529    pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
530    where
531        F: FnOnce() -> R + Send + 'static,
532        R: Send + 'static,
533    {
534        self.0.handle.spawn_blocking(func)
535    }
536
537    /// Moves the given value to a dedicated background thread for deallocation.
538    ///
539    /// This is useful when dropping a value is expensive (e.g. large nested collections)
540    /// and should not block the current task. Uses a persistent named thread (`"drop"`)
541    /// to avoid thread creation overhead on hot paths.
542    pub fn spawn_drop<T: Send + 'static>(&self, value: T) {
543        self.spawn_blocking_named("drop", move || drop(value));
544    }
545
546    /// Spawns a blocking closure on a dedicated, named OS thread.
547    ///
548    /// Unlike [`spawn_blocking`](Self::spawn_blocking) which uses tokio's blocking thread pool,
549    /// this reuses the same OS thread for all tasks submitted under the same `name`. The thread
550    /// is created lazily on first use and its OS thread name is set to `name`.
551    ///
552    /// This is useful for tasks that benefit from running on a stable thread, e.g. for
553    /// thread-local state reuse or to avoid thread creation overhead on hot paths.
554    ///
555    /// Returns a [`LazyHandle`](crate::LazyHandle) handle that resolves on first access and caches
556    /// the result.
557    pub fn spawn_blocking_named<F, R>(&self, name: &'static str, func: F) -> crate::LazyHandle<R>
558    where
559        F: FnOnce() -> R + Send + 'static,
560        R: Send + 'static,
561    {
562        crate::LazyHandle::new(self.0.worker_map.spawn_on(name, func))
563    }
564
565    /// Attempts to spawn a blocking closure on a dedicated, named OS thread.
566    ///
567    /// Returns `None` if the named worker already has a task running or queued, allowing the caller
568    /// to fall back to another executor instead of serializing behind the named worker.
569    pub fn try_spawn_blocking_named<F, R>(
570        &self,
571        name: &'static str,
572        func: F,
573    ) -> Option<crate::LazyHandle<R>>
574    where
575        F: FnOnce() -> R + Send + 'static,
576        R: Send + 'static,
577    {
578        self.0.worker_map.try_spawn_on(name, func).map(crate::LazyHandle::new)
579    }
580
581    /// Spawns a blocking closure on a named OS thread if it is idle, otherwise falls back to
582    /// tokio's unnamed blocking thread pool.
583    ///
584    /// Returns `true` if the closure was spawned on the named thread.
585    pub fn spawn_blocking_named_or_tokio<F>(&self, name: &'static str, func: F) -> bool
586    where
587        F: FnOnce() + Send + 'static,
588    {
589        let func = Arc::new(parking_lot::Mutex::new(Some(func)));
590        let named_func = func.clone();
591
592        if self
593            .try_spawn_blocking_named(name, move || {
594                if let Some(func) = named_func.lock().take() {
595                    func();
596                }
597            })
598            .is_some()
599        {
600            return true
601        }
602
603        if let Some(func) = func.lock().take() {
604            self.spawn_blocking(func);
605        }
606        false
607    }
608
609    /// Spawns the task onto the runtime.
610    /// The given future resolves as soon as the [Shutdown] signal is received.
611    ///
612    /// See also [`Handle::spawn`].
613    pub fn spawn_with_signal<F>(&self, f: impl FnOnce(Shutdown) -> F) -> JoinHandle<()>
614    where
615        F: Future<Output = ()> + Send + 'static,
616    {
617        let on_shutdown = self.0.on_shutdown.clone();
618        let fut = f(on_shutdown);
619        let task = fut.in_current_span();
620        self.0.handle.spawn(task)
621    }
622
623    /// Spawns a critical task depending on the given [`TaskKind`].
624    fn spawn_critical_as<F>(
625        &self,
626        name: &'static str,
627        fut: F,
628        task_kind: TaskKind,
629    ) -> JoinHandle<()>
630    where
631        F: Future<Output = ()> + Send + 'static,
632    {
633        self.0.metrics.inc_critical_tasks();
634        let panicked_tasks_tx = self.0.task_events_tx.clone();
635        let on_shutdown = self.0.on_shutdown.clone();
636
637        // wrap the task in catch unwind
638        let task = std::panic::AssertUnwindSafe(fut)
639            .catch_unwind()
640            .map_err(move |error| {
641                let task_error = PanickedTaskError::new(name, error);
642                error!("{task_error}");
643                let _ = panicked_tasks_tx.send(TaskEvent::Panic(task_error));
644            })
645            .in_current_span();
646
647        let finished_critical_tasks_total_metrics =
648            self.0.metrics.finished_critical_tasks_total.clone();
649        let task = async move {
650            let _inc_counter_on_drop = IncCounterOnDrop::new(finished_critical_tasks_total_metrics);
651            let task = pin!(task);
652            let _ = select(on_shutdown, task).await;
653        };
654
655        self.spawn_on_rt(task, task_kind)
656    }
657
658    /// This spawns a critical task onto the runtime.
659    /// The given future resolves as soon as the [Shutdown] signal is received.
660    ///
661    /// If this task panics, the [`TaskManager`] is notified.
662    pub fn spawn_critical_task<F>(&self, name: &'static str, fut: F) -> JoinHandle<()>
663    where
664        F: Future<Output = ()> + Send + 'static,
665    {
666        self.spawn_critical_as(name, fut, TaskKind::Default)
667    }
668
669    /// This spawns a critical blocking task onto the runtime.
670    /// The given future resolves as soon as the [Shutdown] signal is received.
671    ///
672    /// If this task panics, the [`TaskManager`] is notified.
673    pub fn spawn_critical_blocking_task<F>(&self, name: &'static str, fut: F) -> JoinHandle<()>
674    where
675        F: Future<Output = ()> + Send + 'static,
676    {
677        self.spawn_critical_as(name, fut, TaskKind::Blocking)
678    }
679
680    /// This spawns a critical task onto a dedicated named OS thread.
681    /// The given future resolves as soon as the [`Shutdown`] signal is received.
682    ///
683    /// If this task panics, the [`TaskManager`] is notified.
684    pub fn spawn_critical_os_thread<F>(
685        &self,
686        thread_name: &'static str,
687        task_name: &'static str,
688        fut: F,
689    ) -> thread::JoinHandle<()>
690    where
691        F: Future<Output = ()> + Send + 'static,
692    {
693        self.0.metrics.inc_critical_tasks();
694        let handle = self.0.handle.clone();
695        let panicked_tasks_tx = self.0.task_events_tx.clone();
696        let on_shutdown = self.0.on_shutdown.clone();
697
698        let task = std::panic::AssertUnwindSafe(fut)
699            .catch_unwind()
700            .map_err(move |error| {
701                let task_error = PanickedTaskError::new(task_name, error);
702                error!("{task_error}");
703                let _ = panicked_tasks_tx.send(TaskEvent::Panic(task_error));
704            })
705            .in_current_span();
706
707        let finished_critical_tasks_total_metrics =
708            self.0.metrics.finished_critical_tasks_total.clone();
709        let task = async move {
710            let _inc_counter_on_drop = IncCounterOnDrop::new(finished_critical_tasks_total_metrics);
711            let task = pin!(task);
712            let _ = select(on_shutdown, task).await;
713        };
714
715        thread::Builder::new()
716            .name(thread_name.to_string())
717            .spawn(move || {
718                let _guard = handle.enter();
719                handle.block_on(task);
720            })
721            .unwrap_or_else(|e| panic!("failed to spawn critical OS thread {thread_name:?}: {e}"))
722    }
723
724    /// This spawns a critical task onto the runtime.
725    ///
726    /// If this task panics, the [`TaskManager`] is notified.
727    /// The [`TaskManager`] will wait until the given future has completed before shutting down.
728    ///
729    /// # Example
730    ///
731    /// ```no_run
732    /// # async fn t(executor: reth_tasks::TaskExecutor) {
733    ///
734    /// executor.spawn_critical_with_graceful_shutdown_signal("grace", async move |shutdown| {
735    ///     // await the shutdown signal
736    ///     let guard = shutdown.await;
737    ///     // do work before exiting the program
738    ///     tokio::time::sleep(std::time::Duration::from_secs(1)).await;
739    ///     // allow graceful shutdown
740    ///     drop(guard);
741    /// });
742    /// # }
743    /// ```
744    pub fn spawn_critical_with_graceful_shutdown_signal<F>(
745        &self,
746        name: &'static str,
747        f: impl FnOnce(GracefulShutdown) -> F,
748    ) -> JoinHandle<()>
749    where
750        F: Future<Output = ()> + Send + 'static,
751    {
752        let panicked_tasks_tx = self.0.task_events_tx.clone();
753        let on_shutdown = GracefulShutdown::new(
754            self.0.on_shutdown.clone(),
755            GracefulShutdownGuard::new(Arc::clone(&self.0.graceful_tasks)),
756        );
757        let fut = f(on_shutdown);
758
759        // wrap the task in catch unwind
760        let task = std::panic::AssertUnwindSafe(fut)
761            .catch_unwind()
762            .map_err(move |error| {
763                let task_error = PanickedTaskError::new(name, error);
764                error!("{task_error}");
765                let _ = panicked_tasks_tx.send(TaskEvent::Panic(task_error));
766            })
767            .map(drop)
768            .in_current_span();
769
770        self.0.handle.spawn(task)
771    }
772
773    /// This spawns a regular task onto the runtime.
774    ///
775    /// The [`TaskManager`] will wait until the given future has completed before shutting down.
776    ///
777    /// # Example
778    ///
779    /// ```no_run
780    /// # async fn t(executor: reth_tasks::TaskExecutor) {
781    ///
782    /// executor.spawn_with_graceful_shutdown_signal(async move |shutdown| {
783    ///     // await the shutdown signal
784    ///     let guard = shutdown.await;
785    ///     // do work before exiting the program
786    ///     tokio::time::sleep(std::time::Duration::from_secs(1)).await;
787    ///     // allow graceful shutdown
788    ///     drop(guard);
789    /// });
790    /// # }
791    /// ```
792    pub fn spawn_with_graceful_shutdown_signal<F>(
793        &self,
794        f: impl FnOnce(GracefulShutdown) -> F,
795    ) -> JoinHandle<()>
796    where
797        F: Future<Output = ()> + Send + 'static,
798    {
799        let on_shutdown = GracefulShutdown::new(
800            self.0.on_shutdown.clone(),
801            GracefulShutdownGuard::new(Arc::clone(&self.0.graceful_tasks)),
802        );
803        let fut = f(on_shutdown);
804
805        self.0.handle.spawn(fut)
806    }
807
808    /// Sends a request to the `TaskManager` to initiate a graceful shutdown.
809    ///
810    /// Caution: This will terminate the entire program.
811    pub fn initiate_graceful_shutdown(
812        &self,
813    ) -> Result<GracefulShutdown, tokio::sync::mpsc::error::SendError<()>> {
814        self.0
815            .task_events_tx
816            .send(TaskEvent::GracefulShutdown)
817            .map_err(|_send_error_with_task_event| tokio::sync::mpsc::error::SendError(()))?;
818
819        Ok(GracefulShutdown::new(
820            self.0.on_shutdown.clone(),
821            GracefulShutdownGuard::new(Arc::clone(&self.0.graceful_tasks)),
822        ))
823    }
824
825    /// Fires the shutdown signal and waits until all graceful tasks complete.
826    pub fn graceful_shutdown(&self) {
827        let _ = self.do_graceful_shutdown(None);
828    }
829
830    /// Fires the shutdown signal and waits until all graceful tasks complete or the timeout
831    /// elapses.
832    ///
833    /// Returns `true` if all tasks completed before the timeout.
834    pub fn graceful_shutdown_with_timeout(&self, timeout: Duration) -> bool {
835        self.do_graceful_shutdown(Some(timeout))
836    }
837
838    /// Shuts down the owned tokio runtime for all clones, waiting at most `timeout` for blocking
839    /// tasks. Externally supplied runtimes are left running.
840    ///
841    /// Panics if called from an async context while an owned runtime remains.
842    pub fn shutdown_timeout(self, timeout: Duration) {
843        let runtime = self.0.tokio_runtime.lock().unwrap().take();
844        if let Some(runtime) = runtime {
845            runtime.shutdown_timeout(timeout);
846        }
847    }
848
849    fn do_graceful_shutdown(&self, timeout: Option<Duration>) -> bool {
850        let _ = self.0.task_events_tx.send(TaskEvent::GracefulShutdown);
851        let deadline = timeout.map(|t| Instant::now() + t);
852        while self.0.graceful_tasks.load(Ordering::SeqCst) > 0 {
853            if deadline.is_some_and(|d| Instant::now() > d) {
854                debug!("graceful shutdown timed out");
855                return false;
856            }
857            std::thread::yield_now();
858        }
859        debug!("gracefully shut down");
860        true
861    }
862}
863
864// ── RuntimeBuilder ────────────────────────────────────────────────────
865
866/// Builder for constructing a [`Runtime`].
867#[derive(Debug, Clone)]
868pub struct RuntimeBuilder {
869    config: RuntimeConfig,
870}
871
872impl RuntimeBuilder {
873    /// Create a new builder with the given configuration.
874    pub const fn new(config: RuntimeConfig) -> Self {
875        Self { config }
876    }
877
878    /// Build the [`Runtime`].
879    ///
880    /// The [`TaskManager`] is automatically spawned as a background task that monitors
881    /// critical tasks for panics. Use [`Runtime::take_task_manager_handle`] to extract
882    /// the join handle if you need to poll for panic errors.
883    #[tracing::instrument(name = "RuntimeBuilder::build", level = "debug", skip_all)]
884    pub fn build(self) -> Result<Runtime, RuntimeBuildError> {
885        debug!(?self.config, "Building runtime");
886        let config = self.config;
887
888        let (owned_runtime, handle) = match &config.tokio {
889            TokioConfig::Owned { worker_threads, thread_keep_alive, thread_name } => {
890                let mut builder = tokio::runtime::Builder::new_multi_thread();
891                builder
892                    .enable_all()
893                    .thread_keep_alive(*thread_keep_alive)
894                    .thread_name(*thread_name);
895
896                if let Some(threads) = worker_threads {
897                    builder.worker_threads(*threads);
898                }
899
900                let runtime = builder.build()?;
901                let h = runtime.handle().clone();
902                (Some(runtime), h)
903            }
904            TokioConfig::ExistingHandle(h) => (None, h.clone()),
905        };
906
907        let (task_manager, on_shutdown, task_events_tx, graceful_tasks) =
908            TaskManager::new_parts(handle.clone());
909
910        #[cfg(feature = "rayon")]
911        let (
912            cpu_pool,
913            rpc_pool,
914            storage_pool,
915            blocking_guard,
916            proof_storage_worker_pool,
917            proof_account_worker_pool,
918            prewarming_pool,
919            bal_streaming_pool,
920            state_trie_overlay_worker_pool,
921        ) = {
922            let default_threads = config.rayon.default_thread_count();
923            let rpc_threads = config.rayon.rpc_threads.unwrap_or(default_threads);
924
925            let cpu_pool = build_pool_with_panic_handler(
926                rayon::ThreadPoolBuilder::new()
927                    .num_threads(default_threads)
928                    .thread_name(|i| format!("cpu-{i:02}")),
929            )?;
930
931            let rpc_raw = build_pool_with_panic_handler(
932                rayon::ThreadPoolBuilder::new()
933                    .num_threads(rpc_threads)
934                    .thread_name(|i| format!("rpc-{i:02}")),
935            )?;
936            let rpc_pool = BlockingTaskPool::new(rpc_raw);
937
938            let storage_threads =
939                config.rayon.storage_threads.unwrap_or(DEFAULT_STORAGE_POOL_THREADS);
940            let storage_pool = build_pool_with_panic_handler(
941                rayon::ThreadPoolBuilder::new()
942                    .num_threads(storage_threads)
943                    .thread_name(|i| format!("storage-{i:02}")),
944            )?;
945
946            let blocking_guard = BlockingTaskGuard::new(config.rayon.max_blocking_tasks);
947
948            let proof_storage_worker_threads =
949                config.rayon.proof_storage_worker_threads.unwrap_or(default_threads * 2);
950            let proof_storage_worker_pool =
951                WorkerPool::new(proof_storage_worker_threads, "proof-strg");
952
953            let proof_account_worker_threads =
954                config.rayon.proof_account_worker_threads.unwrap_or(default_threads * 2);
955            let proof_account_worker_pool =
956                WorkerPool::new(proof_account_worker_threads, "proof-acct");
957
958            let prewarming_threads = config.rayon.prewarming_threads.unwrap_or(default_threads);
959            let prewarming_pool = WorkerPool::new(prewarming_threads, "prewarm");
960
961            let bal_streaming_threads =
962                config.rayon.bal_streaming_threads.unwrap_or(default_threads);
963            let bal_streaming_pool = WorkerPool::new(bal_streaming_threads, "bal-stream");
964
965            let state_trie_overlay_worker_threads = config
966                .rayon
967                .state_trie_overlay_worker_threads
968                .unwrap_or(DEFAULT_STATE_TRIE_OVERLAY_WORKER_THREADS);
969            let state_trie_overlay_worker_pool =
970                Arc::new(WorkerPool::new(state_trie_overlay_worker_threads, "state-ovly"));
971
972            debug!(
973                default_threads,
974                rpc_threads,
975                storage_threads,
976                proof_storage_worker_threads,
977                proof_account_worker_threads,
978                prewarming_threads,
979                bal_streaming_threads,
980                state_trie_overlay_worker_threads,
981                max_blocking_tasks = config.rayon.max_blocking_tasks,
982                "Configured lazy rayon worker pools"
983            );
984
985            (
986                cpu_pool,
987                rpc_pool,
988                storage_pool,
989                blocking_guard,
990                proof_storage_worker_pool,
991                proof_account_worker_pool,
992                prewarming_pool,
993                bal_streaming_pool,
994                state_trie_overlay_worker_pool,
995            )
996        };
997
998        let task_manager_handle = handle.spawn(async move {
999            let result = task_manager.await;
1000            if let Err(ref err) = result {
1001                debug!("{err}");
1002            }
1003            result
1004        });
1005
1006        let inner = RuntimeInner {
1007            tokio_runtime: Mutex::new(owned_runtime),
1008            handle,
1009            on_shutdown,
1010            task_events_tx,
1011            metrics: Default::default(),
1012            graceful_tasks,
1013            #[cfg(feature = "rayon")]
1014            cpu_pool,
1015            #[cfg(feature = "rayon")]
1016            rpc_pool,
1017            #[cfg(feature = "rayon")]
1018            storage_pool,
1019            #[cfg(feature = "rayon")]
1020            blocking_guard,
1021            #[cfg(feature = "rayon")]
1022            proof_storage_worker_pool,
1023            #[cfg(feature = "rayon")]
1024            proof_account_worker_pool,
1025            #[cfg(feature = "rayon")]
1026            prewarming_pool,
1027            #[cfg(feature = "rayon")]
1028            bal_streaming_pool,
1029            #[cfg(feature = "rayon")]
1030            state_trie_overlay_worker_pool,
1031            worker_map: WorkerMap::new(),
1032            task_manager_handle: Mutex::new(Some(task_manager_handle)),
1033        };
1034
1035        Ok(Runtime(Arc::new(inner)))
1036    }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn test_runtime_config_default() {
1045        let config = RuntimeConfig::default();
1046        assert!(matches!(config.tokio, TokioConfig::Owned { .. }));
1047    }
1048
1049    #[test]
1050    fn test_runtime_config_existing_handle() {
1051        let rt = TokioRuntime::new().unwrap();
1052        let config =
1053            Runtime::test_config().with_tokio(TokioConfig::existing_handle(rt.handle().clone()));
1054        assert!(matches!(config.tokio, TokioConfig::ExistingHandle(_)));
1055    }
1056
1057    #[cfg(feature = "rayon")]
1058    #[test]
1059    fn test_rayon_config_thread_count() {
1060        let config = RayonConfig::default();
1061        let count = config.default_thread_count();
1062        assert!(count >= 1);
1063    }
1064
1065    #[test]
1066    fn test_runtime_builder() {
1067        let rt = TokioRuntime::new().unwrap();
1068        let config =
1069            Runtime::test_config().with_tokio(TokioConfig::existing_handle(rt.handle().clone()));
1070        let runtime = RuntimeBuilder::new(config).build().unwrap();
1071        let _ = runtime.handle();
1072    }
1073
1074    #[tokio::test]
1075    async fn last_runtime_clone_can_drop_on_its_worker() {
1076        let runtime = RuntimeBuilder::new(Runtime::test_config()).build().unwrap();
1077        let task_runtime = runtime.clone();
1078        let (release, wait) = tokio::sync::oneshot::channel();
1079        let task = runtime.handle().spawn(async move {
1080            wait.await.unwrap();
1081            drop(task_runtime);
1082        });
1083
1084        thread::spawn(move || drop(runtime)).join().unwrap();
1085        release.send(()).unwrap();
1086        task.await.unwrap();
1087    }
1088
1089    #[tokio::test]
1090    async fn shutdown_with_live_runtime_clones() {
1091        let runtime = RuntimeBuilder::new(Runtime::test_config()).build().unwrap();
1092        let retained = runtime.clone();
1093        let task_runtime = runtime.clone();
1094        let (started, ready) = tokio::sync::oneshot::channel();
1095        let task = runtime.handle().spawn(async move {
1096            let _runtime = task_runtime;
1097            started.send(()).unwrap();
1098            std::future::pending::<()>().await;
1099        });
1100        ready.await.unwrap();
1101
1102        thread::spawn(move || runtime.shutdown_timeout(Duration::from_secs(5))).join().unwrap();
1103
1104        assert!(task.await.unwrap_err().is_cancelled());
1105        assert!(retained.handle().spawn(async {}).await.unwrap_err().is_cancelled());
1106        retained.shutdown_timeout(Duration::from_secs(5));
1107    }
1108
1109    #[tokio::test]
1110    async fn shutdown_preserves_external_runtime() {
1111        let runtime = RuntimeBuilder::new(
1112            Runtime::test_config().with_tokio(TokioConfig::existing_handle(Handle::current())),
1113        )
1114        .build()
1115        .unwrap();
1116        let retained = runtime.clone();
1117
1118        runtime.shutdown_timeout(Duration::from_secs(5));
1119
1120        assert_eq!(retained.handle().spawn(async { 42 }).await.unwrap(), 42);
1121    }
1122
1123    #[test]
1124    fn critical_os_thread_uses_requested_name() {
1125        let runtime = Runtime::test();
1126        let (tx, rx) = std::sync::mpsc::channel();
1127
1128        let handle = runtime.spawn_critical_os_thread(
1129            "critical-os-test",
1130            "critical os thread test",
1131            async move {
1132                let name = thread::current().name().unwrap().to_string();
1133                tx.send(name).unwrap();
1134            },
1135        );
1136
1137        let name = rx.recv_timeout(Duration::from_secs(5)).unwrap();
1138        assert_eq!(name, "critical-os-test");
1139        handle.join().unwrap();
1140    }
1141
1142    #[test]
1143    fn critical_os_thread_panic_is_reported() {
1144        let runtime = Runtime::test();
1145        let manager_handle = runtime.take_task_manager_handle().unwrap();
1146
1147        let handle = runtime.spawn_critical_os_thread(
1148            "critical-os-panic",
1149            "critical os thread panic test",
1150            async { panic!("critical os thread panic") },
1151        );
1152
1153        let err =
1154            runtime.handle().block_on(async move { manager_handle.await.unwrap().unwrap_err() });
1155        assert_eq!(err.task_name, "critical os thread panic test");
1156        assert_eq!(err.error, Some("critical os thread panic".to_string()));
1157        handle.join().unwrap();
1158    }
1159
1160    #[cfg(feature = "rayon")]
1161    #[test]
1162    fn test_worker_pools_are_lazy() {
1163        let runtime = Runtime::test();
1164
1165        // Worker pools are lazy — not initialized until first access.
1166        assert!(!runtime.0.bal_streaming_pool.is_initialized());
1167        assert!(!runtime.0.proof_storage_worker_pool.is_initialized());
1168        assert!(!runtime.0.state_trie_overlay_worker_pool.is_initialized());
1169
1170        // Accessing them triggers initialization and returns the configured thread count.
1171        assert_eq!(runtime.bal_streaming_pool().current_num_threads(), 2);
1172        assert!(runtime.0.bal_streaming_pool.is_initialized());
1173
1174        assert_eq!(runtime.proof_storage_worker_pool().current_num_threads(), 2);
1175        assert_eq!(runtime.proof_account_worker_pool().current_num_threads(), 2);
1176        assert_eq!(runtime.prewarming_pool().current_num_threads(), 2);
1177        assert_eq!(runtime.state_trie_overlay_worker_pool().current_num_threads(), 2);
1178    }
1179}