1use 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#[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#[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#[derive(Debug)]
139pub(super) struct ServiceStreamPoolInner {
140 pub(super) id: u64,
141 pub(super) device: DeviceId,
142 pub(super) pool_size: usize,
146 pub(super) root_capacity: usize,
147 pub(super) aux_capacity: usize,
148 pub(super) state: Mutex<ServiceStreamPoolState>,
151 pub(super) available: Notify,
154 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 pub(super) closed: AtomicBool,
164 pub(super) process_exit_started: AtomicBool,
168}
169
170#[cfg(any(test, feature = "test-hooks"))]
171type ServiceStreamPoolRegistry = HashMap<(usize, usize), Weak<ServiceStreamPoolInner>>;
172
173#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
180pub(crate) enum StreamPermitClass {
181 Root,
183 #[default]
185 Auxiliary,
186}
187
188#[derive(Debug, Default)]
194pub(super) struct ServiceStreamPoolState {
195 pub(super) idle: Vec<Arc<Runtime>>,
198 pub(super) created: usize,
201 pub(super) root_outstanding: usize,
205 pub(super) aux_outstanding: usize,
207 pub(super) waiting_acquires: usize,
210 pub(super) pending_cleanups: usize,
212 pub(super) active_cleanups: usize,
215 pub(super) synchronization_failures: u64,
218}
219
220#[derive(Debug, Clone)]
229pub struct ServiceStreamPool {
230 pub(super) inner: Arc<ServiceStreamPoolInner>,
231}
232
233impl ServiceStreamPool {
234 pub fn new(device: DeviceId, pool_size: usize) -> Result<Self> {
240 Self::new_with_classes(device, pool_size, 1, pool_size)
246 }
247
248 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 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 #[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 pub fn device(&self) -> DeviceId {
330 self.inner.device
331 }
332
333 pub(crate) fn pool_size(&self) -> usize {
335 self.inner.pool_size
336 }
337
338 #[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 #[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 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 drop(idle);
384 Ok(())
385 }
386
387 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 drop(state);
407 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 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 drop(state);
453
454 self.inner.cleanup_supervisor.request_shutdown(deadline, operation)?;
458 self.inner.cleanup_supervisor.wait_for_exit_until(deadline, operation)
459 }
460
461 pub(super) fn close_and_join_for_process_exit(&self) {
465 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 drop(idle);
484 self.inner.cleanup_supervisor.join_for_process_exit();
485 }
486
487 #[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 pub fn acquire(&self) -> Result<StreamLease> {
571 self.acquire_with_class(StreamPermitClass::Auxiliary)
572 }
573
574 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 #[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 #[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 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 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 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 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 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
838struct 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
857struct 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 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 if pool.inner.closed.load(Ordering::Acquire) {
924 return Err(self.fail(stream_pool_closed(operation, pool)));
925 }
926 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 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 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
990pub(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 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 Ok(runtime) => {
1026 pool.discard_created_slot(self.permit_class);
1027 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 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 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#[derive(Debug)]
1146struct StreamLeaseObservation {
1147 observation: RuntimeObservationHandle,
1148 lease_id: RuntimeResourceId,
1149 device_ordinal: u32,
1150 scope: RuntimeStreamLeaseScope,
1151 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
1204pub struct StreamLease {
1213 pool: ServiceStreamPool,
1214 runtime: Option<Arc<Runtime>>,
1216 observation: Option<Arc<StreamLeaseObservation>>,
1217 resource_epoch: Option<Arc<QueryServiceResourceEpoch>>,
1222 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 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 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 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 pub fn shared_runtime(&self) -> Result<Arc<Runtime>> {
1314 self.runtime().map(Arc::new)
1315 }
1316}
1317
1318impl Drop for StreamLease {
1319 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
1337pub struct AttemptWorkLease {
1343 context: ExecutionContext,
1344 _stream_lease: StreamLease,
1345 _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 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 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}