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