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