1#[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
35pub const DEFAULT_THREAD_KEEP_ALIVE: Duration = Duration::from_secs(15);
37
38pub const DEFAULT_RESERVED_CPU_CORES: usize = 2;
40
41pub const DEFAULT_STORAGE_POOL_THREADS: usize = 16;
43
44#[cfg(feature = "rayon")]
46pub const DEFAULT_STATE_TRIE_OVERLAY_WORKER_THREADS: usize = 4;
47
48pub const DEFAULT_MAX_BLOCKING_TASKS: usize = 512;
50
51#[derive(Debug, Clone)]
53pub enum TokioConfig {
54 Owned {
56 worker_threads: Option<usize>,
58 thread_keep_alive: Duration,
60 thread_name: &'static str,
62 },
63 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 pub const fn existing_handle(handle: Handle) -> Self {
80 Self::ExistingHandle(handle)
81 }
82
83 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#[derive(Debug, Clone)]
95#[cfg(feature = "rayon")]
96pub struct RayonConfig {
97 pub cpu_threads: Option<usize>,
100 pub reserved_cpu_cores: usize,
102 pub rpc_threads: Option<usize>,
105 pub storage_threads: Option<usize>,
108 pub max_blocking_tasks: usize,
110 pub proof_storage_worker_threads: Option<usize>,
113 pub proof_account_worker_threads: Option<usize>,
116 pub prewarming_threads: Option<usize>,
119 pub bal_streaming_threads: Option<usize>,
122 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 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 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 pub const fn with_rpc_threads(mut self, rpc_threads: usize) -> Self {
161 self.rpc_threads = Some(rpc_threads);
162 self
163 }
164
165 pub const fn with_storage_threads(mut self, storage_threads: usize) -> Self {
167 self.storage_threads = Some(storage_threads);
168 self
169 }
170
171 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 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 pub const fn with_prewarming_threads(mut self, prewarming_threads: usize) -> Self {
191 self.prewarming_threads = Some(prewarming_threads);
192 self
193 }
194
195 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 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 fn default_thread_count(&self) -> usize {
212 let _ = self.reserved_cpu_cores;
215 self.cpu_threads.unwrap_or_else(|| available_parallelism().map_or(1, NonZeroUsize::get))
216 }
217}
218
219#[derive(Debug, Clone, Default)]
221pub struct RuntimeConfig {
222 pub tokio: TokioConfig,
224 #[cfg(feature = "rayon")]
226 pub rayon: RayonConfig,
227}
228
229impl RuntimeConfig {
230 pub fn with_tokio(mut self, tokio: TokioConfig) -> Self {
232 self.tokio = tokio;
233 self
234 }
235
236 #[cfg(feature = "rayon")]
238 pub const fn with_rayon(mut self, rayon: RayonConfig) -> Self {
239 self.rayon = rayon;
240 self
241 }
242}
243
244#[derive(Debug, thiserror::Error)]
246pub enum RuntimeBuildError {
247 #[error("Failed to build tokio runtime: {0}")]
249 TokioBuild(#[from] std::io::Error),
250 #[cfg(feature = "rayon")]
252 #[error("Failed to build rayon thread pool: {0}")]
253 RayonBuild(#[from] rayon::ThreadPoolBuildError),
254}
255
256struct RuntimeInner {
259 tokio_runtime: Mutex<Option<TokioRuntime>>,
261 handle: Handle,
263 on_shutdown: Shutdown,
265 task_events_tx: UnboundedSender<TaskEvent>,
267 metrics: TaskExecutorMetrics,
269 graceful_tasks: Arc<AtomicUsize>,
271 #[cfg(feature = "rayon")]
273 cpu_pool: rayon::ThreadPool,
274 #[cfg(feature = "rayon")]
276 rpc_pool: BlockingTaskPool,
277 #[cfg(feature = "rayon")]
279 storage_pool: rayon::ThreadPool,
280 #[cfg(feature = "rayon")]
282 blocking_guard: BlockingTaskGuard,
283 #[cfg(feature = "rayon")]
285 proof_storage_worker_pool: WorkerPool,
286 #[cfg(feature = "rayon")]
288 proof_account_worker_pool: WorkerPool,
289 #[cfg(feature = "rayon")]
291 prewarming_pool: WorkerPool,
292 #[cfg(feature = "rayon")]
294 bal_streaming_pool: WorkerPool,
295 #[cfg(feature = "rayon")]
297 state_trie_overlay_worker_pool: Arc<WorkerPool>,
298 worker_map: WorkerMap,
301 task_manager_handle: Mutex<Option<JoinHandle<Result<(), PanickedTaskError>>>>,
305}
306
307impl Drop for RuntimeInner {
308 fn drop(&mut self) {
309 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#[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
336impl Runtime {
339 pub fn take_task_manager_handle(&self) -> Option<JoinHandle<Result<(), PanickedTaskError>>> {
345 self.0.task_manager_handle.lock().unwrap().take()
346 }
347
348 pub fn handle(&self) -> &Handle {
350 &self.0.handle
351 }
352
353 #[cfg(feature = "rayon")]
355 pub fn cpu_pool(&self) -> &rayon::ThreadPool {
356 &self.0.cpu_pool
357 }
358
359 #[cfg(feature = "rayon")]
361 pub fn rpc_pool(&self) -> &BlockingTaskPool {
362 &self.0.rpc_pool
363 }
364
365 #[cfg(feature = "rayon")]
367 pub fn storage_pool(&self) -> &rayon::ThreadPool {
368 &self.0.storage_pool
369 }
370
371 #[cfg(feature = "rayon")]
373 pub fn blocking_guard(&self) -> BlockingTaskGuard {
374 self.0.blocking_guard.clone()
375 }
376
377 #[cfg(feature = "rayon")]
379 pub fn proof_storage_worker_pool(&self) -> &WorkerPool {
380 &self.0.proof_storage_worker_pool
381 }
382
383 #[cfg(feature = "rayon")]
385 pub fn proof_account_worker_pool(&self) -> &WorkerPool {
386 &self.0.proof_account_worker_pool
387 }
388
389 #[cfg(feature = "rayon")]
391 pub fn prewarming_pool(&self) -> &WorkerPool {
392 &self.0.prewarming_pool
393 }
394
395 #[cfg(feature = "rayon")]
397 pub fn bal_streaming_pool(&self) -> &WorkerPool {
398 &self.0.bal_streaming_pool
399 }
400
401 #[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
408impl Runtime {
411 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
447enum TaskKind {
451 Default,
453 Blocking,
455}
456
457impl Runtime {
458 pub fn on_shutdown_signal(&self) -> &Shutdown {
460 &self.0.on_shutdown
461 }
462
463 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 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 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 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 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 pub fn spawn_drop<T: Send + 'static>(&self, value: T) {
543 self.spawn_blocking_named("drop", move || drop(value));
544 }
545
546 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn graceful_shutdown(&self) {
827 let _ = self.do_graceful_shutdown(None);
828 }
829
830 pub fn graceful_shutdown_with_timeout(&self, timeout: Duration) -> bool {
835 self.do_graceful_shutdown(Some(timeout))
836 }
837
838 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#[derive(Debug, Clone)]
868pub struct RuntimeBuilder {
869 config: RuntimeConfig,
870}
871
872impl RuntimeBuilder {
873 pub const fn new(config: RuntimeConfig) -> Self {
875 Self { config }
876 }
877
878 #[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 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 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}