Skip to main content

xet_runtime/core/runtime/
native.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::fmt::Display;
4use std::future::Future;
5use std::panic::AssertUnwindSafe;
6use std::pin::pin;
7use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8use std::sync::{Arc, LazyLock, OnceLock, Weak};
9use std::task::{Context, Waker};
10
11use futures::FutureExt;
12use tokio::runtime::{Builder as TokioRuntimeBuilder, Handle as TokioRuntimeHandle, Runtime as TokioRuntime};
13use tokio::sync::oneshot;
14use tokio::task::JoinHandle;
15use tracing::{debug, info};
16
17pub use super::RuntimeMode;
18use crate::config::XetConfig;
19use crate::error::RuntimeError;
20#[cfg(feature = "fd-track")]
21use crate::fd_diagnostics::{report_fd_count, track_fd_scope};
22use crate::logging::SystemMonitor;
23use crate::utils::ClosureGuard as CallbackGuard;
24
25const THREADPOOL_THREAD_ID_PREFIX: &str = "hf-xet"; // thread names will be hf-xet-0, hf-xet-1, etc.
26const THREADPOOL_STACK_SIZE: usize = 8_000_000; // 8MB stack size
27
28/// Cap the number of tokio threads to 32 to avoid massive expansion on huge CPUs; can be overridden with
29/// TOKIO_WORKER_THREADS.
30///
31/// Note that the compute intensive parts of the code get offloaded to blocking threads, which don't count against this
32/// limit.
33const THREADPOOL_MAX_ASYNC_THREADS: usize = 32;
34
35/// Returns the number of Tokio worker threads to use:
36/// 1) If `TOKIO_WORKER_THREADS` is set to a positive integer, use that.
37/// 2) Otherwise, use `min(available_parallelism, THREADPOOL_MAX_ASYNC_THREADS)`, with a floor of 2.
38fn get_num_tokio_worker_threads() -> usize {
39    use std::num::NonZeroUsize;
40
41    // Allow TOKIO_WORKER_THREADS to override this value.
42    if let Ok(val) = std::env::var("TOKIO_WORKER_THREADS") {
43        match val.parse::<usize>() {
44            Ok(n) if n > 0 => {
45                info!("Using {n} async threads from TOKIO_WORKER_THREADS");
46                return n;
47            },
48            _ => {
49                use tracing::warn;
50
51                warn!(
52                    value = %val,
53                    "Invalid TOKIO_WORKER_THREADS; must be a positive integer. Falling back to auto."
54                );
55            },
56        }
57    }
58
59    let cores = std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
60
61    // Minimum 2 threads needed to run everything
62    let n = cores.clamp(2, THREADPOOL_MAX_ASYNC_THREADS);
63    info!("Using {n} async threads for tokio runtime");
64    n
65}
66
67type OwnedRuntimeCell = Arc<std::sync::RwLock<Option<Arc<TokioRuntime>>>>;
68
69// Use thread-local references to the active XetRuntime on each tokio worker thread so code can
70// resolve the active pool. Weak (not Arc) avoids a cycle: worker TLS -> Arc<XetRuntime> ->
71// OwnedRuntimeCell -> TokioRuntime -> worker threads.
72thread_local! {
73    static THREAD_THREADPOOL_REF: RefCell<Option<(u32, Weak<XetRuntime>)>> =
74        const { RefCell::new(None) };
75}
76
77// External-mode XetRuntime instances from `from_external_with_config` are registered by tokio runtime ID
78// so duplicate attachment can be detected. Removed in Drop when the last Arc is released.
79static EXTERNAL_THREADPOOL_REGISTRY: LazyLock<std::sync::RwLock<HashMap<tokio::runtime::Id, Weak<XetRuntime>>>> =
80    LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
81
82#[derive(Debug)]
83enum RuntimeBackend {
84    External { handle_id: Option<tokio::runtime::Id> },
85    OwnedThreadPool { runtime: OwnedRuntimeCell },
86}
87
88/// [`XetRuntime`] is the async execution backend: it either owns a Tokio multi-thread runtime or wraps an
89/// external [`TokioRuntimeHandle`](tokio::runtime::Handle) (see [`RuntimeMode`]).
90///
91/// It exposes [`Self::bridge_async`] and [`Self::bridge_sync`] to run work on the pool, [`Self::spawn`] for
92/// fire-and-forget tasks, and [`Self::perform_sigint_shutdown`] / [`Self::in_sigint_shutdown`] so callers can
93/// align with process-wide SIGINT teardown.
94///
95/// # Example
96///
97/// ```rust
98/// use xet_runtime::config::XetConfig;
99/// use xet_runtime::core::XetRuntime;
100///
101/// let pool = XetRuntime::new(&XetConfig::new()).expect("Error initializing runtime.");
102///
103/// let result = pool
104///     .bridge_sync(async {
105///         // Your async code here
106///         42
107///     })
108///     .expect("Task Error.");
109///
110/// assert_eq!(result, 42);
111/// ```
112#[derive(Debug)]
113pub struct XetRuntime {
114    // Runtime backend and its owned state (if any).
115    backend: RuntimeBackend,
116
117    // We use this handle when we actually enter the runtime to avoid the lock.  It is
118    // the same as using the runtime, with the exception that it does not block a shutdown
119    // while holding a reference to the runtime does.
120    handle_ref: OnceLock<TokioRuntimeHandle>,
121
122    // The number of external threads calling into this runtime.
123    external_executor_count: AtomicUsize,
124
125    // Are we in the middle of a sigint shutdown?
126    sigint_shutdown: AtomicBool,
127
128    // PID of the process that created this runtime; used in Drop to detect fork.
129    creation_pid: u32,
130
131    //  System monitor instance if enabled, monitor starts on initiation
132    system_monitor: Option<SystemMonitor>,
133}
134
135fn system_monitor_for_config(config: &XetConfig) -> Option<SystemMonitor> {
136    config
137        .system_monitor
138        .enabled
139        .then(|| {
140            SystemMonitor::follow_process(config.system_monitor.sample_interval, config.system_monitor.log_path.clone())
141                .ok()
142        })
143        .flatten()
144}
145
146impl XetRuntime {
147    /// Creates a new owned tokio thread pool with the given configuration.
148    pub fn new(config: &XetConfig) -> Result<Arc<Self>, RuntimeError> {
149        #[cfg(feature = "fd-track")]
150        let _fd_scope = track_fd_scope("XetRuntime::new");
151
152        let runtime = Arc::new(std::sync::RwLock::new(None));
153
154        let rt = Arc::new(Self {
155            backend: RuntimeBackend::OwnedThreadPool {
156                runtime: runtime.clone(),
157            },
158            handle_ref: OnceLock::new(),
159            external_executor_count: 0.into(),
160            sigint_shutdown: false.into(),
161            creation_pid: std::process::id(),
162            system_monitor: system_monitor_for_config(config),
163        });
164
165        let rt_weak = Arc::downgrade(&rt);
166        let pid = std::process::id();
167        let set_threadlocal_reference = move || {
168            THREAD_THREADPOOL_REF.set(Some((pid, rt_weak.clone())));
169        };
170
171        let thread_id = AtomicUsize::new(0);
172        let get_thread_name = move || {
173            let id = thread_id.fetch_add(1, Ordering::Relaxed);
174            format!("{THREADPOOL_THREAD_ID_PREFIX}-{id}")
175        };
176
177        let tokio_rt = TokioRuntimeBuilder::new_multi_thread()
178            .worker_threads(get_num_tokio_worker_threads())
179            .on_thread_start(set_threadlocal_reference)
180            .thread_keep_alive(std::time::Duration::from_millis(100))
181            .thread_name_fn(get_thread_name)
182            .thread_stack_size(THREADPOOL_STACK_SIZE)
183            .enable_all()
184            .build()
185            .map_err(RuntimeError::RuntimeInit)?;
186
187        let handle = tokio_rt.handle().clone();
188        let tokio_rt = Arc::new(tokio_rt);
189        *runtime.write().unwrap() = Some(tokio_rt);
190        rt.handle_ref.set(handle).unwrap();
191
192        #[cfg(feature = "fd-track")]
193        report_fd_count("XetRuntime::new complete");
194
195        Ok(rt)
196    }
197
198    /// Wraps an existing tokio handle with a new `XetRuntime`, using the config for
199    /// system monitor setup.
200    pub fn from_external_with_config(
201        rt_handle: TokioRuntimeHandle,
202        config: &XetConfig,
203    ) -> Result<Arc<Self>, RuntimeError> {
204        #[cfg(feature = "fd-track")]
205        let _fd_scope = track_fd_scope("XetRuntime::from_external_with_config");
206
207        let id = rt_handle.id();
208
209        let mut reg = EXTERNAL_THREADPOOL_REGISTRY.write()?;
210        if let Some(existing) = reg.get(&id)
211            && existing.upgrade().is_some()
212        {
213            return Err(RuntimeError::ExternalAlreadyAttached(id));
214        }
215
216        let rt = Arc::new(Self {
217            backend: RuntimeBackend::External { handle_id: Some(id) },
218            handle_ref: rt_handle.into(),
219            external_executor_count: 0.into(),
220            sigint_shutdown: false.into(),
221            creation_pid: std::process::id(),
222            system_monitor: system_monitor_for_config(config),
223        });
224
225        reg.insert(id, Arc::downgrade(&rt));
226
227        #[cfg(feature = "fd-track")]
228        report_fd_count("XetRuntime::from_external_with_config complete");
229
230        Ok(rt)
231    }
232
233    /// Wraps an existing tokio handle without system monitoring.
234    pub fn from_external(rt_handle: TokioRuntimeHandle) -> Arc<Self> {
235        Arc::new(Self {
236            backend: RuntimeBackend::External { handle_id: None },
237            handle_ref: rt_handle.into(),
238            external_executor_count: 0.into(),
239            sigint_shutdown: false.into(),
240            creation_pid: std::process::id(),
241            system_monitor: None,
242        })
243    }
244
245    /// Returns the current thread's active owned [`XetRuntime`], if any.
246    ///
247    /// This is populated on owned runtime worker threads and on spawn_blocking
248    /// threads created by an owned runtime. External runtimes do not set this.
249    #[inline]
250    pub fn current_if_exists() -> Option<Arc<Self>> {
251        let pid = std::process::id();
252        THREAD_THREADPOOL_REF.with_borrow(|entry| {
253            entry
254                .as_ref()
255                .filter(|(entry_pid, _)| *entry_pid == pid)
256                .and_then(|(_, weak_pool)| weak_pool.upgrade())
257        })
258    }
259
260    #[inline]
261    pub fn handle(&self) -> TokioRuntimeHandle {
262        self.handle_ref.get().expect("Not initialized with handle set.").clone()
263    }
264
265    #[inline]
266    pub fn num_worker_threads(&self) -> usize {
267        self.handle().metrics().num_workers()
268    }
269
270    /// Gives the number of concurrent sync bridge callers (`external_run_async_task` and `bridge_sync`).
271    #[inline]
272    pub fn external_executor_count(&self) -> usize {
273        self.external_executor_count.load(Ordering::SeqCst)
274    }
275
276    /// Cancels and shuts down the runtime.  All tasks currently running will be aborted.
277    ///
278    /// A concurrent [`bridge_sync`](Self::bridge_sync) or in-flight
279    /// [`bridge_async`](Self::bridge_async) may still hold a cloned `Arc` to the tokio runtime
280    /// until that call returns, so teardown of the reactor may complete only after those finish.
281    pub fn perform_sigint_shutdown(&self) {
282        #[cfg(feature = "fd-track")]
283        let _fd_scope = track_fd_scope("XetRuntime::perform_sigint_shutdown");
284
285        // Shut down the tokio
286        self.sigint_shutdown.store(true, Ordering::SeqCst);
287
288        if cfg!(debug_assertions) {
289            eprintln!("SIGINT detected, shutting down.");
290        }
291
292        // External mode wraps a caller-owned runtime and has no owned runtime to tear down.
293        let Some(runtime_cell) = self.runtime_cell_if_owned() else {
294            if let Some(monitor) = &self.system_monitor {
295                let _ = monitor.stop();
296            }
297            return;
298        };
299
300        // When a task is shut down, it will stop running at whichever .await it has yielded at.  All local
301        // variables are destroyed by running their destructor.
302        let maybe_runtime = match runtime_cell.write() {
303            Ok(mut guard) => guard.take(),
304            Err(poisoned) => {
305                eprintln!("WARNING: perform_sigint_shutdown encountered a poisoned runtime lock; continuing shutdown.");
306                poisoned.into_inner().take()
307            },
308        };
309
310        let Some(runtime) = maybe_runtime else {
311            eprintln!("WARNING: perform_sigint_shutdown called on runtime that has already been shut down.");
312            if let Some(monitor) = &self.system_monitor {
313                let _ = monitor.stop();
314            }
315            return;
316        };
317
318        // Dropping the runtime will cancel all the tasks; shutdown occurs when the next async call
319        // is encountered.  Ideally, all async code should be cancellation safe.
320        drop(runtime);
321
322        // Stops the system monitor loop if there is one running.
323        if let Some(monitor) = &self.system_monitor {
324            let _ = monitor.stop();
325        }
326    }
327
328    /// Discards the runtime without shutdown; to be used after fork-exec or spawn.
329    pub fn discard_runtime(&self) {
330        // This function makes a best effort attempt to clean everything up.
331
332        let Some(runtime_cell) = self.runtime_cell_if_owned() else {
333            return;
334        };
335
336        // When a task is shut down, it will stop running at whichever .await it has yielded at.  All local
337        // variables are destroyed by running their destructor.
338        //
339        // If this call fails, then it means that there is a recursive call to this runtime, or that
340        // this process is in the middle of a shutdown, so we can ignore it silently.
341        let Ok(mut rt_lock) = runtime_cell.write() else {
342            return;
343        };
344
345        let Some(runtime) = rt_lock.take() else {
346            return;
347        };
348
349        // In this context, we actually want to simply leak the runtime, as doing anything with it will
350        // likely cause a deadlock.  The memory will be reaped when the child process returns, and it's
351        // likely in the copy-on-write state anyway.
352        std::mem::forget(runtime);
353    }
354
355    /// Returns true if we're in the middle of a sigint shutdown,
356    /// and false otherwise.
357    pub fn in_sigint_shutdown(&self) -> bool {
358        self.sigint_shutdown.load(Ordering::SeqCst)
359    }
360
361    fn check_sigint(&self) -> Result<(), RuntimeError> {
362        if self.in_sigint_shutdown() {
363            Err(RuntimeError::KeyboardInterrupt)
364        } else {
365            Ok(())
366        }
367    }
368
369    /// This function should ONLY be used by threads outside of tokio; it should not be called
370    /// from within a task running on the runtime worker pool.  Doing so can lead to deadlocking.
371    pub fn external_run_async_task<F>(&self, future: F) -> Result<F::Output, RuntimeError>
372    where
373        F: Future + Send + 'static,
374        F::Output: Send + 'static,
375    {
376        self.external_executor_count.fetch_add(1, Ordering::SeqCst);
377        let _executor_count_guard = CallbackGuard::new(|| {
378            self.external_executor_count.fetch_sub(1, Ordering::SeqCst);
379        });
380
381        self.handle().block_on(async move {
382            // Run the actual task on a task worker thread so we can get back information
383            // on issues, including reporting panics as runtime errors.
384            self.handle().spawn(future).await.map_err(RuntimeError::from)
385        })
386    }
387
388    /// Spawn an async task to run in the background on the current pool of worker threads.
389    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
390    where
391        F: Future + Send + 'static,
392        F::Output: Send + 'static,
393    {
394        // If the runtime has been shut down, this will immediately abort.
395        debug!("xet-runtime: spawn called, {}", self);
396        self.handle().spawn(future)
397    }
398
399    /// Run a future on the appropriate runtime for this `XetRuntime`.
400    ///
401    /// - **External mode**: the future is awaited directly on the caller's executor.
402    /// - **Owned mode**: the future is spawned onto the owned thread pool and the result is delivered via a `oneshot`
403    ///   channel (compatible with any executor).
404    ///
405    /// This is the primary async entry point. Session-level async methods should call
406    /// `ctx.runtime.bridge_async(...)`.
407    pub async fn bridge_async<T, F>(&self, task_name: &'static str, fut: F) -> Result<T, RuntimeError>
408    where
409        F: Future<Output = T> + Send + 'static,
410        T: Send + 'static,
411    {
412        self.check_sigint()?;
413        if std::process::id() != self.creation_pid {
414            return Err(RuntimeError::InvalidRuntime(format!(
415                "XetRuntime was created in process {} but is being used in process {}",
416                self.creation_pid,
417                std::process::id(),
418            )));
419        }
420        match &self.backend {
421            // Box to heap-erase the future's deep type; inlining it here overflows rustc's
422            // type-layout query-depth limit in callers. OwnedThreadPool erases via `spawn`.
423            RuntimeBackend::External { .. } => Ok(Box::pin(fut).await),
424            RuntimeBackend::OwnedThreadPool { .. } => self.bridge_to_owned(task_name, fut).await,
425        }
426    }
427
428    /// Run an async future synchronously, blocking the calling thread until completion.
429    ///
430    /// Only supported on **Owned** runtimes. Returns
431    /// [`RuntimeError::InvalidRuntime`] when called on an External-mode runtime.
432    ///
433    /// The caller must **not** be on a tokio worker thread (calling from
434    /// `spawn_blocking` threads, OS threads, or the main thread is fine).
435    ///
436    /// This is the primary sync entry point. Session-level `_blocking` methods
437    /// should simply call `ctx.runtime.bridge_sync(...)`.
438    pub fn bridge_sync<F>(&self, future: F) -> Result<F::Output, RuntimeError>
439    where
440        F: Future + Send + 'static,
441        F::Output: Send + 'static,
442    {
443        self.check_sigint()?;
444        if std::process::id() != self.creation_pid {
445            return Err(RuntimeError::InvalidRuntime(format!(
446                "XetRuntime was created in process {} but is being used in process {}",
447                self.creation_pid,
448                std::process::id(),
449            )));
450        }
451        if matches!(self.backend, RuntimeBackend::External { .. }) {
452            return Err(RuntimeError::InvalidRuntime(
453                "bridge_sync() cannot be called on an External-mode runtime; \
454                 use the async API instead"
455                    .into(),
456            ));
457        }
458
459        self.external_executor_count.fetch_add(1, Ordering::SeqCst);
460        let _executor_count_guard = CallbackGuard::new(|| {
461            self.external_executor_count.fetch_sub(1, Ordering::SeqCst);
462        });
463
464        let spawn_handle = self.handle();
465        self.handle()
466            .block_on(async move { spawn_handle.spawn(future).await.map_err(RuntimeError::from) })
467    }
468
469    /// Bridge a future onto this runtime's `hf-xet-*` thread pool from any async context,
470    /// including non-tokio executors (smol, async-std, `futures::executor::block_on`).
471    ///
472    /// Unlike [`bridge_sync`](Self::bridge_sync) which **blocks** the calling thread,
473    /// this method returns a future that any executor can poll.
474    /// The result is delivered via a `tokio::sync::oneshot` channel whose receiver only
475    /// requires a `std::task::Waker`, making it compatible with every standard executor.
476    ///
477    /// Returns `Err(RuntimeError::TaskPanic)` if the spawned future panics, or
478    /// `Err(RuntimeError::TaskCanceled)` if the runtime shuts down before the result
479    /// can be delivered.
480    async fn bridge_to_owned<T, F>(&self, task_name: &'static str, fut: F) -> Result<T, RuntimeError>
481    where
482        F: Future<Output = T> + Send + 'static,
483        T: Send + 'static,
484    {
485        let (tx, rx) = oneshot::channel();
486        self.spawn(async move {
487            let result = AssertUnwindSafe(fut).catch_unwind().await;
488            let _ = tx.send(result);
489        });
490        match rx.await {
491            Ok(Ok(value)) => Ok(value),
492            Ok(Err(panic_payload)) => {
493                let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
494                    format!("{task_name}: {s}")
495                } else if let Some(s) = panic_payload.downcast_ref::<String>() {
496                    format!("{task_name}: {s}")
497                } else {
498                    format!("{task_name}: <unknown panic>")
499                };
500                Err(RuntimeError::TaskPanic(msg))
501            },
502            Err(_) => Err(RuntimeError::TaskCanceled(task_name.to_string())),
503        }
504    }
505
506    #[inline]
507    fn runtime_cell_if_owned(&self) -> Option<&OwnedRuntimeCell> {
508        match &self.backend {
509            RuntimeBackend::OwnedThreadPool { runtime } => Some(runtime),
510            RuntimeBackend::External { .. } => None,
511        }
512    }
513
514    /// Spawn a blocking task on the runtime's blocking thread pool. Installs a weak thread-local
515    /// reference to this pool for the duration of `f`.
516    pub fn spawn_blocking<F, R>(self: &Arc<Self>, f: F) -> JoinHandle<R>
517    where
518        F: FnOnce() -> R + Send + 'static,
519        R: Send + 'static,
520    {
521        let pool_weak = Arc::downgrade(self);
522        self.handle().spawn_blocking(move || {
523            let pid = std::process::id();
524            THREAD_THREADPOOL_REF.set(Some((pid, pool_weak)));
525            f()
526        })
527    }
528
529    /// Returns the runtime mode (Owned or External).
530    #[inline]
531    pub fn mode(&self) -> RuntimeMode {
532        match &self.backend {
533            RuntimeBackend::External { .. } => RuntimeMode::External,
534            RuntimeBackend::OwnedThreadPool { .. } => RuntimeMode::Owned,
535        }
536    }
537
538    /// Wraps a caller-provided tokio handle after validating that it meets requirements.
539    pub fn from_validated_external(
540        rt_handle: TokioRuntimeHandle,
541        config: &XetConfig,
542    ) -> Result<Arc<Self>, RuntimeError> {
543        if !Self::handle_meets_requirements(&rt_handle) {
544            return Err(RuntimeError::InvalidRuntime(
545                "supplied tokio handle does not meet requirements \
546                 (missing drivers or wrong flavor)"
547                    .into(),
548            ));
549        }
550        Self::from_external_with_config(rt_handle, config)
551    }
552
553    /// Probe whether a tokio runtime handle meets the requirements for use as an
554    /// External-mode runtime.
555    ///
556    /// Checks:
557    /// 1. **Multi-threaded flavor**.
558    /// 2. **Time driver** -- required for timeouts, retry backoff, and progress intervals.
559    /// 3. **IO driver** -- required for all network I/O via `reqwest`/`hyper`.
560    ///
561    /// Driver availability is probed by entering the handle's context and polling a
562    /// driver-dependent future once inside `catch_unwind`. Tokio panics synchronously
563    /// on the first poll when a driver is absent, so the result is immediate.
564    ///
565    /// **Fragility note:** this probing technique relies on tokio panicking
566    /// synchronously on the first poll of `tokio::time::sleep` /
567    /// `tokio::net::TcpListener::bind` when the corresponding driver is absent.
568    /// This is undocumented internal behavior validated against tokio 1.x.
569    pub fn handle_meets_requirements(handle: &TokioRuntimeHandle) -> bool {
570        if matches!(handle.runtime_flavor(), tokio::runtime::RuntimeFlavor::CurrentThread) {
571            return false;
572        }
573
574        let _guard = handle.enter();
575        let waker = Waker::noop();
576        let mut cx = Context::from_waker(waker);
577
578        let has_time = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
579            let mut sleep = pin!(tokio::time::sleep(std::time::Duration::ZERO));
580            let _ = sleep.as_mut().poll(&mut cx);
581        }))
582        .is_ok();
583
584        let has_io = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
585            let mut bind = pin!(tokio::net::TcpListener::bind("127.0.0.1:0"));
586            let _ = bind.as_mut().poll(&mut cx);
587        }))
588        .is_ok();
589
590        has_time && has_io
591    }
592}
593
594impl Drop for XetRuntime {
595    fn drop(&mut self) {
596        #[cfg(feature = "fd-track")]
597        let _fd_scope = track_fd_scope("XetRuntime::drop");
598
599        self.handle_ref.take();
600
601        // Fork detection: if we are in a child process the Tokio worker threads from the
602        // parent do not exist here. shutdown_timeout() would block ~5 s waiting on futexes
603        // that never fire. Use discard_runtime() (std::mem::forget) instead — the OS
604        // reclaims all memory when the child exits.
605        if self.creation_pid != std::process::id() {
606            self.discard_runtime();
607            return;
608        }
609
610        if let RuntimeBackend::External { handle_id: Some(id) } = &self.backend {
611            if let Ok(mut reg) = EXTERNAL_THREADPOOL_REGISTRY.write() {
612                reg.remove(id);
613            }
614            return;
615        }
616
617        // When dropping from within an async context, the default TokioRuntime Drop
618        // would panic ("Cannot drop a runtime in a context where blocking is not allowed").
619        // Avoid this by taking ownership of the runtime and using shutdown_background(),
620        // which spawns a thread for the blocking shutdown work instead.
621        let in_async_context = TokioRuntimeHandle::try_current().is_ok();
622        if let RuntimeBackend::OwnedThreadPool { runtime } = &self.backend
623            && let Ok(mut guard) = runtime.write()
624            && let Some(rt_arc) = guard.take()
625            && let Ok(rt) = Arc::try_unwrap(rt_arc)
626        {
627            if in_async_context {
628                rt.shutdown_background();
629            } else {
630                rt.shutdown_timeout(std::time::Duration::from_secs(5));
631            }
632        }
633    }
634}
635
636impl Display for XetRuntime {
637    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638        let metrics = match &self.backend {
639            RuntimeBackend::External { .. } => self.handle().metrics(),
640            RuntimeBackend::OwnedThreadPool { runtime } => {
641                // Need to be careful that this doesn't acquire locks eagerly, as this function can be called
642                // from some weird places like displaying the backtrace of a panic or exception.
643                let Ok(runtime_rlg) = runtime.try_read() else {
644                    return write!(f, "Locked Tokio Runtime.");
645                };
646
647                let Some(ref runtime) = *runtime_rlg else {
648                    return write!(f, "Terminated Tokio Runtime Handle; cancel_all_and_shutdown called.");
649                };
650                runtime.metrics()
651            },
652        };
653
654        write!(
655            f,
656            "pool: num_workers: {:?}, num_alive_tasks: {:?}, global_queue_depth: {:?}",
657            metrics.num_workers(),
658            metrics.num_alive_tasks(),
659            metrics.global_queue_depth()
660        )
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::core::XetContext;
668
669    #[test]
670    fn test_bridge_async_owned_mode_runs_on_pool() {
671        let ctx = XetContext::default().unwrap();
672        assert_eq!(ctx.runtime.mode(), RuntimeMode::Owned);
673        let rt = ctx.runtime.clone();
674        let result = ctx
675            .runtime
676            .bridge_sync(async move { rt.bridge_async("test", async { 42 }).await.unwrap() });
677        assert_eq!(result.unwrap(), 42);
678    }
679
680    #[test]
681    fn test_bridge_async_external_mode_runs_directly() {
682        let tokio_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
683        let config = XetConfig::new();
684        let rt = XetRuntime::from_external_with_config(tokio_rt.handle().clone(), &config).unwrap();
685        let ctx = XetContext::new(config, rt);
686        assert_eq!(ctx.runtime.mode(), RuntimeMode::External);
687
688        let result = tokio_rt.block_on(async { ctx.runtime.bridge_async("test", async { 99 }).await.unwrap() });
689        assert_eq!(result, 99);
690    }
691
692    #[test]
693    fn test_bridge_sync_owned_mode() {
694        let ctx = XetContext::default().unwrap();
695        assert_eq!(ctx.runtime.mode(), RuntimeMode::Owned);
696        let result = ctx.runtime.bridge_sync(async { 123 }).unwrap();
697        assert_eq!(result, 123);
698    }
699
700    #[test]
701    fn test_default_reuses_owned_xet_runtime_from_tls() {
702        let parent = XetContext::default().unwrap();
703        let parent_runtime = parent.runtime.clone();
704        let parent_config = parent.config.clone();
705
706        let child = parent
707            .runtime
708            .bridge_sync(async move { XetContext::default().unwrap() })
709            .unwrap();
710
711        assert!(Arc::ptr_eq(&child.runtime, &parent_runtime));
712        assert!(!Arc::ptr_eq(&child.config, &parent_config));
713    }
714
715    #[test]
716    fn test_bridge_sync_from_spawn_blocking_owned_mode() {
717        let ctx = XetContext::default().unwrap();
718        let rt = ctx.runtime.clone();
719        let rt2 = ctx.runtime.clone();
720        let jh = rt.spawn_blocking(move || rt2.bridge_sync(async { 456 }).unwrap());
721        let result = ctx.runtime.bridge_sync(async { jh.await.unwrap() }).unwrap();
722        assert_eq!(result, 456);
723    }
724
725    #[test]
726    fn test_bridge_sync_external_mode_returns_error() {
727        let tokio_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
728        let config = XetConfig::new();
729        let rt = XetRuntime::from_external_with_config(tokio_rt.handle().clone(), &config).unwrap();
730        let ctx = XetContext::new(config, rt);
731        assert_eq!(ctx.runtime.mode(), RuntimeMode::External);
732
733        let result = ctx.runtime.bridge_sync(async { 789 });
734        assert!(matches!(result, Err(RuntimeError::InvalidRuntime(_))));
735    }
736
737    #[test]
738    fn test_handle_meets_requirements_multi_thread_all() {
739        let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
740        assert!(XetRuntime::handle_meets_requirements(rt.handle()));
741    }
742
743    #[test]
744    fn test_handle_meets_requirements_current_thread_rejected() {
745        let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
746        assert!(!XetRuntime::handle_meets_requirements(rt.handle()));
747    }
748
749    #[test]
750    fn test_handle_meets_requirements_no_drivers_rejected() {
751        let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
752        assert!(!XetRuntime::handle_meets_requirements(rt.handle()));
753    }
754
755    #[test]
756    fn test_from_validated_external_accepts_valid_handle() {
757        let tokio_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
758        let config = XetConfig::new();
759        let rt = XetRuntime::from_validated_external(tokio_rt.handle().clone(), &config).unwrap();
760        assert_eq!(rt.mode(), RuntimeMode::External);
761    }
762
763    #[test]
764    fn test_from_validated_external_rejects_current_thread_runtime() {
765        let tokio_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
766        let config = XetConfig::new();
767        let result = XetRuntime::from_validated_external(tokio_rt.handle().clone(), &config);
768        assert!(matches!(result, Err(RuntimeError::InvalidRuntime(_))));
769    }
770
771    #[test]
772    fn test_from_validated_external_rejects_runtime_without_drivers() {
773        let tokio_rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
774        let config = XetConfig::new();
775        let result = XetRuntime::from_validated_external(tokio_rt.handle().clone(), &config);
776        assert!(matches!(result, Err(RuntimeError::InvalidRuntime(_))));
777    }
778
779    #[test]
780    fn test_bridge_async_owned_mode_catches_panic() {
781        let ctx = XetContext::default().unwrap();
782        let rt = ctx.runtime.clone();
783        let result = ctx.runtime.bridge_sync(async move {
784            rt.bridge_async("panic_test", async {
785                panic!("intentional test panic");
786            })
787            .await
788        });
789        let err = result.unwrap().unwrap_err();
790        assert!(matches!(err, RuntimeError::TaskPanic(_)));
791    }
792
793    #[test]
794    fn test_context_config_preserved_through_external() {
795        let tokio_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
796        let mut config = XetConfig::new();
797        config.data.default_cas_endpoint = "https://test-endpoint.example.com".into();
798        let rt = XetRuntime::from_external(tokio_rt.handle().clone());
799        let ctx = XetContext::new(config, rt);
800        assert_eq!(ctx.config.data.default_cas_endpoint, "https://test-endpoint.example.com");
801    }
802
803    #[test]
804    fn test_check_sigint_shutdown_not_triggered() {
805        let ctx = XetContext::default().unwrap();
806        assert!(ctx.check_sigint_shutdown().is_ok());
807    }
808
809    #[test]
810    fn test_from_external_with_config_rejects_second_attach() {
811        let tokio_rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
812        let config = XetConfig::new();
813
814        let first = XetRuntime::from_external_with_config(tokio_rt.handle().clone(), &config).unwrap();
815        let second = XetRuntime::from_external_with_config(tokio_rt.handle().clone(), &config);
816
817        assert!(matches!(second, Err(RuntimeError::ExternalAlreadyAttached(_))));
818        drop(first);
819    }
820
821    #[test]
822    fn test_perform_sigint_shutdown_tolerates_poisoned_runtime_lock() {
823        let ctx = XetContext::default().unwrap();
824        let runtime = ctx.runtime.clone();
825        let runtime_cell = runtime.runtime_cell_if_owned().unwrap().clone();
826
827        let _ = std::thread::spawn(move || {
828            let _guard = runtime_cell.write().unwrap();
829            panic!("intentional poison for test");
830        })
831        .join();
832
833        let shutdown_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
834            runtime.perform_sigint_shutdown();
835        }));
836        assert!(shutdown_result.is_ok());
837    }
838
839    #[test]
840    fn test_sigint_shutdown_causes_keyboard_interrupt_on_bridges() {
841        let ctx = XetContext::default().unwrap();
842        ctx.runtime.perform_sigint_shutdown();
843
844        let sync_result = ctx.runtime.bridge_sync(async { 1 });
845        assert!(matches!(sync_result, Err(RuntimeError::KeyboardInterrupt)));
846
847        let tokio_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
848        let tp = ctx.runtime.clone();
849        let async_result = tokio_rt.block_on(async move { tp.bridge_async("sigint_test", async { 1 }).await });
850        assert!(matches!(async_result, Err(RuntimeError::KeyboardInterrupt)));
851    }
852
853    #[test]
854    fn test_concurrent_bridge_sync_stress() {
855        use std::sync::Barrier;
856
857        let ctx = XetContext::default().unwrap();
858        let n = 200;
859        let barrier = Arc::new(Barrier::new(n));
860        let sum = Arc::new(AtomicUsize::new(0));
861
862        let handles: Vec<_> = (0..n)
863            .map(|i| {
864                let tp = ctx.runtime.clone();
865                let barrier = barrier.clone();
866                let sum = sum.clone();
867                std::thread::spawn(move || {
868                    barrier.wait();
869                    let result = tp.bridge_sync(async move { i }).unwrap();
870                    sum.fetch_add(result, Ordering::Relaxed);
871                })
872            })
873            .collect();
874
875        for h in handles {
876            h.join().unwrap();
877        }
878
879        assert_eq!(sum.load(Ordering::Relaxed), (0..n).sum::<usize>());
880    }
881}