Skip to main content

query_runtime/runtime/gpu_execution_pool/
lease.rs

1//! Lifecycle-bound lease and blocking-worker handle for the GPU execution pool.
2
3use std::{
4    future::Future,
5    panic::AssertUnwindSafe,
6    sync::{Arc, mpsc},
7};
8
9use query_planning::error::{QueryError, QueryErrorCode, Result};
10
11use super::{
12    GpuExecutionJobClass, GpuExecutionJobKind, GpuExecutionPool, JobObservation, RUN_OPERATION, job_panic_error,
13};
14use crate::observability::{RuntimeObservationHandle, RuntimeStreamLeaseScope};
15
16/// Opaque lifecycle-bound capability for synchronous GPU work.
17///
18/// This lease deliberately exposes execution but not pool construction,
19/// detached spawning, shutdown, counters, or raw resource-epoch access.
20#[derive(Clone)]
21pub struct GpuExecutorLease {
22    pool: GpuExecutionPool,
23    /// Class the lease's own query-execution job runs in.
24    ///
25    /// An attempt's root execution job is root-class work, one per attempt. A
26    /// second native island of the same attempt executes on an auxiliary
27    /// fragment context that already owns an attempt auxiliary permit, so its
28    /// execution job is auxiliary work backed by that permit, under the same
29    /// ownership rule the context-setup job and the execution lanes follow.
30    /// Without this, both islands of one attempt would charge the root class,
31    /// whose capacity is one job per admitted attempt.
32    execution_class: GpuExecutionJobClass,
33}
34
35impl GpuExecutorLease {
36    pub(crate) fn from_pool(pool: GpuExecutionPool) -> Self {
37        Self { pool, execution_class: GpuExecutionJobClass::Lifecycle }
38    }
39
40    /// Builds a lease whose query-execution job runs as auxiliary work.
41    ///
42    /// The caller must already hold the attempt auxiliary permit that backs
43    /// it, for at least the lifetime of the jobs this lease runs.
44    pub(crate) fn from_pool_for_auxiliary_fragment(pool: GpuExecutionPool) -> Self {
45        Self { pool, execution_class: GpuExecutionJobClass::Execution }
46    }
47
48    /// Runs one job on the lease's dedicated device executor.
49    pub async fn run<F, T>(&self, kind: GpuExecutionJobKind, future: F) -> Result<T>
50    where
51        F: Future<Output = T> + Send + 'static,
52        T: Send + 'static,
53    {
54        self.pool.run(kind, future).await
55    }
56
57    /// Runs one observed job and provides explicit job/worker correlation to
58    /// work that may acquire a service-stream lease.
59    pub fn run_observed_scoped<B, F, T>(
60        &self,
61        kind: GpuExecutionJobKind,
62        observation: RuntimeObservationHandle,
63        build_future: B,
64    ) -> impl Future<Output = Result<T>> + Send + 'static + use<B, F, T>
65    where
66        B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
67        F: Future<Output = T> + Send + 'static,
68        T: Send + 'static,
69    {
70        let class = match kind {
71            GpuExecutionJobKind::QueryExecution => self.execution_class,
72            _ => kind.job_class(),
73        };
74        self.pool.clone().run_observed_scoped_in_class(kind, class, observation, build_future)
75    }
76
77    /// Submits one blocking worker onto the lease's dedicated device executor.
78    ///
79    /// This is the synchronous-work counterpart to [`run`](Self::run): the
80    /// worker runs on a bounded blocking thread. It does not run on an async
81    /// pool worker, and the same class admission applies. It fits this lease's remit of
82    /// synchronous GPU work without exposing pool construction, detached
83    /// spawning, shutdown, or counters.
84    pub fn submit_blocking<F, T>(
85        &self,
86        kind: GpuExecutionJobKind,
87        observation: RuntimeObservationHandle,
88        work: F,
89    ) -> Result<PoolBlockingWork<T>>
90    where
91        F: FnOnce() -> T + Send + 'static,
92        T: Send + 'static,
93    {
94        self.pool.submit_blocking(kind, observation, work)
95    }
96
97    /// Submits blocking work and calls `completion` after its class slot is
98    /// reusable and its terminal outcome is available to join.
99    #[doc(hidden)]
100    pub fn submit_blocking_with_completion<F, T, C>(
101        &self,
102        kind: GpuExecutionJobKind,
103        observation: RuntimeObservationHandle,
104        work: F,
105        completion: C,
106    ) -> Result<PoolBlockingWork<T>>
107    where
108        F: FnOnce() -> T + Send + 'static,
109        T: Send + 'static,
110        C: FnOnce() + Send + 'static,
111    {
112        self.pool.submit_blocking_with_completion(kind, observation, work, completion)
113    }
114}
115
116impl std::fmt::Debug for GpuExecutorLease {
117    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        formatter.debug_struct("GpuExecutorLease").finish_non_exhaustive()
119    }
120}
121
122/// Completion handle for one pool-carried blocking worker.
123///
124/// The waiter holds no pool permits: capacity is charged to the running
125/// worker only, so a coordinator blocked in [`PoolBlockingWork::join`]
126/// can never starve the worker it waits for.
127pub struct PoolBlockingWork<T> {
128    pub(super) receiver: mpsc::Receiver<BlockingWorkOutcome<T>>,
129    pub(super) job: Arc<JobObservation>,
130    pub(super) device_ordinal: usize,
131    pub(super) kind: GpuExecutionJobKind,
132}
133
134impl<T> std::fmt::Debug for PoolBlockingWork<T> {
135    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        formatter.debug_struct("PoolBlockingWork").finish_non_exhaustive()
137    }
138}
139
140pub(super) enum BlockingWorkOutcome<T> {
141    Completed(T),
142    Panicked,
143}
144
145/// A submitted worker may be discarded by pool shutdown before its closure
146/// starts. Its completion notification still has to wake a coordinator that
147/// is waiting to join the submitted handle.
148pub(super) struct BlockingCompletionNotify<C: FnOnce()> {
149    completion: Option<C>,
150}
151
152impl<C: FnOnce()> BlockingCompletionNotify<C> {
153    pub(super) fn new(completion: C) -> Self {
154        Self { completion: Some(completion) }
155    }
156
157    pub(super) fn notify(&mut self) {
158        if let Some(completion) = self.completion.take() {
159            completion();
160        }
161    }
162}
163
164impl<C: FnOnce()> Drop for BlockingCompletionNotify<C> {
165    fn drop(&mut self) {
166        if self.completion.is_some() {
167            // Shutdown can drop queued work during unwind; a notification panic
168            // must not turn that cleanup into a second panic.
169            let _ = std::panic::catch_unwind(AssertUnwindSafe(|| self.notify()));
170        }
171    }
172}
173
174impl<T> PoolBlockingWork<T> {
175    /// Drains the worker and returns its exactly-once terminal outcome.
176    ///
177    /// Join drains the worker. A worker that already started is always awaited and
178    /// its value returned, so the caller can release whatever that worker
179    /// produced through its own accounting. A worker lost to pool shutdown or
180    /// process exit reports typed cancellation and does not hang; a worker
181    /// panic reports the pool's typed panic error. A resolved join implies
182    /// the worker's class slot is already reusable.
183    pub fn join(self) -> Result<T> {
184        match self.receiver.recv() {
185            Ok(BlockingWorkOutcome::Completed(value)) => Ok(value),
186            Ok(BlockingWorkOutcome::Panicked) => Err(job_panic_error(self.device_ordinal, self.kind)),
187            Err(_) => {
188                self.job.cancellation_requested();
189                Err(QueryError::new(
190                    QueryErrorCode::Cancelled,
191                    RUN_OPERATION,
192                    "GPU execution pool blocking worker ended without delivering completion",
193                )
194                .with_fact("device_ordinal", self.device_ordinal)
195                .with_fact("job_kind", self.kind.as_str())
196                .with_fact("reason", "gpu_execution_pool_blocking_worker_lost"))
197            },
198        }
199    }
200}