use std::{cell::Cell, future::Future, sync::OnceLock, time::Duration};
use tokio::{runtime::Builder, task, time::timeout};
struct NautilusRuntime {
runtime: tokio::runtime::Runtime,
injected: bool,
}
static RUNTIME: OnceLock<NautilusRuntime> = OnceLock::new();
thread_local! {
static NAUTILUS_RUNTIME_THREAD: Cell<bool> = const { Cell::new(false) };
}
const NAUTILUS_WORKER_THREADS: &str = "NAUTILUS_WORKER_THREADS";
fn initialize_runtime() -> NautilusRuntime {
#[cfg(feature = "python")]
{
crate::python::runtime::initialize_python();
}
let worker_threads = std::env::var(NAUTILUS_WORKER_THREADS)
.ok()
.and_then(|val| val.parse::<usize>().ok())
.unwrap_or_default();
let mut builder = Builder::new_multi_thread();
if worker_threads > 0 {
builder.worker_threads(worker_threads);
}
let runtime = builder
.on_thread_start(|| NAUTILUS_RUNTIME_THREAD.set(true))
.on_thread_stop(|| NAUTILUS_RUNTIME_THREAD.set(false))
.enable_all()
.build()
.expect("Failed to create tokio runtime");
NautilusRuntime {
runtime,
injected: false,
}
}
pub fn set_runtime(runtime: tokio::runtime::Runtime) -> Result<(), tokio::runtime::Runtime> {
if RUNTIME.get().is_some()
|| !matches!(
runtime.handle().runtime_flavor(),
tokio::runtime::RuntimeFlavor::MultiThread
)
{
return Err(runtime);
}
RUNTIME
.set(NautilusRuntime {
runtime,
injected: true,
})
.map_err(|runtime| runtime.runtime)
}
pub fn get_runtime() -> &'static tokio::runtime::Runtime {
&RUNTIME.get_or_init(initialize_runtime).runtime
}
pub fn block_in_place_on_nautilus<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return f();
};
if is_on_nautilus_runtime(&handle) {
tokio::task::block_in_place(f)
} else {
f()
}
}
pub fn block_on_nautilus<F>(future: F) -> F::Output
where
F: Future,
{
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return get_runtime().block_on(future);
};
assert!(
matches!(
handle.runtime_flavor(),
tokio::runtime::RuntimeFlavor::MultiThread
),
"block_on_nautilus cannot run inside a current-thread Tokio runtime; use block_on_nautilus_with"
);
tokio::task::block_in_place(|| get_runtime().block_on(future))
}
pub fn block_on_nautilus_with<C, F>(create_future: C) -> F::Output
where
C: FnOnce() -> F + Send,
F: Future,
F::Output: Send,
{
let run = move || get_runtime().block_on(async move { create_future().await });
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return run();
};
if is_on_nautilus_runtime(&handle) {
return tokio::task::block_in_place(run);
}
std::thread::scope(|scope| {
let task = scope.spawn(run);
match task.join() {
Ok(output) => output,
Err(payload) => std::panic::resume_unwind(payload),
}
})
}
fn is_on_nautilus_runtime(handle: &tokio::runtime::Handle) -> bool {
RUNTIME.get().is_some_and(|runtime| {
handle.id() == runtime.runtime.handle().id()
&& (runtime.injected || NAUTILUS_RUNTIME_THREAD.get())
})
}
pub fn shutdown_runtime(wait: Duration) {
if let Some(runtime) = RUNTIME.get() {
runtime.runtime.block_on(async {
let _ = timeout(wait, async {
task::yield_now().await;
})
.await;
});
}
}
#[cfg(test)]
#[expect(
clippy::disallowed_types,
reason = "tests exercise direct Tokio LocalSet interoperability"
)]
mod tests {
use std::process::Command;
use rstest::rstest;
use super::*;
const RUNTIME_CHILD_ENV: &str = "NAUTILUS_COMMON_RUNTIME_CHILD";
#[rstest]
fn test_custom_runtime_installation_and_rejection() {
const MARKER: &str = "custom-runtime-installation";
if !in_runtime_child(MARKER) {
run_runtime_child("test_custom_runtime_installation_and_rejection", MARKER);
return;
}
let runtime = Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("custom runtime should build");
let installed_id = runtime.handle().id();
assert!(set_runtime(runtime).is_ok());
assert_eq!(get_runtime().handle().id(), installed_id);
let duplicate = Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("duplicate runtime should build");
let duplicate_id = duplicate.handle().id();
assert_ne!(duplicate_id, installed_id);
let rejected = set_runtime(duplicate).expect_err("duplicate runtime should be rejected");
assert_eq!(rejected.handle().id(), duplicate_id);
assert_eq!(get_runtime().handle().id(), installed_id);
}
fn in_runtime_child(marker: &str) -> bool {
std::env::var(RUNTIME_CHILD_ENV).as_deref() == Ok(marker)
}
fn run_runtime_child(test_name: &str, marker: &str) {
let output = Command::new(std::env::current_exe().expect("test executable must exist"))
.arg(test_name)
.arg("--nocapture")
.arg("--test-threads=1")
.env(RUNTIME_CHILD_ENV, marker)
.output()
.expect("runtime child process must start");
assert!(
output.status.success(),
"runtime child failed with {}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
#[rstest]
fn set_runtime_rejects_current_thread_runtime() {
const MARKER: &str = "reject-current-thread";
if std::env::var(RUNTIME_CHILD_ENV).as_deref() != Ok(MARKER) {
run_runtime_child("set_runtime_rejects_current_thread_runtime", MARKER);
return;
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let rejected = set_runtime(runtime).unwrap_err();
assert_eq!(
rejected.handle().runtime_flavor(),
tokio::runtime::RuntimeFlavor::CurrentThread
);
}
#[rstest]
fn injected_runtime_drives_bridge_future() {
const MARKER: &str = "injected-bridge";
if std::env::var(RUNTIME_CHILD_ENV).as_deref() != Ok(MARKER) {
run_runtime_child("injected_runtime_drives_bridge_future", MARKER);
return;
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let expected_id = runtime.handle().id();
set_runtime(runtime).unwrap();
let actual_id = block_on_nautilus_with(|| async { tokio::runtime::Handle::current().id() });
assert_eq!(actual_id, expected_id);
}
#[rstest]
fn block_on_nautilus_with_works_without_current_runtime() {
let value = block_on_nautilus_with(|| async { 42 });
assert_eq!(value, 42);
}
#[rstest]
fn block_on_nautilus_with_works_inside_multi_thread_runtime() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let value = runtime.block_on(async {
block_on_nautilus_with(|| async {
tokio::time::sleep(Duration::from_millis(1)).await;
42
})
});
assert_eq!(value, 42);
}
#[rstest]
fn block_on_nautilus_with_works_inside_current_thread_runtime() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let value = runtime.block_on(async {
block_on_nautilus_with(|| async {
tokio::time::sleep(Duration::from_millis(1)).await;
42
})
});
assert_eq!(value, 42);
}
#[rstest]
fn block_on_nautilus_with_works_inside_multi_thread_local_set() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let local_set = tokio::task::LocalSet::new();
let value = runtime.block_on(local_set.run_until(async {
block_on_nautilus_with(|| async {
tokio::time::sleep(Duration::from_millis(1)).await;
42
})
}));
assert_eq!(value, 42);
}
#[rstest]
fn block_on_nautilus_works_inside_foreign_multi_thread_runtime() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let value = runtime.block_on(async { block_on_nautilus(async { 42 }) });
assert_eq!(value, 42);
}
#[rstest]
#[should_panic(expected = "block_on_nautilus cannot run inside a current-thread Tokio runtime")]
fn block_on_nautilus_rejects_current_thread_runtime() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async { block_on_nautilus(async { 42 }) });
}
#[rstest]
fn block_on_nautilus_with_works_inside_nautilus_worker() {
let (caller_thread, factory_thread, value) = get_runtime().block_on(async {
get_runtime()
.spawn(async {
let caller_thread = std::thread::current().id();
let (factory_thread, value) = block_on_nautilus_with(|| async {
let factory_thread = std::thread::current().id();
tokio::time::sleep(Duration::from_millis(1)).await;
(factory_thread, 42)
});
(caller_thread, factory_thread, value)
})
.await
.unwrap()
});
assert_eq!(factory_thread, caller_thread);
assert_eq!(value, 42);
}
#[rstest]
fn block_on_nautilus_with_works_inside_nautilus_blocking_thread() {
let value = get_runtime().block_on(async {
get_runtime()
.spawn_blocking(|| {
block_on_nautilus_with(|| async {
tokio::time::sleep(Duration::from_millis(1)).await;
42
})
})
.await
.unwrap()
});
assert_eq!(value, 42);
}
#[rstest]
fn block_in_place_on_nautilus_works_inside_nautilus_local_set() {
let local_set = tokio::task::LocalSet::new();
let value = get_runtime()
.block_on(local_set.run_until(async { block_in_place_on_nautilus(|| 42) }));
assert_eq!(value, 42);
}
}