Skip to main content

query_runtime/runtime/
service_stream.rs

1//! Service-level CUDA stream pooling for native worker tasks.
2//!
3//! One `ServiceStreamPool` exists per installed device. The backend builds it in
4//! `DeviceRegistry::install_resolved` with the capacities from the device
5//! resource profile: `A_d` root slots, `A_d * S_d` auxiliary slots, and a
6//! physical `pool_size` that bounds how many cuDF runtimes (each owning one CUDA
7//! stream) exist at once. The capacity table in
8//! `website/docs/design/admission.mdx#device-profiles-and-grants` owns that
9//! contract; the lease and lane vocabulary comes from
10//! `website/docs/design/gpu-execution.mdx#cuda-streams-and-dependencies`.
11//!
12//! A lease is produced in two steps. `SlotRequest::take_or_register` takes a
13//! class permit and either an idle runtime or the right to create one, all
14//! under the pool mutex and without CUDA calls. `StreamSlot::into_lease` then
15//! builds the runtime on a thread where CUDA work is allowed; that build is
16//! the only CUDA call on the acquire path. Dropping a `StreamLease` runs no
17//! CUDA either: the runtime goes to the cleanup supervisor in
18//! `stream_cleanup`, which synchronizes the stream and only then returns the
19//! runtime to `idle` and releases the class permit.
20//!
21//! Accounting fails closed. `closed` is set once and never cleared, by an
22//! explicit close, by a stream synchronization failure, or by an abandoned
23//! cleanup. A closed pool rejects acquisition and destroys every returning
24//! runtime; none is idled. Acquisition errors carry a `reason` fact:
25//! `stream_pool_closed`, `stream_pool_wait_timeout`, or
26//! `device_health_rejected_stream_acquire`; shutdown timeouts report
27//! `stream_pool_idle_timeout` or `stream_pool_quiescence_timeout` together
28//! with the cleanup-deadline marker.
29
30use std::{
31    cell::Cell,
32    fmt,
33    future::pending,
34    mem,
35    pin::{Pin, pin},
36    sync::{
37        Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError,
38        atomic::{AtomicBool, AtomicU64, Ordering},
39    },
40    thread,
41    time::Instant,
42};
43#[cfg(any(test, feature = "test-hooks"))]
44use std::{
45    collections::HashMap,
46    sync::{OnceLock, Weak},
47};
48
49use cudf::{
50    DeviceId, Runtime,
51    runtime::{DeviceHealthSnapshot, DeviceHealthStatus, device_health_snapshot},
52};
53use query_planning::error::{QueryError, QueryErrorCode, Result};
54use tokio::{
55    sync::{Notify, futures::Notified},
56    time::sleep_until,
57};
58
59#[cfg(test)]
60use super::DeviceFacts;
61pub(crate) use super::stream_cleanup::StreamCleanupOutcome;
62use super::{
63    ExecutionCancellation, ExecutionContext,
64    context::{AttemptAuxiliaryPermit, QueryServiceResourceEpoch},
65    deadline::block_until,
66    stream_cleanup::{ServiceStreamTestProbeSlot, StreamCleanupCompletion, StreamCleanupSupervisor},
67};
68use crate::{
69    native_error::cudf_query_error,
70    observability::{
71        RuntimeEventContext, RuntimeObservationHandle, RuntimeObservationPayload, RuntimeObservationRecordClass,
72        RuntimeResourceDomain, RuntimeResourceId, RuntimeResourceRow, RuntimeStreamLeaseObservation,
73        RuntimeStreamLeaseScope, RuntimeTopologyRecord, StreamLeaseAcquisition, StreamLeaseLifecycleState,
74    },
75};
76
77/// Fallback and upper bound for the default service stream pool size.
78///
79/// The pool creates streams lazily. This value caps concurrency and does not
80/// set a startup allocation count. `default_service_stream_pool_size_for_device`
81/// uses this value as the fallback when device SM count detection is
82/// unavailable and as the upper bound when detection succeeds.
83#[cfg(any(test, feature = "test-hooks"))]
84pub(crate) const DEFAULT_SERVICE_STREAM_POOL_SIZE: usize = 64;
85static NEXT_STREAM_POOL_ID: AtomicU64 = AtomicU64::new(1);
86
87/// Returns the default service stream pool size for a CUDA device.
88///
89/// The default is `min(device_multiprocessor_count * 2, 64)`. If the
90/// multiprocessor count cannot be detected, this returns
91/// [`DEFAULT_SERVICE_STREAM_POOL_SIZE`]. The detected value is cached per
92/// device ordinal for the process lifetime.
93#[cfg(any(test, feature = "test-hooks"))]
94pub(crate) fn default_service_stream_pool_size_for_device(device: DeviceId) -> usize {
95    static DEFAULT_SIZES: OnceLock<Mutex<HashMap<usize, usize>>> = OnceLock::new();
96    let sizes = DEFAULT_SIZES.get_or_init(|| Mutex::new(HashMap::new()));
97    let mut sizes = match sizes.lock() {
98        Ok(sizes) => sizes,
99        Err(_error) => {
100            return default_service_stream_pool_size_from_sm_count(detect_device_multiprocessor_count(device));
101        },
102    };
103    *sizes
104        .entry(device.ordinal())
105        .or_insert_with(|| default_service_stream_pool_size_from_sm_count(detect_device_multiprocessor_count(device)))
106}
107
108#[cfg(any(test, feature = "test-hooks"))]
109pub(super) fn default_service_stream_pool_size_from_sm_count(sm_count: Option<usize>) -> usize {
110    sm_count
111        .filter(|count| *count > 0)
112        .and_then(|count| count.checked_mul(2))
113        .map(|size| size.min(DEFAULT_SERVICE_STREAM_POOL_SIZE))
114        .unwrap_or(DEFAULT_SERVICE_STREAM_POOL_SIZE)
115}
116
117#[cfg(test)]
118pub(super) fn default_service_stream_pool_size_from_device_facts(facts: &DeviceFacts) -> usize {
119    default_service_stream_pool_size_from_sm_count(facts.sm_count.and_then(nonzero_sm_count))
120}
121
122#[cfg(any(test, feature = "test-hooks"))]
123fn detect_device_multiprocessor_count(device: DeviceId) -> Option<usize> {
124    device.multiprocessor_count().ok().and_then(nonzero_sm_count)
125}
126
127#[cfg(any(test, feature = "test-hooks"))]
128pub(super) fn nonzero_sm_count(count: u32) -> Option<usize> {
129    (count > 0).then(|| usize::try_from(count).ok()).flatten()
130}
131
132/// State shared by every clone of one device pool and by its leases.
133///
134/// A `StreamLease` holds a pool clone, so this struct outlives every lease.
135/// Its `Drop` (in `stream_cleanup`) requests cleanup-supervisor shutdown,
136/// sound because no lease, and therefore no pending enqueue, can exist at
137/// that point.
138#[derive(Debug)]
139pub(super) struct ServiceStreamPoolInner {
140    pub(super) id: u64,
141    pub(super) device: DeviceId,
142    /// Bound on runtimes that exist at once, independent of the class
143    /// capacities. A request must pass its class check and find either an
144    /// idle runtime or `created < pool_size`.
145    pub(super) pool_size: usize,
146    pub(super) root_capacity: usize,
147    pub(super) aux_capacity: usize,
148    /// Protects every counter in the pool. `closed` is read again under this
149    /// lock before a slot is taken.
150    pub(super) state: Mutex<ServiceStreamPoolState>,
151    /// Wakes acquire waiters. A waiter calls `Notified::enable` while holding
152    /// `state`, so a release that follows its check cannot be missed.
153    pub(super) available: Notify,
154    /// Wakes shutdown and quiescence waiters; paired with `state`.
155    pub(super) state_changed: Condvar,
156    pub(super) next_lease_id: AtomicU64,
157    pub(super) cleanup_probe: ServiceStreamTestProbeSlot,
158    pub(super) cleanup_supervisor: Arc<StreamCleanupSupervisor>,
159    /// Set once, never cleared. Set by `close`, by a failed stream
160    /// synchronization, by an abandoned cleanup job, and by a cleanup enqueue
161    /// that finds the supervisor gone. Unsynchronized readers may race a
162    /// concurrent close; `take_or_register` rechecks it under `state`.
163    pub(super) closed: AtomicBool,
164    /// Set once by the process-exit hook before it joins the cleanup thread.
165    /// After that, `enqueue_cleanup` leaks its payload; CUDA destruction may
166    /// neither run nor be queued behind the joined thread.
167    pub(super) process_exit_started: AtomicBool,
168}
169
170#[cfg(any(test, feature = "test-hooks"))]
171type ServiceStreamPoolRegistry = HashMap<(usize, usize), Weak<ServiceStreamPoolInner>>;
172
173/// Resource class for bounded stream-pool admission.
174///
175/// For active-attempt limit `A_d` and per-attempt source-work bound `S_d`,
176/// each device reserves `A_d` root stream slots and `A_d * S_d` auxiliary
177/// stream slots. Auxiliary (child/source-read) acquisition can never consume
178/// a future root slot.
179#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
180pub(crate) enum StreamPermitClass {
181    /// Per-attempt root execution stream.
182    Root,
183    /// Fragment and source-read streams.
184    #[default]
185    Auxiliary,
186}
187
188/// Counters behind the pool mutex.
189///
190/// `created` counts runtimes that exist: idle, leased, or in cleanup. The
191/// quiescence waiters and the timeout error derive `active_leased_runtimes`
192/// as `created - idle.len() - pending_cleanups - active_cleanups`.
193#[derive(Debug, Default)]
194pub(super) struct ServiceStreamPoolState {
195    /// Runtimes whose stream was synchronized by the cleanup thread and that
196    /// may be leased again. Always empty once `closed` is set.
197    pub(super) idle: Vec<Arc<Runtime>>,
198    /// Incremented when a slot reserves the right to build a runtime, before
199    /// the build; decremented when a runtime is destroyed or the build fails.
200    pub(super) created: usize,
201    /// Root class permits held. A permit is taken with the slot and released by
202    /// the cleanup job after the stream is synchronized, so a new lease of the
203    /// class cannot start before the previous holder's work has drained.
204    pub(super) root_outstanding: usize,
205    /// Auxiliary class permits held, with the same lifetime as `root_outstanding`.
206    pub(super) aux_outstanding: usize,
207    /// Threads or tasks parked in an acquire. Shutdown waits for zero so a
208    /// late waiter cannot observe a destroyed pool.
209    pub(super) waiting_acquires: usize,
210    /// Cleanup jobs enqueued but not yet started by the cleanup thread.
211    pub(super) pending_cleanups: usize,
212    /// Cleanup jobs between `begin_accounting` and `finish_active_accounting`,
213    /// which includes the `before_quiescence` completion callback.
214    pub(super) active_cleanups: usize,
215    /// Streams whose synchronization failed. Nonzero implies `closed`; the
216    /// value is reported on `stream_pool_closed` errors.
217    pub(super) synchronization_failures: u64,
218}
219
220/// Service-level CUDA stream pool for short-lived native worker tasks.
221///
222/// The pool owns a bounded set of same-device cuDF runtimes. Each runtime
223/// carries one CUDA stream and the process-local device memory manager. Streams
224/// are created lazily up to `pool_size`; dropping a `StreamLease` returns the
225/// runtime to the pool for reuse. This avoids creating a CUDA stream for every
226/// bounded source-read worker while preserving the existing reservation-aware
227/// allocator contract.
228#[derive(Debug, Clone)]
229pub struct ServiceStreamPool {
230    pub(super) inner: Arc<ServiceStreamPoolInner>,
231}
232
233impl ServiceStreamPool {
234    /// Create a stream pool for `device`.
235    ///
236    /// `pool_size` is the maximum number of simultaneously leased streams.
237    /// Runtime/stream creation is lazy: construction records the limit but does
238    /// not allocate CUDA streams immediately.
239    pub fn new(device: DeviceId, pool_size: usize) -> Result<Self> {
240        // Direct constructors keep pre-partition semantics: one root slot for
241        // lifecycle work and an auxiliary class bounded only by the physical
242        // pool size. Backend-owned pools pass explicit `A_d`/`A_d * S_d`
243        // capacities from the device resource profile via
244        // [`Self::new_with_classes`].
245        Self::new_with_classes(device, pool_size, 1, pool_size)
246    }
247
248    /// Creates a pool with explicit root/auxiliary stream-slot capacities.
249    ///
250    /// `root_capacity` is `A_d` and `aux_capacity` is `A_d * S_d` when the
251    /// backend constructs the pool; `pool_size` bounds physical runtimes
252    /// separately and the profile resolver guarantees it covers both classes.
253    /// Construction spawns the cleanup thread but creates no CUDA stream. A
254    /// cleanup-thread spawn failure fails construction, because a pool
255    /// without that thread could not return any lease.
256    pub(crate) fn new_with_classes(
257        device: DeviceId,
258        pool_size: usize,
259        root_capacity: usize,
260        aux_capacity: usize,
261    ) -> Result<Self> {
262        validate_service_stream_pool_size(pool_size)?;
263        for (field, capacity) in [("root_capacity", root_capacity), ("aux_capacity", aux_capacity)] {
264            if capacity == 0 {
265                return Err(QueryError::new(
266                    QueryErrorCode::InvalidConfiguration,
267                    "query_engine.service_stream_pool",
268                    "service stream pool permit-class capacity must be greater than zero",
269                )
270                .with_fact("device_ordinal", device.ordinal())
271                .with_fact("field", field)
272                .with_fact("reason", "zero"));
273            }
274        }
275        let id = NEXT_STREAM_POOL_ID.fetch_add(1, Ordering::Relaxed);
276        let cleanup_probe = ServiceStreamTestProbeSlot::default();
277        #[cfg(test)]
278        cleanup_probe.install_next_pool_probe();
279        // The cleanup receiver is a construction resource. Do not publish a
280        // usable pool unless its owner thread was created successfully.
281        let cleanup_supervisor = StreamCleanupSupervisor::start(device, id, cleanup_probe.clone())?;
282        let inner = Arc::new(ServiceStreamPoolInner {
283            id,
284            device,
285            pool_size,
286            root_capacity,
287            aux_capacity,
288            state: Mutex::new(ServiceStreamPoolState::default()),
289            available: Notify::new(),
290            state_changed: Condvar::new(),
291            next_lease_id: AtomicU64::new(1),
292            cleanup_probe,
293            cleanup_supervisor: Arc::clone(&cleanup_supervisor),
294            closed: AtomicBool::new(false),
295            process_exit_started: AtomicBool::new(false),
296        });
297        cleanup_supervisor.attach_pool(Arc::downgrade(&inner));
298        Ok(Self { inner })
299    }
300
301    /// Return the process-global default pool for `device` and `pool_size`.
302    ///
303    /// This is the fallback used by native execution when a service has not
304    /// injected its own pool yet. Pools are keyed by `(device ordinal,
305    /// pool_size)` so differently sized service tests do not share a capacity
306    /// contract.
307    #[cfg(any(test, feature = "test-hooks"))]
308    pub(crate) fn for_device(device: DeviceId, pool_size: usize) -> Result<Self> {
309        validate_service_stream_pool_size(pool_size)?;
310        static POOLS: OnceLock<Mutex<ServiceStreamPoolRegistry>> = OnceLock::new();
311        let pools = POOLS.get_or_init(|| Mutex::new(HashMap::new()));
312        let mut pools = pools.lock().map_err(|_error| {
313            QueryError::new(
314                QueryErrorCode::Internal,
315                "query_engine.service_stream_pool.registry",
316                "service stream pool registry mutex was poisoned",
317            )
318        })?;
319        let key = (device.ordinal(), pool_size);
320        if let Some(inner) = pools.get(&key).and_then(Weak::upgrade) {
321            return Ok(Self { inner });
322        }
323        let pool = Self::new(device, pool_size)?;
324        pools.insert(key, Arc::downgrade(&pool.inner));
325        Ok(pool)
326    }
327
328    /// CUDA device served by this pool.
329    pub fn device(&self) -> DeviceId {
330        self.inner.device
331    }
332
333    /// Maximum number of simultaneously leased streams.
334    pub(crate) fn pool_size(&self) -> usize {
335        self.inner.pool_size
336    }
337
338    /// Permit-class counters for partition assertions.
339    #[cfg(test)]
340    pub(crate) fn class_counters(&self) -> (usize, usize, usize, usize) {
341        let state = self.inner.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
342        (state.root_outstanding, self.inner.root_capacity, state.aux_outstanding, self.inner.aux_capacity)
343    }
344
345    /// Wait until every runtime created by this pool has completed cleanup and
346    /// returned to the idle set.
347    ///
348    /// This is a quiescence boundary for benchmark warmup and graceful service
349    /// drain. It does not create runtimes that were never needed.
350    #[cfg(test)]
351    pub(crate) fn wait_until_idle(&self, deadline: Instant, operation: &'static str) -> Result<()> {
352        self.wait_until_quiescent(0, deadline, operation)
353    }
354
355    /// Prevent new acquisitions and destroy every currently idle runtime.
356    /// Leased runtimes are discarded after their deferred synchronization.
357    ///
358    /// This is the non-blocking close used by `QueryService::close`, which
359    /// must not wait behind an admission transaction. The flag store is what
360    /// closes the pool; destroying the idle set is opportunistic. When the
361    /// mutex is contended the idle runtimes stay in place and are destroyed by
362    /// a later `close_and_wait_until` or by the final pool drop.
363    pub(crate) fn close(&self, operation: &'static str) -> Result<()> {
364        self.inner.closed.store(true, Ordering::Release);
365        self.inner.available.notify_waiters();
366        let idle = match self.inner.state.try_lock() {
367            Ok(mut state) => {
368                let idle = mem::take(&mut state.idle);
369                state.created = state.created.saturating_sub(idle.len());
370                self.inner.state_changed.notify_all();
371                idle
372            },
373            Err(TryLockError::WouldBlock) => return Ok(()),
374            Err(TryLockError::Poisoned(_)) => {
375                return Err(QueryError::new(
376                    QueryErrorCode::Internal,
377                    operation,
378                    "service stream pool mutex was poisoned",
379                ));
380            },
381        };
382        // Stream destruction runs here, after the match arm released the mutex.
383        drop(idle);
384        Ok(())
385    }
386
387    /// Close this pool and wait until every lease and deferred cleanup has
388    /// completed, then explicitly stop and join the cleanup supervisor, under
389    /// the caller's absolute shutdown deadline.
390    ///
391    /// The wait ends when `created` is zero. A closed pool never idles a
392    /// runtime, so every lease returning through cleanup destroys one, and
393    /// zero means no runtime exists in any state. `waiting_acquires` must
394    /// also be zero so no parked acquirer wakes against a joined supervisor.
395    /// A supervisor that has already failed ends the wait early with its
396    /// terminal error. Repeated calls are allowed; a timed-out
397    /// call leaves the supervisor joinable for the next one.
398    pub(crate) fn close_and_wait_until(&self, deadline: Instant, operation: &'static str) -> Result<()> {
399        self.inner.closed.store(true, Ordering::Release);
400        self.inner.available.notify_waiters();
401        let mut state = self.lock_state_until(deadline, operation)?;
402        let idle = mem::take(&mut state.idle);
403        state.created = state.created.saturating_sub(idle.len());
404        // Runtime destruction is a CUDA call; the cleanup thread and any
405        // acquirer need the pool mutex meanwhile.
406        drop(state);
407        // Destroy the idle runtimes before the wait, so `created == 0` means none exists.
408        drop(idle);
409        self.inner.state_changed.notify_all();
410        let mut state = self.lock_state_until(deadline, operation)?;
411        loop {
412            self.inner.cleanup_supervisor.ensure_not_failed(operation)?;
413            if state.created == 0
414                && state.idle.is_empty()
415                && state.pending_cleanups == 0
416                && state.active_cleanups == 0
417                && state.waiting_acquires == 0
418            {
419                // Same rule as `deadline::mutex_lock_until`: a result reached
420                // after the deadline is not accepted as success, so a
421                // coordinator cannot overrun its budget on a lucky late wake.
422                if Instant::now() >= deadline {
423                    return Err(stream_pool_cleanup_timeout(operation, self, &state, 0));
424                }
425                break;
426            }
427            let remaining = deadline.saturating_duration_since(Instant::now());
428            if remaining.is_zero() {
429                return Err(stream_pool_cleanup_timeout(operation, self, &state, 0));
430            }
431            let (next, timeout) = self.inner.state_changed.wait_timeout(state, remaining).map_err(|_error| {
432                QueryError::new(
433                    QueryErrorCode::Internal,
434                    operation,
435                    "service stream pool mutex was poisoned while waiting for shutdown cleanup",
436                )
437            })?;
438            state = next;
439            if timeout.timed_out()
440                && Instant::now() >= deadline
441                && (state.created != 0
442                    || !state.idle.is_empty()
443                    || state.pending_cleanups != 0
444                    || state.active_cleanups != 0
445                    || state.waiting_acquires != 0)
446            {
447                return Err(stream_pool_cleanup_timeout(operation, self, &state, 0));
448            }
449        }
450        // The elected joiner takes this mutex in `notify_pool_waiters` after
451        // committing the terminal state; holding it here would deadlock.
452        drop(state);
453
454        // A zero counter snapshot only proves that no work is currently
455        // accounted. The supervisor is an owned resource in its own right, so
456        // close must signal it and elect a joiner to commit its outcome.
457        self.inner.cleanup_supervisor.request_shutdown(deadline, operation)?;
458        self.inner.cleanup_supervisor.wait_for_exit_until(deadline, operation)
459    }
460
461    /// Terminal process-exit path. Unlike normal API shutdown it has no
462    /// caller-selected timeout. Returning while this worker can still destroy
463    /// CUDA state would race libcudart finalization.
464    pub(super) fn close_and_join_for_process_exit(&self) {
465        // Ordinary shutdown waits for every lease because its destructor is
466        // part of the reusable-capacity contract. `std::process::exit` skips
467        // those destructors, so waiting for externally held leases here would
468        // deadlock the atexit hook. Mark the terminal boundary first: a lease
469        // racing this hook is leaked and does not run CUDA destruction after
470        // the cleanup supervisor has been joined.
471        self.inner.process_exit_started.store(true, Ordering::Release);
472        self.inner.closed.store(true, Ordering::Release);
473        self.inner.available.notify_waiters();
474        let idle = {
475            let mut state = self.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
476            let idle = mem::take(&mut state.idle);
477            state.created = state.created.saturating_sub(idle.len());
478            self.inner.state_changed.notify_all();
479            idle
480        };
481        // Destroyed outside the mutex and before the join: the cleanup thread
482        // may still need the mutex for a job it is finishing.
483        drop(idle);
484        self.inner.cleanup_supervisor.join_for_process_exit();
485    }
486
487    /// Wait until `expected_leased_runtimes` created runtimes remain
488    /// checked out and every other runtime has completed cleanup.
489    #[cfg(test)]
490    pub(crate) fn wait_until_quiescent(
491        &self,
492        expected_leased_runtimes: usize,
493        deadline: Instant,
494        operation: &'static str,
495    ) -> Result<()> {
496        if expected_leased_runtimes > self.pool_size() {
497            return Err(QueryError::new(
498                QueryErrorCode::InvalidPlan,
499                operation,
500                "expected leased runtime count exceeds the service stream pool size",
501            )
502            .with_fact("device_ordinal", self.device().ordinal())
503            .with_fact("stream_pool_size", self.pool_size())
504            .with_fact("expected_leased_runtimes", expected_leased_runtimes)
505            .with_fact("reason", "expected_leases_exceed_pool_size"));
506        }
507
508        let mut state = self.lock_state(operation)?;
509        loop {
510            self.inner.cleanup_supervisor.ensure_not_failed(operation)?;
511            let active_leased_runtimes = state
512                .created
513                .saturating_sub(state.idle.len())
514                .saturating_sub(state.pending_cleanups)
515                .saturating_sub(state.active_cleanups);
516            if active_leased_runtimes == expected_leased_runtimes
517                && state.pending_cleanups == 0
518                && state.active_cleanups == 0
519            {
520                drop(state);
521                self.inner.cleanup_supervisor.finalize_if_finished_until(deadline, operation)?;
522                return Ok(());
523            }
524            let remaining = deadline.saturating_duration_since(Instant::now());
525            if remaining.is_zero() {
526                return Err(stream_pool_cleanup_timeout(operation, self, &state, expected_leased_runtimes));
527            }
528            let (next, _timeout) = self.inner.state_changed.wait_timeout(state, remaining).map_err(|_error| {
529                QueryError::new(
530                    QueryErrorCode::Internal,
531                    operation,
532                    "service stream pool mutex was poisoned while waiting for cleanup",
533                )
534            })?;
535            state = next;
536        }
537    }
538
539    #[cfg(any(test, feature = "test-hooks"))]
540    #[doc(hidden)]
541    pub fn wait_until_acquire_queued_for_test(&self, deadline: Instant) {
542        let mut state = self
543            .lock_state("query_engine.service_stream_pool.test_wait_for_queued_acquire")
544            .expect("stream pool test state");
545        while state.waiting_acquires == 0 {
546            let remaining = deadline.saturating_duration_since(Instant::now());
547            assert!(!remaining.is_zero(), "stream acquire did not enter the contended wait");
548            let (next, timeout) =
549                self.inner.state_changed.wait_timeout(state, remaining).expect("stream pool test wait");
550            state = next;
551            assert!(!timeout.timed_out() || state.waiting_acquires > 0, "stream acquire wait timed out");
552        }
553    }
554
555    #[cfg(test)]
556    pub(crate) fn fail_next_synchronization_for_test(&self) {
557        self.inner.cleanup_probe.with_test_probe(|probe| {
558            probe.force_next_synchronization_failure.store(true, Ordering::Release);
559        });
560    }
561
562    #[cfg(test)]
563    fn force_next_cleanup_enqueue_fallback_for_test(&self) {
564        self.inner.cleanup_probe.with_test_probe(|probe| {
565            probe.force_next_cleanup_enqueue_fallback.store(true, Ordering::Release);
566        });
567    }
568
569    /// Acquire a CUDA stream/runtime lease, blocking when all pool slots are busy.
570    pub fn acquire(&self) -> Result<StreamLease> {
571        self.acquire_with_class(StreamPermitClass::Auxiliary)
572    }
573
574    /// Acquire a lease of the given permit class, blocking when the class is full.
575    pub(crate) fn acquire_with_class(&self, permit_class: StreamPermitClass) -> Result<StreamLease> {
576        self.acquire_inner(
577            None,
578            None,
579            "query_engine.service_stream_pool.acquire",
580            RuntimeObservationHandle::disabled(),
581            RuntimeStreamLeaseScope::default(),
582            permit_class,
583        )
584    }
585
586    /// Acquire a CUDA stream/runtime lease while observing cancellation and an
587    /// absolute wait deadline.
588    #[cfg(test)]
589    pub(crate) fn acquire_with_cancellation_deadline(
590        &self,
591        cancellation: &ExecutionCancellation,
592        deadline: Instant,
593        operation: &'static str,
594    ) -> Result<StreamLease> {
595        self.acquire_inner(
596            Some(cancellation),
597            Some(deadline),
598            operation,
599            RuntimeObservationHandle::disabled(),
600            RuntimeStreamLeaseScope::default(),
601            StreamPermitClass::Auxiliary,
602        )
603    }
604
605    /// Acquires a lease while recording host-side wait and lifecycle evidence.
606    #[cfg(test)]
607    pub(crate) fn acquire_observed_with_cancellation_deadline(
608        &self,
609        cancellation: &ExecutionCancellation,
610        deadline: Option<Instant>,
611        operation: &'static str,
612        observation: RuntimeObservationHandle,
613        scope: RuntimeStreamLeaseScope,
614    ) -> Result<StreamLease> {
615        self.acquire_observed_with_cancellation_deadline_and_class(
616            cancellation,
617            deadline,
618            operation,
619            observation,
620            scope,
621            StreamPermitClass::Auxiliary,
622        )
623    }
624
625    pub(crate) fn acquire_observed_with_cancellation_deadline_and_class(
626        &self,
627        cancellation: &ExecutionCancellation,
628        deadline: Option<Instant>,
629        operation: &'static str,
630        observation: RuntimeObservationHandle,
631        scope: RuntimeStreamLeaseScope,
632        permit_class: StreamPermitClass,
633    ) -> Result<StreamLease> {
634        self.acquire_inner(Some(cancellation), deadline, operation, observation, scope, permit_class)
635    }
636
637    pub(crate) fn acquire_observed_with_cancellation(
638        &self,
639        cancellation: &ExecutionCancellation,
640        operation: &'static str,
641        observation: RuntimeObservationHandle,
642        scope: RuntimeStreamLeaseScope,
643    ) -> Result<StreamLease> {
644        self.acquire_observed_with_cancellation_and_class(
645            cancellation,
646            operation,
647            observation,
648            scope,
649            StreamPermitClass::Auxiliary,
650        )
651    }
652
653    pub(crate) fn acquire_observed_with_cancellation_and_class(
654        &self,
655        cancellation: &ExecutionCancellation,
656        operation: &'static str,
657        observation: RuntimeObservationHandle,
658        scope: RuntimeStreamLeaseScope,
659        permit_class: StreamPermitClass,
660    ) -> Result<StreamLease> {
661        self.acquire_inner(Some(cancellation), None, operation, observation, scope, permit_class)
662    }
663
664    fn acquire_inner(
665        &self,
666        cancellation: Option<&ExecutionCancellation>,
667        deadline: Option<Instant>,
668        operation: &'static str,
669        observation: RuntimeObservationHandle,
670        scope: RuntimeStreamLeaseScope,
671        permit_class: StreamPermitClass,
672    ) -> Result<StreamLease> {
673        self.reserve_blocking(cancellation, deadline, operation, observation, scope, permit_class)?
674            .into_lease(operation)
675    }
676
677    /// Reserves one stream slot of `permit_class`, blocking the calling thread
678    /// until a slot frees, cancellation is requested, or a deadline passes.
679    /// Each of them wakes the thread directly.
680    ///
681    /// Callers are GPU pool workers and other threads that may sit inside a
682    /// Tokio task, so the wait goes through `deadline::block_until`, which
683    /// polls under `unconstrained`. A plain `block_on` of `Notified` would
684    /// spend the enclosing task's cooperative budget, and a `Notified` that
685    /// returns `Pending` on an exhausted budget defers its wake until the
686    /// worker yields to the scheduler, which a parked thread never does.
687    ///
688    /// `block_until` forbids Tokio timers in the future it polls, so the
689    /// cancellation's own deadline is folded into `wait_deadline` and
690    /// `cancelled_or_expired` is not awaited here. The loop re-runs the full check in
691    /// `take_or_register` after every wake, so which event woke the thread
692    /// does not decide the outcome.
693    fn reserve_blocking(
694        &self,
695        cancellation: Option<&ExecutionCancellation>,
696        deadline: Option<Instant>,
697        operation: &'static str,
698        observation: RuntimeObservationHandle,
699        scope: RuntimeStreamLeaseScope,
700        permit_class: StreamPermitClass,
701    ) -> Result<StreamSlot> {
702        let request = SlotRequest::new(self, cancellation, deadline, operation, observation, scope, permit_class);
703        let wait_deadline = match (deadline, cancellation.and_then(ExecutionCancellation::deadline)) {
704            (Some(deadline), Some(cancellation_deadline)) => Some(deadline.min(cancellation_deadline)),
705            (deadline, cancellation_deadline) => deadline.or(cancellation_deadline),
706        };
707        loop {
708            let available = self.inner.available.notified();
709            let mut available = pin!(available);
710            // `_waiting` keeps this thread counted in `waiting_acquires` for
711            // the whole park, so shutdown cannot complete underneath it.
712            let _waiting = match request.take_or_register(available.as_mut())? {
713                SlotOrWait::Slot(slot) => return Ok(slot),
714                SlotOrWait::Wait(waiting) => waiting,
715            };
716            let _ = block_until(
717                async {
718                    match cancellation {
719                        Some(cancellation) => tokio::select! {
720                            biased;
721                            () = available => {},
722                            () = cancellation.cancel_requested() => {},
723                        },
724                        None => available.await,
725                    }
726                },
727                wait_deadline,
728            );
729        }
730    }
731
732    /// Reserves one stream slot of `permit_class` without blocking the
733    /// caller's thread. The reservation creates no CUDA stream; the lease is
734    /// materialized from it where CUDA work is allowed.
735    ///
736    /// This runs on the host runtime (the query handle reserves auxiliary
737    /// slots before submitting a pool job), so Tokio timers are available and
738    /// the cancellation deadline is awaited through `cancelled_or_expired`.
739    /// Dropping the future mid-wait releases the waiter count through
740    /// `WaitingAcquire` and records the lease as cancelled through
741    /// `SlotRequest::drop`.
742    pub(crate) async fn reserve_async(
743        &self,
744        cancellation: &ExecutionCancellation,
745        deadline: Option<Instant>,
746        operation: &'static str,
747        observation: RuntimeObservationHandle,
748        scope: RuntimeStreamLeaseScope,
749        permit_class: StreamPermitClass,
750    ) -> Result<StreamSlot> {
751        let request = SlotRequest::new(self, Some(cancellation), deadline, operation, observation, scope, permit_class);
752        loop {
753            let available = self.inner.available.notified();
754            let mut available = pin!(available);
755            let _waiting = match request.take_or_register(available.as_mut())? {
756                SlotOrWait::Slot(slot) => return Ok(slot),
757                SlotOrWait::Wait(waiting) => waiting,
758            };
759            let deadline_elapsed = async {
760                match deadline {
761                    Some(deadline) => sleep_until(deadline.into()).await,
762                    None => pending().await,
763                }
764            };
765            tokio::select! {
766                biased;
767                () = available => {},
768                () = cancellation.cancelled_or_expired() => {},
769                () = deadline_elapsed => {},
770            }
771        }
772    }
773
774    fn build_runtime(&self) -> Result<Runtime> {
775        Runtime::builder().with_device(self.device()).with_device_memory_manager().build().map_err(|error| {
776            cudf_query_error(
777                "query_engine.service_stream_pool.runtime",
778                "failed to initialize service stream pool runtime",
779                error,
780            )
781        })
782    }
783
784    fn lock_state(&self, operation: &'static str) -> Result<MutexGuard<'_, ServiceStreamPoolState>> {
785        self.inner.state.lock().map_err(|_error| {
786            QueryError::new(QueryErrorCode::Internal, operation, "service stream pool mutex was poisoned")
787        })
788    }
789
790    /// Takes the pool mutex without letting contention extend past
791    /// `deadline`; on the shutdown paths a stuck holder then produces a typed
792    /// timeout where a plain lock would block without bound. A poisoned mutex
793    /// is reported as the ordinary poison error whatever the deadline.
794    fn lock_state_until(
795        &self,
796        deadline: Instant,
797        operation: &'static str,
798    ) -> Result<MutexGuard<'_, ServiceStreamPoolState>> {
799        loop {
800            match self.inner.state.try_lock() {
801                Ok(state) => return Ok(state),
802                Err(TryLockError::Poisoned(_)) => return self.lock_state(operation),
803                Err(TryLockError::WouldBlock) if Instant::now() >= deadline => {
804                    return Err(QueryError::new(
805                        QueryErrorCode::ServiceOverloaded,
806                        operation,
807                        "timed out acquiring service stream pool state during shutdown",
808                    )
809                    .with_fact("reason", "stream_pool_idle_timeout")
810                    .with_fact("device_ordinal", self.device().ordinal()));
811                },
812                Err(TryLockError::WouldBlock) => thread::yield_now(),
813            }
814        }
815    }
816
817    /// Returns a `SlotAction::Create` reservation whose runtime was never
818    /// leased: the physical count and the class permit both go back, and
819    /// waiters are woken because either may have been the limiting resource.
820    fn discard_created_slot(&self, permit_class: StreamPermitClass) {
821        match self.inner.state.lock() {
822            Ok(mut state) => {
823                state.created = state.created.saturating_sub(1);
824                match permit_class {
825                    StreamPermitClass::Root => state.root_outstanding = state.root_outstanding.saturating_sub(1),
826                    StreamPermitClass::Auxiliary => state.aux_outstanding = state.aux_outstanding.saturating_sub(1),
827                }
828                self.inner.available.notify_waiters();
829                self.inner.state_changed.notify_all();
830            },
831            Err(_error) => {
832                eprintln!("[query-engine] warning: failed to update poisoned service stream pool state");
833            },
834        }
835    }
836}
837
838/// One reservation attempt against a pool. A request dropped before it hands
839/// out a slot, such as an abandoned async reservation, records its lease as
840/// cancelled.
841struct SlotRequest<'a> {
842    pool: &'a ServiceStreamPool,
843    cancellation: Option<&'a ExecutionCancellation>,
844    deadline: Option<Instant>,
845    operation: &'static str,
846    permit_class: StreamPermitClass,
847    observation: Arc<StreamLeaseObservation>,
848    enqueued_at: Instant,
849    handed_out: Cell<bool>,
850}
851
852enum SlotOrWait<'a> {
853    Slot(StreamSlot),
854    Wait(WaitingAcquire<'a>),
855}
856
857/// Counts one waiting acquire in the pool state until it drops, including
858/// when an async reservation is abandoned mid-wait; shutdown waits for the
859/// count to reach zero.
860struct WaitingAcquire<'a> {
861    pool: &'a ServiceStreamPool,
862}
863
864impl Drop for WaitingAcquire<'_> {
865    fn drop(&mut self) {
866        let mut state = self.pool.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
867        state.waiting_acquires = state.waiting_acquires.saturating_sub(1);
868        self.pool.inner.state_changed.notify_all();
869    }
870}
871
872impl<'a> SlotRequest<'a> {
873    fn new(
874        pool: &'a ServiceStreamPool,
875        cancellation: Option<&'a ExecutionCancellation>,
876        deadline: Option<Instant>,
877        operation: &'static str,
878        observation: RuntimeObservationHandle,
879        scope: RuntimeStreamLeaseScope,
880        permit_class: StreamPermitClass,
881    ) -> Self {
882        let observation = Arc::new(StreamLeaseObservation::new(pool, observation, scope));
883        observation.define_resource();
884        observation.record(StreamLeaseLifecycleState::Enqueued);
885        Self {
886            pool,
887            cancellation,
888            deadline,
889            operation,
890            permit_class,
891            observation,
892            enqueued_at: Instant::now(),
893            handed_out: Cell::new(false),
894        }
895    }
896
897    /// Takes a slot if one is free. Otherwise enables `available` while the
898    /// pool lock is held, so a release after this check wakes the waiter,
899    /// and counts the waiter while the returned registration lives.
900    fn take_or_register(&self, available: Pin<&mut Notified<'_>>) -> Result<SlotOrWait<'a>> {
901        let pool = self.pool;
902        let operation = self.operation;
903        if pool.inner.closed.load(Ordering::Acquire) {
904            return Err(self.fail(stream_pool_closed(operation, pool)));
905        }
906        if let Some(cancellation) = self.cancellation
907            && let Err(error) = cancellation.check(operation)
908        {
909            self.observation.record(StreamLeaseLifecycleState::CancellationRequested);
910            self.observation.terminal(StreamLeaseLifecycleState::Cancelled);
911            return Err(error);
912        }
913        if let Some(deadline) = self.deadline
914            && Instant::now() >= deadline
915        {
916            return Err(self.fail(stream_acquire_timeout_error(operation, pool.device(), pool.pool_size())));
917        }
918        let mut state = pool.lock_state(operation)?;
919        // Rechecked under the mutex. A request that passes here holds its
920        // slot before `close` can take the idle set, and its lease is then
921        // destroyed by cleanup, never idled; a request that arrives after
922        // the flag store fails even if idle runtimes are still present.
923        if pool.inner.closed.load(Ordering::Acquire) {
924            return Err(self.fail(stream_pool_closed(operation, pool)));
925        }
926        // Reusing an idle runtime on a faulted device is as unsafe as
927        // creating one, so the health check precedes both actions.
928        let health = device_health_snapshot(pool.device());
929        if health.status() == DeviceHealthStatus::Unhealthy {
930            return Err(self.fail(unhealthy_stream_device_error(operation, pool.device(), health)));
931        }
932        // The class check comes first: a full class waits even while idle
933        // runtimes or physical capacity are available to the other class.
934        let class_open = match self.permit_class {
935            StreamPermitClass::Root => state.root_outstanding < pool.inner.root_capacity,
936            StreamPermitClass::Auxiliary => state.aux_outstanding < pool.inner.aux_capacity,
937        };
938        let action = if !class_open {
939            None
940        } else if let Some(runtime) = state.idle.pop() {
941            Some(SlotAction::Reuse(runtime))
942        } else if state.created < pool.pool_size() {
943            // The physical slot is reserved here; the CUDA stream is built
944            // outside the mutex in `StreamSlot::into_lease`.
945            state.created += 1;
946            Some(SlotAction::Create)
947        } else {
948            None
949        };
950        if let Some(action) = action {
951            match self.permit_class {
952                StreamPermitClass::Root => state.root_outstanding += 1,
953                StreamPermitClass::Auxiliary => state.aux_outstanding += 1,
954            }
955            pool.inner.state_changed.notify_all();
956            self.handed_out.set(true);
957            return Ok(SlotOrWait::Slot(StreamSlot {
958                pool: pool.clone(),
959                action: Some(action),
960                permit_class: self.permit_class,
961                observation: Arc::clone(&self.observation),
962                enqueued_at: self.enqueued_at,
963            }));
964        }
965        available.enable();
966        state.waiting_acquires = state.waiting_acquires.saturating_add(1);
967        pool.inner.state_changed.notify_all();
968        Ok(SlotOrWait::Wait(WaitingAcquire { pool }))
969    }
970
971    fn fail(&self, error: QueryError) -> QueryError {
972        self.observation.terminal(StreamLeaseLifecycleState::Failed { error_code: error.code().to_string() });
973        error
974    }
975}
976
977impl Drop for SlotRequest<'_> {
978    fn drop(&mut self) {
979        if !self.handed_out.get() {
980            self.observation.terminal(StreamLeaseLifecycleState::Cancelled);
981        }
982    }
983}
984
985enum SlotAction {
986    Reuse(Arc<Runtime>),
987    Create,
988}
989
990/// A reserved stream slot: an idle runtime to reuse, or the right to create
991/// one. [`Self::into_lease`] turns it into a lease; dropping it unused returns
992/// the slot to the pool.
993pub(crate) struct StreamSlot {
994    pool: ServiceStreamPool,
995    action: Option<SlotAction>,
996    permit_class: StreamPermitClass,
997    observation: Arc<StreamLeaseObservation>,
998    enqueued_at: Instant,
999}
1000
1001impl fmt::Debug for StreamSlot {
1002    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1003        formatter.debug_struct("StreamSlot").field("permit_class", &self.permit_class).finish_non_exhaustive()
1004    }
1005}
1006
1007impl StreamSlot {
1008    /// Leases the reserved runtime, creating its CUDA stream when the slot
1009    /// carries the right to create one.
1010    ///
1011    /// This is the one CUDA call on the acquire path, so it must run on a
1012    /// thread where CUDA work is allowed. On any failure the reservation is
1013    /// returned to the pool and the lease observation records `Failed`.
1014    pub(crate) fn into_lease(mut self, operation: &'static str) -> Result<StreamLease> {
1015        let pool = self.pool.clone();
1016        let (runtime, acquisition) = match self.action.take().expect("an unconsumed slot holds its action") {
1017            SlotAction::Reuse(runtime) => (runtime, StreamLeaseAcquisition::Reused),
1018            SlotAction::Create => match pool.build_runtime() {
1019                Ok(runtime) if !pool.inner.closed.load(Ordering::Acquire) => {
1020                    (Arc::new(runtime), StreamLeaseAcquisition::Created)
1021                },
1022                // The pool closed while the stream was being built. The
1023                // runtime never carried work, so it is destroyed here rather
1024                // than routed through cleanup.
1025                Ok(runtime) => {
1026                    pool.discard_created_slot(self.permit_class);
1027                    // Return the slot before the stream destroy so a waiter
1028                    // is not delayed by this CUDA call.
1029                    drop(runtime);
1030                    let error = stream_pool_closed(operation, &pool);
1031                    self.observation
1032                        .terminal(StreamLeaseLifecycleState::Failed { error_code: error.code().to_string() });
1033                    return Err(error);
1034                },
1035                Err(error) => {
1036                    pool.discard_created_slot(self.permit_class);
1037                    self.observation
1038                        .terminal(StreamLeaseLifecycleState::Failed { error_code: error.code().to_string() });
1039                    return Err(error);
1040                },
1041            },
1042        };
1043        self.observation.record(StreamLeaseLifecycleState::Acquired {
1044            acquisition,
1045            host_wait_ns: u64::try_from(self.enqueued_at.elapsed().as_nanos()).unwrap_or(u64::MAX),
1046        });
1047        Ok(StreamLease {
1048            pool,
1049            runtime: Some(runtime),
1050            observation: Some(Arc::clone(&self.observation)),
1051            resource_epoch: None,
1052            cleanup_completion: Mutex::new(None),
1053            permit_class: self.permit_class,
1054        })
1055    }
1056}
1057
1058impl Drop for StreamSlot {
1059    /// Returns an unconsumed reservation. Even a reused runtime returns through
1060    /// cleanup, so the class permit and the idle return follow the single
1061    /// accounting path in `stream_cleanup`.
1062    fn drop(&mut self) {
1063        let Some(action) = self.action.take() else { return };
1064        self.observation.terminal(StreamLeaseLifecycleState::Cancelled);
1065        match action {
1066            SlotAction::Reuse(runtime) => self.pool.enqueue_cleanup(runtime, None, None, self.permit_class),
1067            SlotAction::Create => self.pool.discard_created_slot(self.permit_class),
1068        }
1069    }
1070}
1071
1072fn stream_acquire_timeout_error(operation: &'static str, device: DeviceId, pool_size: usize) -> QueryError {
1073    QueryError::new(QueryErrorCode::ServiceOverloaded, operation, "service stream pool acquire timed out")
1074        .with_fact("reason", "stream_pool_wait_timeout")
1075        .with_fact("device_ordinal", device.ordinal())
1076        .with_fact("stream_pool_size", pool_size)
1077}
1078
1079fn unhealthy_stream_device_error(
1080    operation: &'static str,
1081    device: DeviceId,
1082    health: DeviceHealthSnapshot,
1083) -> QueryError {
1084    let mut error = QueryError::new(
1085        QueryErrorCode::CudfPersistentFailure,
1086        operation,
1087        "service stream pool device is unhealthy; runtime creation and reuse are rejected",
1088    )
1089    .with_fact("reason", "device_health_rejected_stream_acquire")
1090    .with_fact("device_ordinal", device.ordinal())
1091    .with_fact("device_fault_count", health.fault_count());
1092    if let Some(first_fault) = health.first_fault() {
1093        error = error
1094            .with_fact("first_device_fault_kind", format!("{:?}", first_fault.kind()))
1095            .with_fact("first_device_fault_operation", first_fault.operation());
1096    }
1097    error
1098}
1099
1100fn stream_pool_closed(operation: &'static str, pool: &ServiceStreamPool) -> QueryError {
1101    // Built on error paths that may already hold the pool mutex, so the
1102    // failure count is attached only when the lock is free.
1103    let synchronization_failures = pool.inner.state.try_lock().ok().map(|state| state.synchronization_failures);
1104    let error = QueryError::new(QueryErrorCode::Cancelled, operation, "service stream pool is closed")
1105        .with_fact("reason", "stream_pool_closed")
1106        .with_fact("device_ordinal", pool.device().ordinal())
1107        .with_fact("stream_pool_size", pool.pool_size());
1108    match synchronization_failures {
1109        Some(failures) => error.with_fact("synchronization_failures", failures),
1110        None => error,
1111    }
1112}
1113
1114fn stream_pool_cleanup_timeout(
1115    operation: &'static str,
1116    pool: &ServiceStreamPool,
1117    state: &ServiceStreamPoolState,
1118    expected_leased_runtimes: usize,
1119) -> QueryError {
1120    let active_leased_runtimes = state
1121        .created
1122        .saturating_sub(state.idle.len())
1123        .saturating_sub(state.pending_cleanups)
1124        .saturating_sub(state.active_cleanups);
1125    let reason =
1126        if expected_leased_runtimes == 0 { "stream_pool_idle_timeout" } else { "stream_pool_quiescence_timeout" };
1127    QueryError::new(QueryErrorCode::ServiceOverloaded, operation, "timed out waiting for service stream pool cleanup")
1128        .with_fact("device_ordinal", pool.device().ordinal())
1129        .with_fact("stream_pool_size", pool.pool_size())
1130        .with_fact("created_runtimes", state.created)
1131        .with_fact("idle_runtimes", state.idle.len())
1132        .with_fact("active_leased_runtimes", active_leased_runtimes)
1133        .with_fact("pending_cleanups", state.pending_cleanups)
1134        .with_fact("active_cleanups", state.active_cleanups)
1135        .with_fact("waiting_acquires", state.waiting_acquires)
1136        .with_fact("synchronization_failures", state.synchronization_failures)
1137        .with_fact("expected_leased_runtimes", expected_leased_runtimes)
1138        .with_fact("reason", reason)
1139        .with_cleanup_deadline_elapsed()
1140}
1141
1142/// Observation identity of one lease attempt, from `Enqueued` to its terminal
1143/// state. Shared by the `SlotRequest`, the `StreamSlot`, and the `StreamLease`
1144/// so that whichever of them ends the attempt records one terminal event.
1145#[derive(Debug)]
1146struct StreamLeaseObservation {
1147    observation: RuntimeObservationHandle,
1148    lease_id: RuntimeResourceId,
1149    device_ordinal: u32,
1150    scope: RuntimeStreamLeaseScope,
1151    /// Set by the first `terminal` call; later calls are no-ops.
1152    terminal_recorded: AtomicBool,
1153}
1154
1155impl StreamLeaseObservation {
1156    fn new(pool: &ServiceStreamPool, observation: RuntimeObservationHandle, scope: RuntimeStreamLeaseScope) -> Self {
1157        let sequence = pool.inner.next_lease_id.fetch_add(1, Ordering::Relaxed);
1158        Self {
1159            observation,
1160            lease_id: RuntimeResourceId::new(format!("stream-pool-{}-lease-{sequence}", pool.inner.id)),
1161            device_ordinal: u32::try_from(pool.device().ordinal()).unwrap_or(u32::MAX),
1162            scope,
1163            terminal_recorded: AtomicBool::new(false),
1164        }
1165    }
1166
1167    fn record(&self, lifecycle: StreamLeaseLifecycleState) {
1168        let context = self
1169            .scope
1170            .plan_execution_id
1171            .clone()
1172            .map_or_else(RuntimeEventContext::default, RuntimeEventContext::for_plan);
1173        self.observation.record_with_context(context, || {
1174            RuntimeObservationPayload::StreamLease(Box::new(RuntimeStreamLeaseObservation {
1175                lease_id: self.lease_id.clone(),
1176                device_ordinal: self.device_ordinal,
1177                scope: self.scope.clone(),
1178                lifecycle,
1179            }))
1180        });
1181    }
1182
1183    fn define_resource(&self) {
1184        self.observation.record_topology(RuntimeObservationRecordClass::Resource, || {
1185            RuntimeTopologyRecord::Resource(RuntimeResourceRow {
1186                plan_execution_id: self.scope.plan_execution_id.clone(),
1187                resource_id: self.lease_id.clone(),
1188                resource_kind: "stream_lease".to_string(),
1189                domain: RuntimeResourceDomain::Device { device_ordinal: self.device_ordinal },
1190                producer_task_id: None,
1191                consumer_task_ids: Vec::new(),
1192                lifetime: "lease".to_string(),
1193            })
1194        });
1195    }
1196
1197    fn terminal(&self, lifecycle: StreamLeaseLifecycleState) {
1198        if !self.terminal_recorded.swap(true, Ordering::AcqRel) {
1199            self.record(lifecycle);
1200        }
1201    }
1202}
1203
1204/// RAII handle for a runtime borrowed from the backend-owned stream pool.
1205///
1206/// Dropping the lease synchronously records that query ownership was released,
1207/// then schedules runtime stream synchronization on the service cleanup thread.
1208/// The runtime returns to the pool only after synchronization completes. This
1209/// keeps source-read output safe to consume on another runtime before the stream
1210/// is reused by an unrelated worker without running CUDA synchronization on the
1211/// dropping thread.
1212pub struct StreamLease {
1213    pool: ServiceStreamPool,
1214    /// `None` only after `Drop` has moved the runtime into cleanup.
1215    runtime: Option<Arc<Runtime>>,
1216    observation: Option<Arc<StreamLeaseObservation>>,
1217    /// Query-service epoch this lease is bound to, when it was obtained
1218    /// through an admitted context. `runtime()` refuses to derive runtimes
1219    /// once the epoch closes, and the cleanup job retains the epoch until the
1220    /// runtime is destroyed.
1221    resource_epoch: Option<Arc<QueryServiceResourceEpoch>>,
1222    /// At most one completion, installed through `&self` because the lease
1223    /// is already shared by its execution contexts when the release path
1224    /// installs it.
1225    cleanup_completion: Mutex<Option<StreamCleanupCompletion>>,
1226    permit_class: StreamPermitClass,
1227}
1228
1229impl fmt::Debug for StreamLease {
1230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1231        f.debug_struct("StreamLease")
1232            .field("device_ordinal", &self.pool.device().ordinal())
1233            .field("has_runtime", &self.runtime.is_some())
1234            .field("has_resource_epoch", &self.resource_epoch.is_some())
1235            .field(
1236                "has_cleanup_completion",
1237                &self.cleanup_completion.lock().map(|completion| completion.is_some()).unwrap_or(true),
1238            )
1239            .finish()
1240    }
1241}
1242
1243impl StreamLease {
1244    pub(super) fn pool(&self) -> &ServiceStreamPool {
1245        &self.pool
1246    }
1247
1248    pub(crate) fn with_resource_epoch(mut self, resource_epoch: Arc<QueryServiceResourceEpoch>) -> Self {
1249        self.resource_epoch = Some(resource_epoch);
1250        self
1251    }
1252
1253    /// Installs the callbacks that the cleanup thread runs after this lease's
1254    /// stream has been synchronized. Returns `false`, and runs nothing, when a
1255    /// completion is already installed.
1256    ///
1257    /// `before_quiescence` receives the synchronization outcome and runs while
1258    /// the cleanup still counts as active, so a quiescence waiter cannot pass
1259    /// until it returns; the attempt's report commit and capacity release
1260    /// happen there. `after_quiescence` runs once the counters are released.
1261    /// Both run on the cleanup thread, or on the dropping thread when the
1262    /// enqueue falls back to synchronous cleanup. If the cleanup thread dies
1263    /// first, `before_quiescence` still runs with `CallbackAbandoned`.
1264    pub(crate) fn install_cleanup_completion(
1265        &self,
1266        before_quiescence: impl FnOnce(StreamCleanupOutcome) + Send + 'static,
1267        after_quiescence: impl FnOnce() + Send + 'static,
1268    ) -> bool {
1269        // Materialize the handoff before inspecting the slot. A duplicate
1270        // installation is an ownership invariant violation; the rejected
1271        // completion is dropped, which reports `CallbackAbandoned` and runs
1272        // its tail, so the report and health work it carried is not lost and
1273        // the query's capacity is not released without a terminal commit.
1274        let completion = StreamCleanupCompletion::new(before_quiescence, after_quiescence);
1275        let mut slot = match self.cleanup_completion.lock() {
1276            Ok(slot) => slot,
1277            Err(error) => error.into_inner(),
1278        };
1279        if slot.is_some() {
1280            return false;
1281        }
1282        *slot = Some(completion);
1283        true
1284    }
1285
1286    pub(crate) fn device(&self) -> DeviceId {
1287        self.runtime.as_ref().expect("stream lease runtime is present").device()
1288    }
1289
1290    /// Clone a counted cuDF runtime derived from this lease.
1291    ///
1292    /// Fails once the bound query-service epoch has closed. The returned
1293    /// runtime retains that epoch, and so does every cuDF value produced from
1294    /// it, which keeps the service generation alive while GPU state derived
1295    /// from this stream exists.
1296    pub fn runtime(&self) -> Result<Runtime> {
1297        let runtime = self.runtime.as_ref().expect("stream lease runtime is present").as_ref().clone();
1298        let Some(resource_epoch) = &self.resource_epoch else {
1299            return Ok(runtime);
1300        };
1301        resource_epoch.check_open("query_engine.service_stream_pool.lease.runtime")?;
1302        let retained: Arc<dyn Send + Sync> = Arc::clone(resource_epoch) as _;
1303        runtime.with_retained_lifetime(retained).map_err(|error| {
1304            cudf_query_error(
1305                "query_engine.service_stream_pool.lease.runtime",
1306                "failed to retain query-service lifetime for leased runtime",
1307                error,
1308            )
1309        })
1310    }
1311
1312    /// Clone the shared runtime handle for a short-lived execution context.
1313    pub fn shared_runtime(&self) -> Result<Arc<Runtime>> {
1314        self.runtime().map(Arc::new)
1315    }
1316}
1317
1318impl Drop for StreamLease {
1319    /// Runs no CUDA call. The lease may be dropped on a host-runtime task, a
1320    /// pool worker, or inside another `Drop`; the stream synchronization that
1321    /// must precede reuse happens on the cleanup thread.
1322    fn drop(&mut self) {
1323        let Some(runtime) = self.runtime.take() else {
1324            return;
1325        };
1326        if let Some(observation) = self.observation.take() {
1327            observation.terminal(StreamLeaseLifecycleState::Released);
1328        }
1329        let cleanup_completion = match self.cleanup_completion.get_mut() {
1330            Ok(completion) => completion.take(),
1331            Err(error) => error.into_inner().take(),
1332        };
1333        self.pool.enqueue_cleanup(runtime, self.resource_epoch.take(), cleanup_completion, self.permit_class);
1334    }
1335}
1336
1337/// One unit of auxiliary attempt work: its attempt auxiliary permit, its
1338/// service stream lease, and the execution context derived from both.
1339///
1340/// Execution lanes and scheduled source reads hold the same lease; a lane's
1341/// context additionally carries that lane's fork subset.
1342pub struct AttemptWorkLease {
1343    context: ExecutionContext,
1344    _stream_lease: StreamLease,
1345    // Drop after the derived context and global stream lease so the
1346    // attempt-local envelope remains charged for every actual auxiliary
1347    // stream lifetime.
1348    _attempt_auxiliary_permit: Option<AttemptAuxiliaryPermit>,
1349}
1350
1351impl AttemptWorkLease {
1352    pub(super) fn new(
1353        context: ExecutionContext,
1354        stream_lease: StreamLease,
1355        attempt_auxiliary_permit: Option<AttemptAuxiliaryPermit>,
1356    ) -> Self {
1357        Self { context, _stream_lease: stream_lease, _attempt_auxiliary_permit: attempt_auxiliary_permit }
1358    }
1359
1360    #[doc(hidden)]
1361    pub fn context(&self) -> &ExecutionContext {
1362        &self.context
1363    }
1364}
1365
1366pub(super) fn validate_service_stream_pool_size(pool_size: usize) -> Result<()> {
1367    if pool_size == 0 {
1368        return Err(QueryError::new(
1369            QueryErrorCode::InvalidConfiguration,
1370            "query_engine.service_stream_pool.validate",
1371            "service stream pool size must be greater than zero",
1372        )
1373        .with_fact("field", "service_stream_pool_size")
1374        .with_fact("reason", "zero"));
1375    }
1376    Ok(())
1377}
1378
1379#[cfg(test)]
1380mod tests {
1381    use std::{
1382        sync::{Barrier, mpsc},
1383        time::Duration,
1384    };
1385
1386    use cudf::{
1387        runtime::{DeviceFaultKind, DeviceFaultNativeStatus, report_device_fault},
1388        testing::device_safety::DeviceSafetyTestScope,
1389    };
1390    use query_planning::error::QueryErrorStatus;
1391
1392    use super::*;
1393    use crate::{
1394        observability::{
1395            PlanExecutionId, RuntimeJobId, RuntimeObservationConfig, RuntimeObservationOwner, RuntimeTaskId,
1396            RuntimeWorkerId, assert_live_topology_references_resolve,
1397        },
1398        runtime::stream_cleanup::{
1399            STREAM_CLEANUP_SUPERVISOR_BUILD_OPERATION, STREAM_CLEANUP_SUPERVISOR_JOINED,
1400            STREAM_CLEANUP_SUPERVISOR_NOT_JOINED, StreamCleanupEnqueueError, StreamCleanupSupervisorTerminal,
1401            block_stream_cleanup_thread_for_test, fail_next_cleanup_supervisor_spawn_for_test,
1402            force_cleanup_supervisor_unexpected_exit_for_test, observe_cleanup_supervisor_join_waiter_for_test,
1403            observe_cleanup_supervisor_shutdown_send_failure_for_test, panic_cleanup_supervisor_after_outcome_for_test,
1404            pause_cleanup_supervisor_before_worker_return_for_test, probe_cleanup_job_for_test,
1405            stream_cleanup_thread_id_for_test, wait_for_cleanup_supervisor_worker_finished_for_test,
1406        },
1407    };
1408
1409    #[test]
1410    fn zero_capacity_rejects_before_runtime_initialization() {
1411        let error = validate_service_stream_pool_size(0).unwrap_err();
1412        assert_eq!(error.code(), QueryErrorCode::InvalidConfiguration);
1413        assert_eq!(error.operation(), "query_engine.service_stream_pool.validate");
1414        assert_eq!(error.fact_value("field"), Some("service_stream_pool_size"));
1415        assert_eq!(error.fact_value("reason"), Some("zero"));
1416    }
1417
1418    fn observed_stream_states(owner: &RuntimeObservationOwner) -> Vec<StreamLeaseLifecycleState> {
1419        owner
1420            .live_snapshot()
1421            .expect("observation remains live")
1422            .events()
1423            .iter()
1424            .filter_map(|event| match &event.payload {
1425                RuntimeObservationPayload::StreamLease(observation) => Some(observation.lifecycle.clone()),
1426                _ => None,
1427            })
1428            .collect()
1429    }
1430
1431    #[test]
1432    fn stream_cleanup_jobs_run_on_background_thread() {
1433        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1434        assert_ne!(stream_cleanup_thread_id_for_test(&pool), thread::current().id());
1435    }
1436
1437    #[test]
1438    fn process_exit_joins_an_orphaned_cleanup_supervisor() {
1439        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1440        let supervisor = Arc::clone(&pool.inner.cleanup_supervisor);
1441
1442        drop(pool);
1443        supervisor.join_for_process_exit();
1444
1445        assert_eq!(supervisor.join_disposition.load(Ordering::Acquire), STREAM_CLEANUP_SUPERVISOR_JOINED,);
1446    }
1447
1448    #[test]
1449    fn cleanup_submission_racing_process_exit_is_retained_after_terminal_boundary() {
1450        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1451        let supervisor = Arc::clone(&pool.inner.cleanup_supervisor);
1452        let submission_gate = supervisor.submission_gate.lock().unwrap();
1453        let submitter_supervisor = Arc::clone(&supervisor);
1454        let submitter_pool = pool.clone();
1455        let rendezvous = Arc::new(Barrier::new(2));
1456        let submitter_rendezvous = Arc::clone(&rendezvous);
1457        let (done, receiver) = mpsc::channel();
1458        let submitter = thread::spawn(move || {
1459            submitter_rendezvous.wait();
1460            submitter_supervisor.enqueue_before_process_exit(
1461                probe_cleanup_job_for_test(done),
1462                &submitter_pool.inner.process_exit_started,
1463            )
1464        });
1465
1466        rendezvous.wait();
1467        pool.inner.process_exit_started.store(true, Ordering::Release);
1468        drop(submission_gate);
1469        let retained_job = match submitter.join().expect("cleanup submitter must not panic") {
1470            Err(StreamCleanupEnqueueError::ProcessExit(job)) => job,
1471            Ok(()) | Err(StreamCleanupEnqueueError::SupervisorStopped(_)) => {
1472                panic!("cleanup crossing the process-exit boundary must be retained")
1473            },
1474        };
1475        assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)));
1476
1477        pool.close_and_join_for_process_exit();
1478        drop(retained_job);
1479        assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Disconnected)));
1480    }
1481
1482    #[test]
1483    fn cleanup_supervisor_spawn_failure_rejects_pool_construction() {
1484        fail_next_cleanup_supervisor_spawn_for_test();
1485
1486        let error = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap_err();
1487
1488        assert_eq!(error.code(), QueryErrorCode::Internal);
1489        assert_eq!(error.status(), QueryErrorStatus::Permanent);
1490        assert_eq!(error.operation(), STREAM_CLEANUP_SUPERVISOR_BUILD_OPERATION);
1491        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_spawn_failed"));
1492        assert_eq!(error.fact_value("device_ordinal"), Some("0"));
1493    }
1494
1495    #[test]
1496    fn close_and_wait_explicitly_joins_cleanup_supervisor_for_all_clones() {
1497        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1498        let clone = pool.clone();
1499        let deadline = Instant::now() + Duration::from_secs(5);
1500
1501        pool.close_and_wait_until(deadline, "query_engine.service_stream_pool.test_join").unwrap();
1502        assert_eq!(
1503            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1504            STREAM_CLEANUP_SUPERVISOR_JOINED,
1505        );
1506
1507        clone.close_and_wait_until(deadline, "query_engine.service_stream_pool.test_join").unwrap();
1508        let error = clone.acquire().unwrap_err();
1509        assert_eq!(error.fact_value("reason"), Some("stream_pool_closed"));
1510    }
1511
1512    #[test]
1513    fn busy_cleanup_supervisor_times_out_then_joins_after_late_cleanup() {
1514        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1515        let blocker = block_stream_cleanup_thread_for_test(&pool);
1516        let lease = pool.acquire().unwrap();
1517        drop(lease);
1518
1519        let timeout = pool
1520            .close_and_wait_until(
1521                Instant::now() + Duration::from_millis(10),
1522                "query_engine.service_stream_pool.test_busy_cleanup",
1523            )
1524            .unwrap_err();
1525        assert!(timeout.is_cleanup_deadline_elapsed());
1526        assert_eq!(timeout.fact_value("reason"), Some("stream_pool_idle_timeout"));
1527        assert_eq!(
1528            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1529            STREAM_CLEANUP_SUPERVISOR_NOT_JOINED,
1530        );
1531
1532        drop(blocker);
1533        pool.close_and_wait_until(
1534            Instant::now() + Duration::from_secs(5),
1535            "query_engine.service_stream_pool.test_busy_cleanup",
1536        )
1537        .unwrap();
1538        assert_eq!(
1539            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1540            STREAM_CLEANUP_SUPERVISOR_JOINED,
1541        );
1542    }
1543
1544    #[test]
1545    fn unexpected_cleanup_supervisor_exit_is_permanent_and_joined() {
1546        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1547        force_cleanup_supervisor_unexpected_exit_for_test(&pool);
1548        let publication = pool
1549            .inner
1550            .cleanup_supervisor
1551            .wait_for_exit_until(
1552                Instant::now() + Duration::from_secs(5),
1553                "query_engine.service_stream_pool.test_unexpected_exit_publication",
1554            )
1555            .unwrap_err();
1556        assert_eq!(publication.fact_value("reason"), Some("stream_cleanup_supervisor_unexpected_exit"));
1557
1558        let error = pool
1559            .close_and_wait_until(
1560                Instant::now() + Duration::from_secs(5),
1561                "query_engine.service_stream_pool.test_unexpected_exit",
1562            )
1563            .unwrap_err();
1564
1565        assert_eq!(error.code(), QueryErrorCode::Internal);
1566        assert_eq!(error.status(), QueryErrorStatus::Permanent);
1567        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_unexpected_exit"));
1568        assert_eq!(
1569            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1570            STREAM_CLEANUP_SUPERVISOR_JOINED,
1571        );
1572    }
1573
1574    #[test]
1575    fn unexpected_cleanup_supervisor_exit_recovers_queued_completion_payloads() {
1576        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 2).unwrap();
1577        let cleanup_thread = stream_cleanup_thread_id_for_test(&pool);
1578        let blocker = block_stream_cleanup_thread_for_test(&pool);
1579        // Queue the forced exit before the leases so the receiver's teardown
1580        // drops both queued jobs and exercises their Drop recovery protocol.
1581        force_cleanup_supervisor_unexpected_exit_for_test(&pool);
1582        let first = pool.acquire().unwrap();
1583        let second = pool.acquire().unwrap();
1584        let (before_sender, before_receiver) = mpsc::channel();
1585        let (after_sender, after_receiver) = mpsc::channel();
1586
1587        assert!(first.install_cleanup_completion(
1588            {
1589                let before_sender = before_sender.clone();
1590                move |outcome| before_sender.send(("first", outcome, thread::current().id())).unwrap()
1591            },
1592            {
1593                let after_sender = after_sender.clone();
1594                move || after_sender.send(("first", thread::current().id())).unwrap()
1595            },
1596        ));
1597        assert!(second.install_cleanup_completion(
1598            move |outcome| before_sender.send(("second", outcome, thread::current().id())).unwrap(),
1599            move || after_sender.send(("second", thread::current().id())).unwrap(),
1600        ));
1601        drop(first);
1602        drop(second);
1603        drop(blocker);
1604
1605        let error = pool
1606            .close_and_wait_until(
1607                Instant::now() + Duration::from_secs(5),
1608                "query_engine.service_stream_pool.test_exit_with_payload",
1609            )
1610            .unwrap_err();
1611        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_unexpected_exit"));
1612        let mut before = [
1613            before_receiver.recv_timeout(Duration::from_secs(5)).expect("first recovery before callback"),
1614            before_receiver.recv_timeout(Duration::from_secs(5)).expect("second recovery before callback"),
1615        ];
1616        before.sort_by_key(|(label, _, _)| *label);
1617        let mut after = [
1618            after_receiver.recv_timeout(Duration::from_secs(5)).expect("first recovery after callback"),
1619            after_receiver.recv_timeout(Duration::from_secs(5)).expect("second recovery after callback"),
1620        ];
1621        after.sort_by_key(|(label, _)| *label);
1622        assert_eq!(before.iter().map(|(label, _, _)| *label).collect::<Vec<_>>(), vec!["first", "second"]);
1623        assert!(
1624            before.iter().all(|(_, outcome, _)| *outcome == StreamCleanupOutcome::CallbackAbandoned),
1625            "queued cleanup recovery must preserve abandonment rather than misclassifying it as a sync failure"
1626        );
1627        assert_eq!(after.iter().map(|(label, _)| *label).collect::<Vec<_>>(), vec!["first", "second"]);
1628        assert!(before.iter().all(|(_, _, thread_id)| *thread_id == cleanup_thread));
1629        assert!(after.iter().all(|(_, thread_id)| *thread_id == cleanup_thread));
1630        assert_eq!(
1631            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1632            STREAM_CLEANUP_SUPERVISOR_JOINED,
1633        );
1634    }
1635
1636    fn wait_until_idle_commits_an_already_finished_cleanup_panic_case() {
1637        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1638        let pause = pause_cleanup_supervisor_before_worker_return_for_test(&pool);
1639        let lease = pool.acquire().unwrap();
1640        assert!(lease.install_cleanup_completion(|_| panic!("injected cleanup panic before idle boundary"), || {},));
1641        drop(lease);
1642
1643        pause.arrived.wait();
1644        pause.release.wait();
1645        let deadline = Instant::now() + Duration::from_secs(5);
1646        wait_for_cleanup_supervisor_worker_finished_for_test(&pool, deadline);
1647
1648        let error =
1649            pool.wait_until_idle(deadline, "query_engine.service_stream_pool.test_idle_finished_panic").unwrap_err();
1650        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_panicked"));
1651        assert_eq!(error.fact_value("supervisor_stage"), Some("cleanup_executor"));
1652        assert_eq!(
1653            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1654            STREAM_CLEANUP_SUPERVISOR_JOINED,
1655        );
1656    }
1657
1658    #[test]
1659    fn wait_until_idle_commits_an_already_finished_cleanup_panic() {
1660        wait_until_idle_commits_an_already_finished_cleanup_panic_case();
1661        wait_until_quiescent_commits_an_already_finished_cleanup_panic();
1662    }
1663
1664    fn wait_until_quiescent_commits_an_already_finished_cleanup_panic() {
1665        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1666        let pause = pause_cleanup_supervisor_before_worker_return_for_test(&pool);
1667        let lease = pool.acquire().unwrap();
1668        assert!(
1669            lease.install_cleanup_completion(|_| panic!("injected cleanup panic before quiescence boundary"), || {},)
1670        );
1671        drop(lease);
1672
1673        pause.arrived.wait();
1674        pause.release.wait();
1675        let deadline = Instant::now() + Duration::from_secs(5);
1676        wait_for_cleanup_supervisor_worker_finished_for_test(&pool, deadline);
1677
1678        let error = pool
1679            .wait_until_quiescent(0, deadline, "query_engine.service_stream_pool.test_quiescent_finished_panic")
1680            .unwrap_err();
1681        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_panicked"));
1682        assert_eq!(error.fact_value("supervisor_stage"), Some("cleanup_executor"));
1683        assert_eq!(
1684            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1685            STREAM_CLEANUP_SUPERVISOR_JOINED,
1686        );
1687    }
1688
1689    #[test]
1690    fn cleanup_supervisor_send_failure_does_not_mask_panicked_worker_outcome() {
1691        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1692        let cleanup_thread = stream_cleanup_thread_id_for_test(&pool);
1693        let pause = pause_cleanup_supervisor_before_worker_return_for_test(&pool);
1694        let shutdown_send_failed = observe_cleanup_supervisor_shutdown_send_failure_for_test(&pool);
1695        let lease = pool.acquire().unwrap();
1696        let (before_sender, before_receiver) = mpsc::channel();
1697        let (after_sender, after_receiver) = mpsc::channel();
1698        assert!(lease.install_cleanup_completion(
1699            move |_| {
1700                before_sender.send(thread::current().id()).unwrap();
1701                panic!("injected cleanup panic before shutdown send failure");
1702            },
1703            move || after_sender.send(thread::current().id()).unwrap(),
1704        ));
1705        drop(lease);
1706
1707        // The worker has caught the callback panic and dropped its receiver,
1708        // including all queued-job recovery, but has not returned the outcome
1709        // to the join handle yet.
1710        pause.arrived.wait();
1711        let closer_pool = pool.clone();
1712        let closer = thread::spawn(move || {
1713            closer_pool.close_and_wait_until(
1714                Instant::now() + Duration::from_secs(5),
1715                "query_engine.service_stream_pool.test_send_failure_panic",
1716            )
1717        });
1718        shutdown_send_failed
1719            .recv_timeout(Duration::from_secs(5))
1720            .expect("close observes the disconnected cleanup receiver");
1721        {
1722            let state = pool.inner.cleanup_supervisor.control.state.lock().unwrap();
1723            assert!(state.shutdown_requested);
1724            assert!(state.terminal.is_none(), "send failure cannot commit a terminal outcome");
1725        }
1726
1727        pause.release.wait();
1728        let error = closer.join().expect("close thread joins").unwrap_err();
1729        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_panicked"));
1730        assert_eq!(error.fact_value("supervisor_stage"), Some("cleanup_executor"));
1731        assert_eq!(
1732            before_receiver.recv_timeout(Duration::from_secs(5)).expect("in-flight before callback"),
1733            cleanup_thread
1734        );
1735        assert_eq!(
1736            after_receiver.recv_timeout(Duration::from_secs(5)).expect("in-flight after callback"),
1737            cleanup_thread
1738        );
1739        let state = pool.inner.state.lock().unwrap();
1740        assert_eq!(state.pending_cleanups, 0);
1741        assert_eq!(state.active_cleanups, 0);
1742        assert_eq!(state.created, 0);
1743        assert_eq!(
1744            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1745            STREAM_CLEANUP_SUPERVISOR_JOINED,
1746        );
1747    }
1748
1749    #[test]
1750    fn cleanup_supervisor_elects_one_joiner_for_concurrent_close_callers() {
1751        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1752        let blocker = block_stream_cleanup_thread_for_test(&pool);
1753        let join_waiter = observe_cleanup_supervisor_join_waiter_for_test(&pool);
1754        let first_pool = pool.clone();
1755        let first = thread::spawn(move || {
1756            first_pool.close_and_wait_until(
1757                Instant::now() + Duration::from_secs(5),
1758                "query_engine.service_stream_pool.test_concurrent_joiner_first",
1759            )
1760        });
1761        let second_pool = pool.clone();
1762        let second = thread::spawn(move || {
1763            second_pool.close_and_wait_until(
1764                Instant::now() + Duration::from_secs(5),
1765                "query_engine.service_stream_pool.test_concurrent_joiner_second",
1766            )
1767        });
1768
1769        join_waiter.recv_timeout(Duration::from_secs(5)).expect("second close caller waits for the elected joiner");
1770        drop(blocker);
1771        first.join().expect("first close thread joins").expect("first close succeeds");
1772        second.join().expect("second close thread joins").expect("second close succeeds");
1773        let state = pool.inner.cleanup_supervisor.control.state.lock().unwrap();
1774        assert!(matches!(state.terminal, Some(StreamCleanupSupervisorTerminal::Clean)));
1775        assert!(!state.joiner_active);
1776        assert_eq!(
1777            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1778            STREAM_CLEANUP_SUPERVISOR_JOINED,
1779        );
1780    }
1781
1782    #[test]
1783    fn cleanup_supervisor_precommit_join_timeout_is_retryable() {
1784        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1785        let blocker = block_stream_cleanup_thread_for_test(&pool);
1786
1787        let timeout = pool
1788            .close_and_wait_until(
1789                Instant::now() + Duration::from_millis(10),
1790                "query_engine.service_stream_pool.test_precommit_join_timeout",
1791            )
1792            .unwrap_err();
1793        assert!(timeout.is_cleanup_deadline_elapsed());
1794        assert_eq!(timeout.fact_value("reason"), Some("stream_cleanup_supervisor_join_timeout"));
1795        {
1796            let state = pool.inner.cleanup_supervisor.control.state.lock().unwrap();
1797            assert!(state.terminal.is_none(), "a deadline before join cannot commit a terminal outcome");
1798            assert!(!state.joiner_active, "a later close caller must be able to retry the join");
1799        }
1800        assert_eq!(
1801            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1802            STREAM_CLEANUP_SUPERVISOR_NOT_JOINED,
1803        );
1804
1805        drop(blocker);
1806        pool.close_and_wait_until(
1807            Instant::now() + Duration::from_secs(5),
1808            "query_engine.service_stream_pool.test_precommit_join_timeout",
1809        )
1810        .unwrap();
1811        assert_eq!(
1812            pool.inner.cleanup_supervisor.join_disposition.load(Ordering::Acquire),
1813            STREAM_CLEANUP_SUPERVISOR_JOINED,
1814        );
1815    }
1816
1817    #[test]
1818    fn cleanup_supervisor_join_panic_supersedes_provisional_clean_outcome() {
1819        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1820        panic_cleanup_supervisor_after_outcome_for_test(&pool);
1821        let deadline = Instant::now() + Duration::from_secs(5);
1822
1823        let error =
1824            pool.close_and_wait_until(deadline, "query_engine.service_stream_pool.test_join_panic").unwrap_err();
1825        assert_eq!(error.code(), QueryErrorCode::Internal);
1826        assert_eq!(error.status(), QueryErrorStatus::Permanent);
1827        assert_eq!(error.fact_value("reason"), Some("stream_cleanup_supervisor_panicked"));
1828        assert_eq!(error.fact_value("supervisor_stage"), Some("thread_join"));
1829
1830        let repeated =
1831            pool.close_and_wait_until(deadline, "query_engine.service_stream_pool.test_join_panic").unwrap_err();
1832        assert_eq!(repeated.fact_value("reason"), Some("stream_cleanup_supervisor_panicked"));
1833        assert_eq!(repeated.fact_value("supervisor_stage"), Some("thread_join"));
1834    }
1835
1836    #[test]
1837    fn cleanup_phase_callbacks_straddle_quiescence_accounting() {
1838        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1839        let lease = pool.acquire().unwrap();
1840        let before_pool = pool.clone();
1841        let after_pool = pool;
1842        let (before_sender, before_receiver) = mpsc::channel();
1843        let (after_sender, after_receiver) = mpsc::channel();
1844
1845        assert!(lease.install_cleanup_completion(
1846            move |_| {
1847                let active_cleanups = before_pool.inner.state.lock().unwrap().active_cleanups;
1848                before_sender.send((active_cleanups, thread::current().id())).unwrap();
1849            },
1850            move || {
1851                let active_cleanups = after_pool.inner.state.lock().unwrap().active_cleanups;
1852                after_sender.send((active_cleanups, thread::current().id())).unwrap();
1853            },
1854        ));
1855        drop(lease);
1856
1857        let (before_active, before_thread) =
1858            before_receiver.recv_timeout(Duration::from_secs(5)).expect("internal cleanup callback runs");
1859        let (after_active, after_thread) =
1860            after_receiver.recv_timeout(Duration::from_secs(5)).expect("external completion callback runs");
1861        assert_eq!(before_active, 1, "resource teardown remains inside cleanup accounting");
1862        assert_eq!(after_active, 0, "external completion runs after cleanup quiescence is published");
1863        assert_eq!(before_thread, after_thread, "both phases run on the pre-created cleanup supervisor");
1864    }
1865
1866    #[test]
1867    fn synchronous_cleanup_enqueue_fallback_dispatches_external_completion() {
1868        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1869        let lease = pool.acquire().unwrap();
1870        let dropping_thread = thread::current().id();
1871        let before_pool = pool.clone();
1872        let after_pool = pool.clone();
1873        let (before_sender, before_receiver) = mpsc::channel();
1874        let (after_sender, after_receiver) = mpsc::channel();
1875
1876        assert!(lease.install_cleanup_completion(
1877            move |_| {
1878                let active_cleanups = before_pool.inner.state.lock().unwrap().active_cleanups;
1879                before_sender.send((active_cleanups, thread::current().id())).unwrap();
1880            },
1881            move || {
1882                let active_cleanups = after_pool.inner.state.lock().unwrap().active_cleanups;
1883                after_sender.send((active_cleanups, thread::current().id())).unwrap();
1884            },
1885        ));
1886        pool.force_next_cleanup_enqueue_fallback_for_test();
1887        drop(lease);
1888
1889        let (before_active, before_thread) =
1890            before_receiver.recv_timeout(Duration::from_secs(5)).expect("fallback internal cleanup callback runs");
1891        let (after_active, after_thread) =
1892            after_receiver.recv_timeout(Duration::from_secs(5)).expect("fallback external completion callback runs");
1893        assert_eq!(before_active, 1, "fallback resource teardown remains inside cleanup accounting");
1894        assert_eq!(before_thread, dropping_thread, "fallback synchronizes resources on the dropping thread");
1895        assert_eq!(after_active, 0, "fallback external completion runs after cleanup quiescence is published");
1896        assert_eq!(after_thread, dropping_thread, "fallback transfers completion on its synchronous cleanup path");
1897    }
1898
1899    #[test]
1900    fn synchronization_failure_discards_runtime_and_quarantines_pool() {
1901        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1902        let lease = pool.acquire().unwrap();
1903        pool.fail_next_synchronization_for_test();
1904        drop(lease);
1905        pool.wait_until_idle(
1906            Instant::now() + Duration::from_secs(5),
1907            "query_engine.service_stream_pool.test_sync_failure",
1908        )
1909        .unwrap();
1910
1911        let error = pool.acquire().expect_err("synchronization failure closes the affected pool");
1912        assert_eq!(error.fact_value("reason"), Some("stream_pool_closed"));
1913        assert_eq!(error.fact_value("synchronization_failures"), Some("1"));
1914        let state = pool.inner.state.lock().unwrap();
1915        assert_eq!(state.created, 0);
1916        assert!(state.idle.is_empty());
1917    }
1918
1919    #[test]
1920    fn unhealthy_device_rejects_idle_runtime_reuse() {
1921        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1922        let lease = pool.acquire().unwrap();
1923        drop(lease);
1924        let _ = stream_cleanup_thread_id_for_test(&pool);
1925        let _scope = DeviceSafetyTestScope::install([]);
1926        report_device_fault(
1927            cudf::DeviceId::DEFAULT,
1928            DeviceFaultKind::FatalDeviceOperation,
1929            "injected_stream_reuse_fault",
1930            Some(DeviceFaultNativeStatus::CUDF_FATAL_CUDA),
1931        );
1932
1933        let error = pool.acquire().expect_err("unhealthy device must reject idle runtime reuse");
1934        assert_eq!(error.code(), QueryErrorCode::CudfPersistentFailure);
1935        assert_eq!(error.fact_value("reason"), Some("device_health_rejected_stream_acquire"));
1936    }
1937
1938    #[test]
1939    fn observed_stream_acquire_distinguishes_create_reuse_and_release() {
1940        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1941        let owner =
1942            RuntimeObservationOwner::new("stream-lifecycle", None, RuntimeObservationConfig::bounded(32, 32 * 1024));
1943        let cancellation = ExecutionCancellation::new();
1944
1945        let created = pool
1946            .acquire_observed_with_cancellation(
1947                &cancellation,
1948                "query_engine.service_stream_pool.test_observed_create",
1949                owner.handle(),
1950                RuntimeStreamLeaseScope::default(),
1951            )
1952            .unwrap();
1953        drop(created);
1954        let _ = stream_cleanup_thread_id_for_test(&pool);
1955        let reused = pool
1956            .acquire_observed_with_cancellation(
1957                &cancellation,
1958                "query_engine.service_stream_pool.test_observed_reuse",
1959                owner.handle(),
1960                RuntimeStreamLeaseScope::default(),
1961            )
1962            .unwrap();
1963        drop(reused);
1964        let _ = stream_cleanup_thread_id_for_test(&pool);
1965
1966        let states = observed_stream_states(&owner);
1967        assert_live_topology_references_resolve(&owner.live_snapshot().unwrap());
1968        assert_eq!(states.len(), 6);
1969        assert!(matches!(states[0], StreamLeaseLifecycleState::Enqueued));
1970        assert!(matches!(
1971            states[1],
1972            StreamLeaseLifecycleState::Acquired { acquisition: StreamLeaseAcquisition::Created, .. }
1973        ));
1974        assert!(matches!(states[2], StreamLeaseLifecycleState::Released));
1975        assert!(matches!(states[3], StreamLeaseLifecycleState::Enqueued));
1976        assert!(matches!(
1977            states[4],
1978            StreamLeaseLifecycleState::Acquired { acquisition: StreamLeaseAcquisition::Reused, .. }
1979        ));
1980        assert!(matches!(states[5], StreamLeaseLifecycleState::Released));
1981    }
1982
1983    #[test]
1984    fn observed_stream_wait_cancellation_has_one_terminal_without_sleep() {
1985        let pool = ServiceStreamPool::new(cudf::DeviceId::DEFAULT, 1).unwrap();
1986        let held = pool.acquire().unwrap();
1987        let cancellation = ExecutionCancellation::new();
1988        let owner =
1989            RuntimeObservationOwner::new("stream-cancel", None, RuntimeObservationConfig::bounded(32, 32 * 1024));
1990        let waiter_pool = pool.clone();
1991        let waiter_cancellation = cancellation.clone();
1992        let observation = owner.handle();
1993        let (started_sender, started_receiver) = mpsc::channel();
1994        let waiter = thread::spawn(move || {
1995            started_sender.send(()).unwrap();
1996            waiter_pool.acquire_observed_with_cancellation_deadline(
1997                &waiter_cancellation,
1998                Some(Instant::now() + Duration::from_secs(1)),
1999                "query_engine.service_stream_pool.test_observed_cancel",
2000                observation,
2001                RuntimeStreamLeaseScope::for_plan(PlanExecutionId::new("stream-plan"))
2002                    .with_source_worker(RuntimeTaskId::new("source-task"), RuntimeWorkerId::new("source-worker"))
2003                    .with_gpu_worker(RuntimeJobId::new("gpu-job"), RuntimeWorkerId::new("gpu-worker")),
2004            )
2005        });
2006        started_receiver.recv().unwrap();
2007        pool.wait_until_acquire_queued_for_test(Instant::now() + Duration::from_secs(5));
2008        cancellation.cancel();
2009
2010        let error = waiter.join().unwrap().unwrap_err();
2011        drop(held);
2012        assert_eq!(error.code(), QueryErrorCode::Cancelled);
2013        assert_eq!(error.operation(), "query_engine.service_stream_pool.test_observed_cancel");
2014        let snapshot = owner.live_snapshot().unwrap();
2015        let event = snapshot
2016            .events()
2017            .iter()
2018            .find(|event| matches!(event.payload, RuntimeObservationPayload::StreamLease(_)))
2019            .unwrap();
2020        assert_eq!(event.plan_execution_id.as_ref().map(|id| id.as_str()), Some("stream-plan"));
2021        let RuntimeObservationPayload::StreamLease(observation) = &event.payload else { unreachable!() };
2022        assert_eq!(observation.scope.task_id.as_ref().map(|id| id.as_str()), Some("source-task"));
2023        assert_eq!(observation.scope.source_worker_id.as_ref().map(|id| id.as_str()), Some("source-worker"));
2024        assert_eq!(observation.scope.gpu_job_id.as_ref().map(|id| id.as_str()), Some("gpu-job"));
2025        assert_eq!(observation.scope.gpu_worker_id.as_ref().map(|id| id.as_str()), Some("gpu-worker"));
2026        assert_eq!(
2027            observed_stream_states(&owner),
2028            vec![
2029                StreamLeaseLifecycleState::Enqueued,
2030                StreamLeaseLifecycleState::CancellationRequested,
2031                StreamLeaseLifecycleState::Cancelled,
2032            ]
2033        );
2034    }
2035
2036    #[test]
2037    fn stream_acquire_deadline_times_out_while_pool_is_saturated() {
2038        let device = cudf::DeviceId::DEFAULT;
2039        let pool = ServiceStreamPool::new(device, 1).unwrap();
2040        let held = pool.acquire().unwrap();
2041        let cancellation = ExecutionCancellation::new();
2042        let deadline = Instant::now() + Duration::from_millis(10);
2043
2044        let error = pool
2045            .acquire_with_cancellation_deadline(
2046                &cancellation,
2047                deadline,
2048                "query_engine.service_stream_pool.test_timed_acquire",
2049            )
2050            .unwrap_err();
2051
2052        drop(held);
2053        assert_eq!(error.code(), QueryErrorCode::ServiceOverloaded);
2054        assert_eq!(error.operation(), "query_engine.service_stream_pool.test_timed_acquire");
2055        assert_eq!(error.fact_value("reason"), Some("stream_pool_wait_timeout"));
2056        assert_eq!(error.fact_value("stream_pool_size"), Some("1"));
2057    }
2058}
2059
2060#[cfg(test)]
2061mod permit_class_tests {
2062    use super::*;
2063
2064    fn root_class_waits_while_auxiliary_slots_are_free_case() {
2065        let pool = ServiceStreamPool::new_with_classes(cudf::DeviceId::DEFAULT, 3, 1, 2).unwrap();
2066        let root = pool.acquire_with_class(StreamPermitClass::Root).unwrap();
2067        let aux_one = pool.acquire_with_class(StreamPermitClass::Auxiliary).unwrap();
2068        let aux_two = pool.acquire_with_class(StreamPermitClass::Auxiliary).unwrap();
2069        let (root_out, root_cap, aux_out, aux_cap) = pool.class_counters();
2070        assert_eq!((root_out, root_cap, aux_out, aux_cap), (1, 1, 2, 2));
2071
2072        let (sender, receiver) = std::sync::mpsc::channel();
2073        let second_pool = pool;
2074        let waiter = std::thread::spawn(move || {
2075            sender.send(()).unwrap();
2076            second_pool.acquire_observed_with_cancellation_deadline_and_class(
2077                &ExecutionCancellation::new(),
2078                Some(Instant::now() + std::time::Duration::from_millis(200)),
2079                "query_engine.service_stream_pool.test_root_wait",
2080                RuntimeObservationHandle::disabled(),
2081                RuntimeStreamLeaseScope::default(),
2082                StreamPermitClass::Root,
2083            )
2084        });
2085        receiver.recv_timeout(std::time::Duration::from_secs(1)).unwrap();
2086        std::thread::sleep(std::time::Duration::from_millis(50));
2087        assert!(!waiter.is_finished(), "root class is full even though every physical slot is busy anyway");
2088
2089        drop(aux_two);
2090        std::thread::sleep(std::time::Duration::from_millis(50));
2091        assert!(
2092            !waiter.is_finished(),
2093            "freeing an auxiliary slot must not release a root waiter: classes are partitioned"
2094        );
2095        drop(root);
2096        let acquired = waiter.join().unwrap().expect("root waiter acquires after the root lease returns");
2097        drop(acquired);
2098        drop(aux_one);
2099    }
2100
2101    #[test]
2102    fn root_class_waits_while_auxiliary_slots_are_free() {
2103        root_class_waits_while_auxiliary_slots_are_free_case();
2104        auxiliary_class_waits_while_root_slots_are_free();
2105    }
2106
2107    fn auxiliary_class_waits_while_root_slots_are_free() {
2108        let pool = ServiceStreamPool::new_with_classes(cudf::DeviceId::DEFAULT, 3, 1, 1).unwrap();
2109        let _root = pool.acquire_with_class(StreamPermitClass::Root).unwrap();
2110        let aux = pool.acquire_with_class(StreamPermitClass::Auxiliary).unwrap();
2111        let (sender, receiver) = std::sync::mpsc::channel();
2112        let second_pool = pool;
2113        let waiter = std::thread::spawn(move || {
2114            sender.send(()).unwrap();
2115            second_pool.acquire_observed_with_cancellation_deadline_and_class(
2116                &ExecutionCancellation::new(),
2117                Some(Instant::now() + std::time::Duration::from_millis(200)),
2118                "query_engine.service_stream_pool.test_aux_wait",
2119                RuntimeObservationHandle::disabled(),
2120                RuntimeStreamLeaseScope::default(),
2121                StreamPermitClass::Auxiliary,
2122            )
2123        });
2124        receiver.recv_timeout(std::time::Duration::from_secs(1)).unwrap();
2125        std::thread::sleep(std::time::Duration::from_millis(50));
2126        assert!(!waiter.is_finished(), "auxiliary class is full while the root slot stays free");
2127        drop(aux);
2128        let acquired = waiter.join().unwrap().expect("auxiliary waiter acquires after an auxiliary return");
2129        drop(acquired);
2130    }
2131
2132    #[test]
2133    fn zero_class_capacity_is_rejected() {
2134        let error = ServiceStreamPool::new_with_classes(cudf::DeviceId::DEFAULT, 2, 0, 1).unwrap_err();
2135        assert_eq!(error.code(), QueryErrorCode::InvalidConfiguration);
2136        assert_eq!(error.fact_value("field"), Some("root_capacity"));
2137        let error = ServiceStreamPool::new_with_classes(cudf::DeviceId::DEFAULT, 2, 1, 0).unwrap_err();
2138        assert_eq!(error.fact_value("field"), Some("aux_capacity"));
2139    }
2140}