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