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::{
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
40pub const DEFAULT_THREAD_KEEP_ALIVE: Duration = Duration::from_secs(15);
42
43pub const DEFAULT_RESERVED_CPU_CORES: usize = 2;
45
46pub const DEFAULT_STORAGE_POOL_THREADS: usize = 16;
48
49#[cfg(feature = "rayon")]
51pub const DEFAULT_STATE_TRIE_OVERLAY_WORKER_THREADS: usize = 4;
52
53pub const DEFAULT_MAX_BLOCKING_TASKS: usize = 512;
55
56#[derive(Debug, Clone)]
58pub enum TokioConfig {
59 Owned {
61 worker_threads: Option<usize>,
63 thread_keep_alive: Duration,
65 thread_name: &'static str,
67 },
68 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 pub const fn existing_handle(handle: Handle) -> Self {
85 Self::ExistingHandle(handle)
86 }
87
88 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#[derive(Debug, Clone)]
100#[cfg(feature = "rayon")]
101pub struct RayonConfig {
102 pub cpu_threads: Option<usize>,
105 pub reserved_cpu_cores: usize,
107 pub rpc_threads: Option<usize>,
110 pub storage_threads: Option<usize>,
113 pub max_blocking_tasks: usize,
115 pub proof_storage_worker_threads: Option<usize>,
118 pub proof_account_worker_threads: Option<usize>,
121 pub prewarming_threads: Option<usize>,
124 pub bal_streaming_threads: Option<usize>,
127 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 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 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 pub const fn with_rpc_threads(mut self, rpc_threads: usize) -> Self {
166 self.rpc_threads = Some(rpc_threads);
167 self
168 }
169
170 pub const fn with_storage_threads(mut self, storage_threads: usize) -> Self {
172 self.storage_threads = Some(storage_threads);
173 self
174 }
175
176 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 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 pub const fn with_prewarming_threads(mut self, prewarming_threads: usize) -> Self {
196 self.prewarming_threads = Some(prewarming_threads);
197 self
198 }
199
200 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 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 fn default_thread_count(&self) -> usize {
217 let _ = self.reserved_cpu_cores;
220 self.cpu_threads.unwrap_or_else(|| available_parallelism().map_or(1, NonZeroUsize::get))
221 }
222
223 fn proof_storage_worker_threads_override(&self) -> Option<usize> {
226 self.proof_storage_worker_threads.filter(|&threads| threads > 0)
227 }
228
229 fn proof_account_worker_threads_override(&self) -> Option<usize> {
232 self.proof_account_worker_threads.filter(|&threads| threads > 0)
233 }
234}
235
236#[derive(Debug, Clone, Default)]
238pub struct RuntimeConfig {
239 pub tokio: TokioConfig,
241 #[cfg(feature = "rayon")]
243 pub rayon: RayonConfig,
244}
245
246impl RuntimeConfig {
247 pub fn with_tokio(mut self, tokio: TokioConfig) -> Self {
249 self.tokio = tokio;
250 self
251 }
252
253 #[cfg(feature = "rayon")]
255 pub const fn with_rayon(mut self, rayon: RayonConfig) -> Self {
256 self.rayon = rayon;
257 self
258 }
259
260 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#[derive(Debug, thiserror::Error)]
277pub enum RuntimeBuildError {
278 #[error("Failed to build tokio runtime: {0}")]
280 TokioBuild(#[from] std::io::Error),
281 #[cfg(feature = "rayon")]
283 #[error("Failed to build rayon thread pool: {0}")]
284 RayonBuild(#[from] rayon::ThreadPoolBuildError),
285}
286
287struct RuntimeInner {
290 tokio_runtime: Mutex<Option<TokioRuntime>>,
292 handle: Handle,
294 on_shutdown: Shutdown,
296 task_events_tx: UnboundedSender<TaskEvent>,
298 metrics: TaskExecutorMetrics,
300 graceful_tasks: Arc<AtomicUsize>,
302 #[cfg(feature = "rayon")]
304 cpu_pool: rayon::ThreadPool,
305 #[cfg(feature = "rayon")]
307 rpc_pool: BlockingTaskPool,
308 #[cfg(feature = "rayon")]
310 storage_pool: rayon::ThreadPool,
311 #[cfg(feature = "rayon")]
313 blocking_guard: BlockingTaskGuard,
314 #[cfg(feature = "rayon")]
316 proof_storage_worker_pool: WorkerPool,
317 #[cfg(feature = "rayon")]
319 proof_account_worker_pool: WorkerPool,
320 #[cfg(feature = "rayon")]
322 proof_storage_worker_threads_override: Option<usize>,
323 #[cfg(feature = "rayon")]
325 proof_account_worker_threads_override: Option<usize>,
326 #[cfg(feature = "rayon")]
328 prewarming_pool: Arc<WorkerPool>,
329 #[cfg(feature = "rayon")]
331 bal_streaming_pool: Arc<WorkerPool>,
332 #[cfg(feature = "rayon")]
334 state_trie_overlay_worker_pool: Arc<WorkerPool>,
335 worker_map: WorkerMap,
338 task_manager_handle: Mutex<Option<JoinHandle<Result<(), PanickedTaskError>>>>,
342}
343
344impl Drop for RuntimeInner {
345 fn drop(&mut self) {
346 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#[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
373impl Runtime {
376 pub fn take_task_manager_handle(&self) -> Option<JoinHandle<Result<(), PanickedTaskError>>> {
382 self.0.task_manager_handle.lock().unwrap().take()
383 }
384
385 pub fn handle(&self) -> &Handle {
387 &self.0.handle
388 }
389
390 #[cfg(feature = "rayon")]
392 pub fn cpu_pool(&self) -> &rayon::ThreadPool {
393 &self.0.cpu_pool
394 }
395
396 #[cfg(feature = "rayon")]
398 pub fn rpc_pool(&self) -> &BlockingTaskPool {
399 &self.0.rpc_pool
400 }
401
402 #[cfg(feature = "rayon")]
404 pub fn storage_pool(&self) -> &rayon::ThreadPool {
405 &self.0.storage_pool
406 }
407
408 #[cfg(feature = "rayon")]
410 pub fn blocking_guard(&self) -> BlockingTaskGuard {
411 self.0.blocking_guard.clone()
412 }
413
414 #[cfg(feature = "rayon")]
416 pub fn proof_storage_worker_pool(&self) -> &WorkerPool {
417 &self.0.proof_storage_worker_pool
418 }
419
420 #[cfg(feature = "rayon")]
422 pub fn proof_account_worker_pool(&self) -> &WorkerPool {
423 &self.0.proof_account_worker_pool
424 }
425
426 #[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 #[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 #[cfg(feature = "rayon")]
446 pub fn prewarming_pool(&self) -> &Arc<WorkerPool> {
447 &self.0.prewarming_pool
448 }
449
450 #[cfg(feature = "rayon")]
452 pub fn bal_streaming_pool(&self) -> &Arc<WorkerPool> {
453 &self.0.bal_streaming_pool
454 }
455
456 #[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
463impl Runtime {
466 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
502enum TaskKind {
506 Default,
508 Blocking,
510}
511
512impl Runtime {
513 pub fn on_shutdown_signal(&self) -> &Shutdown {
515 &self.0.on_shutdown
516 }
517
518 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 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 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 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 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 self.0.handle.spawn_blocking(Box::new(func) as Box<dyn FnOnce() -> R + Send>)
588 }
589
590 pub fn spawn_drop<T: Send + 'static>(&self, value: T) {
596 self.spawn_blocking_named("drop", move || drop(value));
597 }
598
599 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn graceful_shutdown(&self) {
890 let _ = self.do_graceful_shutdown(None);
891 }
892
893 pub fn graceful_shutdown_with_timeout(&self, timeout: Duration) -> bool {
898 self.do_graceful_shutdown(Some(timeout))
899 }
900
901 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#[derive(Debug, Clone)]
931pub struct RuntimeBuilder {
932 config: RuntimeConfig,
933}
934
935impl RuntimeBuilder {
936 pub const fn new(config: RuntimeConfig) -> Self {
938 Self { config }
939 }
940
941 #[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 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 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 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}