Skip to main content

nautilus_common/live/
runtime.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! The centralized Tokio runtime for a running Nautilus system.
17//!
18//! # Design Rationale
19//!
20//! NautilusTrader uses a single global Tokio runtime because:
21//! - A single long-lived runtime avoids repeated startup/shutdown overhead.
22//! - The runtime is lazily initialized on first call to `get_runtime()` via `OnceLock`.
23//! - Worker thread count is configurable via the `NAUTILUS_WORKER_THREADS` environment variable.
24//! - Rust-native hosts can install a pre-built runtime via [`set_runtime`] before first use.
25//!
26//! # Custom Runtime Injection
27//!
28//! Callers who use [`set_runtime`] must supply a multi-threaded runtime built with
29//! `tokio::runtime::Builder::new_multi_thread()` and `enable_all()`. Adapters assume I/O,
30//! timers, spawning, and `tokio::task::block_in_place()` are available.
31//!
32//! # Python Support
33//!
34//! When the `python` feature is enabled, the runtime initializes the Python interpreter
35//! before starting worker threads. The PyO3 module registers an `atexit` handler via
36//! `shutdown_runtime()` to cleanly shut down when Python exits.
37//!
38//! A runtime passed to [`set_runtime`] is already built, so this module cannot run the default
39//! Python initialization hook before its worker threads start. Hosts using custom runtimes with
40//! Python support must prepare Python before building the runtime.
41//!
42//! # Testing Considerations
43//!
44//! The global runtime pattern makes it harder to inject test doubles. For testing:
45//! - Unit tests can use `#[tokio::test]` which creates its own runtime.
46//! - Integration tests should be aware they share the global runtime state.
47
48use std::{sync::OnceLock, time::Duration};
49
50use tokio::{runtime::Builder, task, time::timeout};
51
52static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
53
54/// Environment variable name to configure the number of OS threads for the common runtime.
55/// If not set or if the value cannot be parsed as a positive integer, the default value is used.
56const NAUTILUS_WORKER_THREADS: &str = "NAUTILUS_WORKER_THREADS";
57
58/// The default number of OS threads to use if the environment variable is not set.
59///
60/// 0 means Tokio will use the default (number of logical CPUs).
61const DEFAULT_OS_THREADS: usize = 0;
62
63/// Creates and configures a new multi-threaded Tokio runtime.
64///
65/// The number of OS threads is configured using the `NAUTILUS_WORKER_THREADS`
66/// environment variable. If not set, all available logical CPUs will be used.
67///
68/// # Panics
69///
70/// Panics if the runtime could not be created, which typically indicates
71/// an inability to spawn threads or allocate necessary resources.
72fn initialize_runtime() -> tokio::runtime::Runtime {
73    // Initialize Python if running as a Python extension module
74    #[cfg(feature = "python")]
75    {
76        crate::python::runtime::initialize_python();
77    }
78
79    let worker_threads = std::env::var(NAUTILUS_WORKER_THREADS)
80        .ok()
81        .and_then(|val| val.parse::<usize>().ok())
82        .unwrap_or(DEFAULT_OS_THREADS);
83
84    let mut builder = Builder::new_multi_thread();
85
86    let builder = if worker_threads > 0 {
87        builder.worker_threads(worker_threads)
88    } else {
89        &mut builder
90    };
91
92    builder
93        .enable_all()
94        .build()
95        .expect("Failed to create tokio runtime")
96}
97
98/// Sets a custom pre-built Tokio runtime as the global Nautilus runtime.
99///
100/// Must be called before the first [`get_runtime`] invocation (i.e. before
101/// `LiveNode::build()` or any adapter/client usage). This gives callers who
102/// own `main()` full control over worker threads, blocking threads, thread
103/// names, stack sizes, and any other [`tokio::runtime::Builder`] options.
104///
105/// # Runtime Requirements
106///
107/// The supplied runtime must be multi-threaded and have all Tokio drivers
108/// enabled with `tokio::runtime::Builder::enable_all()`.
109///
110/// # Errors
111///
112/// Returns `Err(runtime)` if a runtime was already initialized.
113pub fn set_runtime(runtime: tokio::runtime::Runtime) -> Result<(), tokio::runtime::Runtime> {
114    RUNTIME.set(runtime)
115}
116
117/// Returns a reference to the global Nautilus Tokio runtime.
118///
119/// The runtime is lazily initialized on the first call and reused thereafter.
120/// If a custom runtime was previously installed via [`set_runtime`], that
121/// runtime is returned instead.
122pub fn get_runtime() -> &'static tokio::runtime::Runtime {
123    RUNTIME.get_or_init(initialize_runtime)
124}
125
126/// Provides a best-effort flush for runtime tasks during shutdown.
127///
128/// The function yields once to the Tokio scheduler and gives outstanding tasks a chance
129/// to observe shutdown signals before Python finalizes the interpreter, which calls this via
130/// an `atexit` hook.
131pub fn shutdown_runtime(wait: Duration) {
132    if let Some(runtime) = RUNTIME.get() {
133        runtime.block_on(async {
134            let _ = timeout(wait, async {
135                task::yield_now().await;
136            })
137            .await;
138        });
139    }
140}