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, Tokio's default is used.
56const NAUTILUS_WORKER_THREADS: &str = "NAUTILUS_WORKER_THREADS";
57
58/// Creates and configures a new multi-threaded Tokio runtime.
59///
60/// The number of OS threads is configured using the `NAUTILUS_WORKER_THREADS`
61/// environment variable. If not set, all available logical CPUs will be used.
62///
63/// # Panics
64///
65/// Panics if the runtime could not be created, which typically indicates
66/// an inability to spawn threads or allocate necessary resources.
67fn initialize_runtime() -> tokio::runtime::Runtime {
68    // Initialize Python if running as a Python extension module
69    #[cfg(feature = "python")]
70    {
71        crate::python::runtime::initialize_python();
72    }
73
74    let worker_threads = std::env::var(NAUTILUS_WORKER_THREADS)
75        .ok()
76        .and_then(|val| val.parse::<usize>().ok())
77        .unwrap_or_default();
78
79    let mut builder = Builder::new_multi_thread();
80
81    if worker_threads > 0 {
82        builder.worker_threads(worker_threads);
83    }
84
85    builder
86        .enable_all()
87        .build()
88        .expect("Failed to create tokio runtime")
89}
90
91/// Sets a custom pre-built Tokio runtime as the global Nautilus runtime.
92///
93/// Must be called before the first [`get_runtime`] invocation (i.e. before
94/// `LiveNode::build()` or any adapter/client usage). This gives callers who
95/// own `main()` full control over worker threads, blocking threads, thread
96/// names, stack sizes, and any other [`tokio::runtime::Builder`] options.
97///
98/// # Runtime Requirements
99///
100/// The supplied runtime must be multi-threaded and have all Tokio drivers
101/// enabled with `tokio::runtime::Builder::enable_all()`.
102///
103/// # Errors
104///
105/// Returns `Err(runtime)` if a runtime was already initialized.
106pub fn set_runtime(runtime: tokio::runtime::Runtime) -> Result<(), tokio::runtime::Runtime> {
107    RUNTIME.set(runtime)
108}
109
110/// Returns a reference to the global Nautilus Tokio runtime.
111///
112/// The runtime is lazily initialized on the first call and reused thereafter.
113/// If a custom runtime was previously installed via [`set_runtime`], that
114/// runtime is returned instead.
115pub fn get_runtime() -> &'static tokio::runtime::Runtime {
116    RUNTIME.get_or_init(initialize_runtime)
117}
118
119/// Provides a best-effort flush for runtime tasks during shutdown.
120///
121/// The function yields once to the Tokio scheduler and gives outstanding tasks a chance
122/// to observe shutdown signals before Python finalizes the interpreter, which calls this via
123/// an `atexit` hook.
124pub fn shutdown_runtime(wait: Duration) {
125    if let Some(runtime) = RUNTIME.get() {
126        runtime.block_on(async {
127            let _ = timeout(wait, async {
128                task::yield_now().await;
129            })
130            .await;
131        });
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::process::Command;
138
139    use rstest::rstest;
140
141    use super::*;
142
143    const RUNTIME_CHILD_ENV: &str = "NAUTILUS_COMMON_RUNTIME_CHILD";
144
145    #[rstest]
146    fn test_custom_runtime_installation_and_rejection() {
147        const MARKER: &str = "custom-runtime-installation";
148        if !in_runtime_child(MARKER) {
149            run_runtime_child("test_custom_runtime_installation_and_rejection", MARKER);
150            return;
151        }
152
153        let runtime = Builder::new_multi_thread()
154            .worker_threads(1)
155            .enable_all()
156            .build()
157            .expect("custom runtime should build");
158        let installed_id = runtime.handle().id();
159
160        assert!(set_runtime(runtime).is_ok());
161        assert_eq!(get_runtime().handle().id(), installed_id);
162
163        let duplicate = Builder::new_multi_thread()
164            .worker_threads(1)
165            .enable_all()
166            .build()
167            .expect("duplicate runtime should build");
168        let duplicate_id = duplicate.handle().id();
169        assert_ne!(duplicate_id, installed_id);
170
171        let rejected = set_runtime(duplicate).expect_err("duplicate runtime should be rejected");
172        assert_eq!(rejected.handle().id(), duplicate_id);
173        assert_eq!(get_runtime().handle().id(), installed_id);
174    }
175
176    fn in_runtime_child(marker: &str) -> bool {
177        std::env::var(RUNTIME_CHILD_ENV).as_deref() == Ok(marker)
178    }
179
180    fn run_runtime_child(test_name: &str, marker: &str) {
181        let output = Command::new(std::env::current_exe().expect("test executable must exist"))
182            .arg(test_name)
183            .arg("--nocapture")
184            .arg("--test-threads=1")
185            .env(RUNTIME_CHILD_ENV, marker)
186            .output()
187            .expect("runtime child process must start");
188
189        assert!(
190            output.status.success(),
191            "runtime child failed with {}\nstdout:\n{}\nstderr:\n{}",
192            output.status,
193            String::from_utf8_lossy(&output.stdout),
194            String::from_utf8_lossy(&output.stderr),
195        );
196    }
197}