nautilus_common/live/
runtime.rs1use std::{cell::Cell, future::Future, sync::OnceLock, time::Duration};
49
50use tokio::{runtime::Builder, task, time::timeout};
51
52struct NautilusRuntime {
53 runtime: tokio::runtime::Runtime,
54 injected: bool,
55}
56
57static RUNTIME: OnceLock<NautilusRuntime> = OnceLock::new();
58
59thread_local! {
60 static NAUTILUS_RUNTIME_THREAD: Cell<bool> = const { Cell::new(false) };
61}
62
63const NAUTILUS_WORKER_THREADS: &str = "NAUTILUS_WORKER_THREADS";
66
67fn initialize_runtime() -> NautilusRuntime {
77 #[cfg(feature = "python")]
79 {
80 crate::python::runtime::initialize_python();
81 }
82
83 let worker_threads = std::env::var(NAUTILUS_WORKER_THREADS)
84 .ok()
85 .and_then(|val| val.parse::<usize>().ok())
86 .unwrap_or_default();
87
88 let mut builder = Builder::new_multi_thread();
89
90 if worker_threads > 0 {
91 builder.worker_threads(worker_threads);
92 }
93
94 let runtime = builder
95 .on_thread_start(|| NAUTILUS_RUNTIME_THREAD.set(true))
96 .on_thread_stop(|| NAUTILUS_RUNTIME_THREAD.set(false))
97 .enable_all()
98 .build()
99 .expect("Failed to create tokio runtime");
100 NautilusRuntime {
101 runtime,
102 injected: false,
103 }
104}
105
106pub fn set_runtime(runtime: tokio::runtime::Runtime) -> Result<(), tokio::runtime::Runtime> {
123 if RUNTIME.get().is_some()
124 || !matches!(
125 runtime.handle().runtime_flavor(),
126 tokio::runtime::RuntimeFlavor::MultiThread
127 )
128 {
129 return Err(runtime);
130 }
131
132 RUNTIME
133 .set(NautilusRuntime {
134 runtime,
135 injected: true,
136 })
137 .map_err(|runtime| runtime.runtime)
138}
139
140pub fn get_runtime() -> &'static tokio::runtime::Runtime {
146 &RUNTIME.get_or_init(initialize_runtime).runtime
147}
148
149pub fn block_in_place_on_nautilus<F, R>(f: F) -> R
157where
158 F: FnOnce() -> R,
159{
160 let Ok(handle) = tokio::runtime::Handle::try_current() else {
161 return f();
162 };
163
164 if is_on_nautilus_runtime(&handle) {
165 tokio::task::block_in_place(f)
166 } else {
167 f()
168 }
169}
170
171pub fn block_on_nautilus<F>(future: F) -> F::Output
182where
183 F: Future,
184{
185 let Ok(handle) = tokio::runtime::Handle::try_current() else {
186 return get_runtime().block_on(future);
187 };
188
189 assert!(
190 matches!(
191 handle.runtime_flavor(),
192 tokio::runtime::RuntimeFlavor::MultiThread
193 ),
194 "block_on_nautilus cannot run inside a current-thread Tokio runtime; use block_on_nautilus_with"
195 );
196
197 tokio::task::block_in_place(|| get_runtime().block_on(future))
198}
199
200pub fn block_on_nautilus_with<C, F>(create_future: C) -> F::Output
219where
220 C: FnOnce() -> F + Send,
221 F: Future,
222 F::Output: Send,
223{
224 let run = move || get_runtime().block_on(async move { create_future().await });
225 let Ok(handle) = tokio::runtime::Handle::try_current() else {
226 return run();
227 };
228
229 if is_on_nautilus_runtime(&handle) {
230 return tokio::task::block_in_place(run);
231 }
232
233 std::thread::scope(|scope| {
234 let task = scope.spawn(run);
235 match task.join() {
236 Ok(output) => output,
237 Err(payload) => std::panic::resume_unwind(payload),
238 }
239 })
240}
241
242fn is_on_nautilus_runtime(handle: &tokio::runtime::Handle) -> bool {
243 RUNTIME.get().is_some_and(|runtime| {
244 handle.id() == runtime.runtime.handle().id()
245 && (runtime.injected || NAUTILUS_RUNTIME_THREAD.get())
246 })
247}
248
249pub fn shutdown_runtime(wait: Duration) {
255 if let Some(runtime) = RUNTIME.get() {
256 runtime.runtime.block_on(async {
257 let _ = timeout(wait, async {
258 task::yield_now().await;
259 })
260 .await;
261 });
262 }
263}
264
265#[cfg(test)]
266#[expect(
267 clippy::disallowed_types,
268 reason = "tests exercise direct Tokio LocalSet interoperability"
269)]
270mod tests {
271 use std::process::Command;
272
273 use rstest::rstest;
274
275 use super::*;
276
277 const RUNTIME_CHILD_ENV: &str = "NAUTILUS_COMMON_RUNTIME_CHILD";
278
279 #[rstest]
280 fn test_custom_runtime_installation_and_rejection() {
281 const MARKER: &str = "custom-runtime-installation";
282 if !in_runtime_child(MARKER) {
283 run_runtime_child("test_custom_runtime_installation_and_rejection", MARKER);
284 return;
285 }
286
287 let runtime = Builder::new_multi_thread()
288 .worker_threads(1)
289 .enable_all()
290 .build()
291 .expect("custom runtime should build");
292 let installed_id = runtime.handle().id();
293
294 assert!(set_runtime(runtime).is_ok());
295 assert_eq!(get_runtime().handle().id(), installed_id);
296
297 let duplicate = Builder::new_multi_thread()
298 .worker_threads(1)
299 .enable_all()
300 .build()
301 .expect("duplicate runtime should build");
302 let duplicate_id = duplicate.handle().id();
303 assert_ne!(duplicate_id, installed_id);
304
305 let rejected = set_runtime(duplicate).expect_err("duplicate runtime should be rejected");
306 assert_eq!(rejected.handle().id(), duplicate_id);
307 assert_eq!(get_runtime().handle().id(), installed_id);
308 }
309
310 fn in_runtime_child(marker: &str) -> bool {
311 std::env::var(RUNTIME_CHILD_ENV).as_deref() == Ok(marker)
312 }
313
314 fn run_runtime_child(test_name: &str, marker: &str) {
315 let output = Command::new(std::env::current_exe().expect("test executable must exist"))
316 .arg(test_name)
317 .arg("--nocapture")
318 .arg("--test-threads=1")
319 .env(RUNTIME_CHILD_ENV, marker)
320 .output()
321 .expect("runtime child process must start");
322
323 assert!(
324 output.status.success(),
325 "runtime child failed with {}\nstdout:\n{}\nstderr:\n{}",
326 output.status,
327 String::from_utf8_lossy(&output.stdout),
328 String::from_utf8_lossy(&output.stderr),
329 );
330 }
331
332 #[rstest]
333 fn set_runtime_rejects_current_thread_runtime() {
334 const MARKER: &str = "reject-current-thread";
335 if std::env::var(RUNTIME_CHILD_ENV).as_deref() != Ok(MARKER) {
336 run_runtime_child("set_runtime_rejects_current_thread_runtime", MARKER);
337 return;
338 }
339 let runtime = tokio::runtime::Builder::new_current_thread()
340 .enable_all()
341 .build()
342 .unwrap();
343
344 let rejected = set_runtime(runtime).unwrap_err();
345
346 assert_eq!(
347 rejected.handle().runtime_flavor(),
348 tokio::runtime::RuntimeFlavor::CurrentThread
349 );
350 }
351
352 #[rstest]
353 fn injected_runtime_drives_bridge_future() {
354 const MARKER: &str = "injected-bridge";
355 if std::env::var(RUNTIME_CHILD_ENV).as_deref() != Ok(MARKER) {
356 run_runtime_child("injected_runtime_drives_bridge_future", MARKER);
357 return;
358 }
359 let runtime = tokio::runtime::Builder::new_multi_thread()
360 .worker_threads(2)
361 .enable_all()
362 .build()
363 .unwrap();
364 let expected_id = runtime.handle().id();
365 set_runtime(runtime).unwrap();
366
367 let actual_id = block_on_nautilus_with(|| async { tokio::runtime::Handle::current().id() });
368
369 assert_eq!(actual_id, expected_id);
370 }
371
372 #[rstest]
373 fn block_on_nautilus_with_works_without_current_runtime() {
374 let value = block_on_nautilus_with(|| async { 42 });
375
376 assert_eq!(value, 42);
377 }
378
379 #[rstest]
380 fn block_on_nautilus_with_works_inside_multi_thread_runtime() {
381 let runtime = tokio::runtime::Builder::new_multi_thread()
382 .worker_threads(2)
383 .enable_all()
384 .build()
385 .unwrap();
386 let value = runtime.block_on(async {
387 block_on_nautilus_with(|| async {
388 tokio::time::sleep(Duration::from_millis(1)).await;
389 42
390 })
391 });
392
393 assert_eq!(value, 42);
394 }
395
396 #[rstest]
397 fn block_on_nautilus_with_works_inside_current_thread_runtime() {
398 let runtime = tokio::runtime::Builder::new_current_thread()
399 .enable_all()
400 .build()
401 .unwrap();
402 let value = runtime.block_on(async {
403 block_on_nautilus_with(|| async {
404 tokio::time::sleep(Duration::from_millis(1)).await;
405 42
406 })
407 });
408
409 assert_eq!(value, 42);
410 }
411
412 #[rstest]
413 fn block_on_nautilus_with_works_inside_multi_thread_local_set() {
414 let runtime = tokio::runtime::Builder::new_multi_thread()
415 .worker_threads(2)
416 .enable_all()
417 .build()
418 .unwrap();
419 let local_set = tokio::task::LocalSet::new();
420 let value = runtime.block_on(local_set.run_until(async {
421 block_on_nautilus_with(|| async {
422 tokio::time::sleep(Duration::from_millis(1)).await;
423 42
424 })
425 }));
426
427 assert_eq!(value, 42);
428 }
429
430 #[rstest]
431 fn block_on_nautilus_works_inside_foreign_multi_thread_runtime() {
432 let runtime = tokio::runtime::Builder::new_multi_thread()
433 .worker_threads(2)
434 .enable_all()
435 .build()
436 .unwrap();
437 let value = runtime.block_on(async { block_on_nautilus(async { 42 }) });
438
439 assert_eq!(value, 42);
440 }
441
442 #[rstest]
443 #[should_panic(expected = "block_on_nautilus cannot run inside a current-thread Tokio runtime")]
444 fn block_on_nautilus_rejects_current_thread_runtime() {
445 let runtime = tokio::runtime::Builder::new_current_thread()
446 .enable_all()
447 .build()
448 .unwrap();
449
450 runtime.block_on(async { block_on_nautilus(async { 42 }) });
451 }
452
453 #[rstest]
454 fn block_on_nautilus_with_works_inside_nautilus_worker() {
455 let (caller_thread, factory_thread, value) = get_runtime().block_on(async {
456 get_runtime()
457 .spawn(async {
458 let caller_thread = std::thread::current().id();
459 let (factory_thread, value) = block_on_nautilus_with(|| async {
460 let factory_thread = std::thread::current().id();
461 tokio::time::sleep(Duration::from_millis(1)).await;
462 (factory_thread, 42)
463 });
464 (caller_thread, factory_thread, value)
465 })
466 .await
467 .unwrap()
468 });
469
470 assert_eq!(factory_thread, caller_thread);
471 assert_eq!(value, 42);
472 }
473
474 #[rstest]
475 fn block_on_nautilus_with_works_inside_nautilus_blocking_thread() {
476 let value = get_runtime().block_on(async {
477 get_runtime()
478 .spawn_blocking(|| {
479 block_on_nautilus_with(|| async {
480 tokio::time::sleep(Duration::from_millis(1)).await;
481 42
482 })
483 })
484 .await
485 .unwrap()
486 });
487
488 assert_eq!(value, 42);
489 }
490
491 #[rstest]
492 fn block_in_place_on_nautilus_works_inside_nautilus_local_set() {
493 let local_set = tokio::task::LocalSet::new();
494 let value = get_runtime()
495 .block_on(local_set.run_until(async { block_in_place_on_nautilus(|| 42) }));
496
497 assert_eq!(value, 42);
498 }
499}