1#[cfg(any(test, feature = "test-hooks"))]
4use std::sync::{Mutex, PoisonError};
5use std::{
6 sync::Arc,
7 time::{Duration, Instant},
8};
9
10#[cfg(test)]
11use query_planning::policy::PlanningPolicy;
12use query_planning::{correlation::TraceId, error::QueryErrorCode};
13
14#[cfg(any(test, feature = "test-hooks"))]
15use super::RuntimeTerminalOutcome;
16use super::{
17 AdmissionRequestId, AdmittedAttempt, AggregateStrategyExecutionOptions, AttemptCapabilityEvidence,
18 ExecutionCancellation, ExecutionContext, GpuExecutionJobKind, GpuExecutionPool, MemoryControlPolicy, PlanScope,
19 QueryError, QueryExecutionConfig, QueryId, QueryService, QueryServiceInner, QueueBounds, Result,
20 SourceChunkPlanningMode, SourceChunkPlanningOptions, SourceSchedulerPolicy, StatisticalAggregateExecutionMode,
21 StreamBinding, memory_control_policy_for_grant, resources::QueryServiceResources,
22 validate_service_stream_pool_device,
23};
24#[cfg(any(test, feature = "test-hooks"))]
25use crate::attempt::AttemptOwner;
26use crate::{
27 observability::{AttemptAdmissionWaitFact, RuntimeStreamLeaseScope},
28 runtime::{
29 GpuExecutorLease, StreamPermitClass,
30 context::{AttemptAuxiliaryPermit, QueryServiceResourceEpoch},
31 gpu::{DeviceContext, GrantIdentity},
32 gpu_execution_pool::GpuExecutionJobClass,
33 service_stream::StreamSlot,
34 },
35};
36
37mod lifetime;
38mod release;
39mod report_commit;
40
41pub(super) use lifetime::QueryHandleLifetime;
42use lifetime::{ContextSetupClaim, RootContextSetupClaimGuard};
43
44#[derive(Debug)]
47struct AuxiliaryReservation {
48 permit: AttemptAuxiliaryPermit,
49 stream: StreamSlot,
50}
51
52#[derive(Debug)]
54pub struct QueryHandle {
55 pub(super) config: QueryExecutionConfig,
56 pub(super) queue_bounds: QueueBounds,
57 pub(super) admission_request_id: AdmissionRequestId,
58 pub(super) admission_sequence: u64,
59 pub(super) admission_wait: Duration,
60 pub(super) admission_wait_fact: Option<AttemptAdmissionWaitFact>,
61 pub(super) device_budget_bytes: u64,
62 pub(super) device_ordinal: usize,
63 pub(super) lifetime: Arc<QueryHandleLifetime>,
64 #[cfg(any(test, feature = "test-hooks"))]
67 pub(super) test_attempt: Mutex<Option<AttemptOwner>>,
68}
69
70impl QueryHandle {
71 pub fn admission_request_id(&self) -> AdmissionRequestId {
73 self.admission_request_id
74 }
75
76 pub fn admission_sequence(&self) -> u64 {
78 self.admission_sequence
79 }
80
81 pub fn admission_wait(&self) -> Duration {
83 self.admission_wait
84 }
85
86 pub fn admission_wait_fact(&self) -> Option<AttemptAdmissionWaitFact> {
90 self.admission_wait_fact
91 }
92
93 pub fn device_budget_bytes(&self) -> u64 {
95 self.device_budget_bytes
96 }
97
98 pub fn device_ordinal(&self) -> usize {
100 self.device_ordinal
101 }
102
103 #[cfg(any(test, feature = "test-hooks"))]
105 pub(in crate::runtime::query_service) fn own_test_attempt(&self, owner: Option<AttemptOwner>) {
106 *self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner) = owner;
107 }
108
109 #[cfg(any(test, feature = "test-hooks"))]
112 #[doc(hidden)]
113 pub fn end_test_attempt(&self) {
114 let owner = self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner).take();
115 drop(owner);
116 }
117
118 #[cfg(any(test, feature = "test-hooks"))]
121 #[doc(hidden)]
122 pub fn commit_test_attempt(&self, outcome: RuntimeTerminalOutcome) {
123 let owner = self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner).take();
124 if let Some(owner) = owner {
125 owner.commit_terminal(outcome).expect("a test attempt commits one terminal outcome");
126 }
127 }
128
129 pub fn join_fragment(&self, fragment: &AttemptCapabilityEvidence) -> Result<()> {
137 let service = QueryService { inner: self.lifetime.service()? };
138 service.inner.admission.join_admitted_fragment(
139 self.device_ordinal,
140 &self.lifetime.admitted_plan_identities,
141 fragment,
142 )
143 }
144
145 pub fn grant_identity(&self) -> Result<GrantIdentity> {
147 self.lifetime.with_resources(|resources| Ok(resources.envelope.memory_grant().identity()))
148 }
149
150 pub fn memory_control_policy(&self) -> MemoryControlPolicy {
152 self.config.memory_policy_for_admitted_query(self.device_budget_bytes)
153 }
154
155 pub fn source_scheduler_policy(&self) -> SourceSchedulerPolicy {
157 self.config.source_scheduler_policy
158 }
159
160 pub fn source_chunk_planning_mode(&self) -> SourceChunkPlanningMode {
162 self.config.source_chunk_planning_mode
163 }
164
165 pub fn statistical_aggregate_execution_mode(&self) -> StatisticalAggregateExecutionMode {
167 self.config.statistical_aggregate_execution_mode
168 }
169
170 #[cfg(test)]
172 pub(crate) fn planning_policy(&self) -> PlanningPolicy {
173 self.config.planning_policy
174 }
175
176 pub fn aggregate_strategy_execution_options(&self) -> AggregateStrategyExecutionOptions {
178 self.config.aggregate_strategy_execution_options
179 }
180
181 pub fn source_chunk_planning_options(&self) -> SourceChunkPlanningOptions {
183 self.config.source_chunk_planning_options
184 }
185
186 async fn reserve_auxiliary_stream(
190 &self,
191 cancellation: &ExecutionCancellation,
192 deadline: Option<Instant>,
193 ) -> Result<StreamSlot> {
194 let operation = "query_engine.query_service.context.stream_acquire";
195 let service = self.lifetime.service()?;
196 let resources = &service.resources;
197 let pool = resources.devices.context_by_ordinal(self.device_ordinal)?.stream_pool().clone();
198 pool.reserve_async(
199 cancellation,
200 deadline,
201 operation,
202 self.lifetime.observation.clone(),
203 RuntimeStreamLeaseScope::default(),
204 StreamPermitClass::Auxiliary,
205 )
206 .await
207 }
208
209 fn context_stream_acquire_deadline(&self, cancellation: &ExecutionCancellation) -> Option<Instant> {
210 self.queue_bounds.wait_deadline(Instant::now(), cancellation.deadline())
211 }
212
213 pub fn acquire_context(&self) -> Result<AdmittedQueryContext> {
215 self.acquire_context_with_cancellation(ExecutionCancellation::new())
216 }
217
218 #[cfg(test)]
220 pub(crate) async fn acquire_context_async(self: &Arc<Self>) -> Result<AdmittedQueryContext> {
221 self.acquire_context_with_cancellation_async(ExecutionCancellation::new()).await
222 }
223
224 pub async fn acquire_context_with_cancellation_async(
226 self: &Arc<Self>,
227 cancellation: ExecutionCancellation,
228 ) -> Result<AdmittedQueryContext> {
229 let operation = "query_engine.query_service.context";
230 let setup_claim = self.lifetime.reserve_context_setup_async(&cancellation, operation).await?;
231 let root_setup_claim = (setup_claim == ContextSetupClaim::Root)
232 .then(|| RootContextSetupClaimGuard::new(Arc::clone(&self.lifetime)));
233 let stream_acquire_deadline = self.context_stream_acquire_deadline(&cancellation);
234 let auxiliary_reservation = match setup_claim {
240 ContextSetupClaim::Auxiliary => {
241 let permit = self
242 .lifetime
243 .attempt_auxiliary_slots
244 .acquire_with_cancellation_deadline_async(
245 &cancellation,
246 stream_acquire_deadline,
247 "query_engine.query_service.context.attempt_auxiliary_acquire",
248 )
249 .await?;
250 let stream = self.reserve_auxiliary_stream(&cancellation, stream_acquire_deadline).await?;
251 Some(AuxiliaryReservation { permit, stream })
252 },
253 ContextSetupClaim::Root => None,
254 ContextSetupClaim::WaitForRoot => {
255 unreachable!("context setup reservation resolves waits before scheduling")
256 },
257 };
258 let pool = self.gpu_execution_pool()?;
259 let job_class = match setup_claim {
260 ContextSetupClaim::Root => GpuExecutionJobClass::Lifecycle,
261 ContextSetupClaim::Auxiliary => GpuExecutionJobClass::Execution,
262 ContextSetupClaim::WaitForRoot => {
263 unreachable!("context setup reservation resolves waits before scheduling")
264 },
265 };
266 let observation = self.lifetime.observation.clone();
267 let handle = Arc::clone(self);
268 pool.run_observed_scoped_in_class(
269 GpuExecutionJobKind::ContextSetup,
270 job_class,
271 observation,
272 move |stream_scope| async move {
273 let _root_setup_claim = root_setup_claim;
274 handle.acquire_context_with_cancellation_and_stream_scope(
275 cancellation,
276 stream_scope,
277 setup_claim,
278 stream_acquire_deadline,
279 auxiliary_reservation,
280 )
281 },
282 )
283 .await
284 .and_then(|result| result)
285 }
286
287 pub fn gpu_executor(&self) -> Result<GpuExecutorLease> {
290 self.gpu_execution_pool().map(GpuExecutorLease::from_pool)
291 }
292
293 pub(crate) fn gpu_execution_pool(&self) -> Result<GpuExecutionPool> {
294 let operation = "query_engine.query_service.gpu_execution_pool";
295 let query_anchor = self.lifetime.acquire_resource_anchor(operation)?;
296 let service = self.lifetime.service()?;
297 let resources = &service.resources;
298 let pool = resources.gpu_execution_pool_for_device(self.device_ordinal)?;
299 let resource_epoch =
300 QueryServiceResourceEpoch::acquire(&service.service_epoch, Some(&query_anchor), operation)?;
301 Ok(pool.with_resource_epoch(resource_epoch))
302 }
303
304 pub fn acquire_context_with_cancellation(
307 &self,
308 cancellation: ExecutionCancellation,
309 ) -> Result<AdmittedQueryContext> {
310 let operation = "query_engine.query_service.context";
311 let setup_claim = self.lifetime.reserve_context_setup_sync(&cancellation, operation)?;
312 let _root_setup_claim = (setup_claim == ContextSetupClaim::Root)
313 .then(|| RootContextSetupClaimGuard::new(Arc::clone(&self.lifetime)));
314 let stream_acquire_deadline = self.context_stream_acquire_deadline(&cancellation);
315 self.acquire_context_with_cancellation_and_stream_scope(
316 cancellation,
317 RuntimeStreamLeaseScope::default(),
318 setup_claim,
319 stream_acquire_deadline,
320 None,
321 )
322 }
323
324 fn acquire_context_with_cancellation_and_stream_scope(
325 &self,
326 cancellation: ExecutionCancellation,
327 stream_scope: RuntimeStreamLeaseScope,
328 setup_claim: ContextSetupClaim,
329 stream_acquire_deadline: Option<Instant>,
330 auxiliary_reservation: Option<AuxiliaryReservation>,
331 ) -> Result<AdmittedQueryContext> {
332 let operation = "query_engine.query_service.context";
333 cancellation.check(operation)?;
337 let query_anchor = self.lifetime.acquire_resource_anchor(operation)?;
338 let service = self.lifetime.service()?;
339 let resources = &service.resources;
340 let device = resources.devices.context_by_ordinal(self.device_ordinal)?;
341 let template = self.lifetime.with_resources(|lifetime| Ok(lifetime.root_context.clone()))?;
342 if let Some(template) = template {
343 if setup_claim != ContextSetupClaim::Auxiliary {
344 return Err(QueryError::new(
345 QueryErrorCode::Internal,
346 operation,
347 "root context was ready without an auxiliary context-setup reservation",
348 )
349 .with_fact("reason", "context_setup_claim_mismatch")
350 .with_fact("context_setup_claim", "root"));
351 }
352 let context = self.clone_auxiliary_context(
357 &cancellation,
358 device,
359 template,
360 stream_scope,
361 stream_acquire_deadline,
362 auxiliary_reservation,
363 )?;
364 let context = context.with_query_resource_anchor(query_anchor);
365 self.lifetime.query_epoch.check_open(operation).map_err(|_error| self.lifetime.released_error())?;
366 return Ok(AdmittedQueryContext { context });
367 }
368 if setup_claim != ContextSetupClaim::Root {
369 return Err(QueryError::new(
370 QueryErrorCode::Internal,
371 operation,
372 "auxiliary context setup ran before the admitted root context was ready",
373 )
374 .with_fact("reason", "context_setup_claim_mismatch")
375 .with_fact("context_setup_claim", "auxiliary"));
376 }
377 debug_assert!(auxiliary_reservation.is_none(), "root context setup cannot carry an auxiliary reservation");
378 let context = self.bind_root_execution_context(cancellation, stream_scope, device, &service, resources)?;
382 self.lifetime.complete_root_context_setup();
383 let context = context.with_query_resource_anchor(query_anchor);
384 self.lifetime.query_epoch.check_open(operation).map_err(|_error| self.lifetime.released_error())?;
385 Ok(AdmittedQueryContext { context })
386 }
387
388 fn clone_auxiliary_context(
389 &self,
390 cancellation: &ExecutionCancellation,
391 device: &DeviceContext,
392 template: ExecutionContext,
393 stream_scope: RuntimeStreamLeaseScope,
394 stream_acquire_deadline: Option<Instant>,
395 auxiliary_reservation: Option<AuxiliaryReservation>,
396 ) -> Result<ExecutionContext> {
397 let operation = "query_engine.query_service.context.stream_acquire";
398 let (attempt_auxiliary_permit, stream_lease) = match auxiliary_reservation {
399 Some(AuxiliaryReservation { permit, stream }) => (permit, stream.into_lease(operation)?),
400 None => {
401 let permit = self.lifetime.attempt_auxiliary_slots.acquire_with_cancellation_deadline(
402 cancellation,
403 stream_acquire_deadline,
404 "query_engine.query_service.context.attempt_auxiliary_acquire",
405 )?;
406 let stream_lease = device.stream_pool().acquire_observed_with_cancellation_deadline_and_class(
407 cancellation,
408 stream_acquire_deadline,
409 operation,
410 self.lifetime.observation.clone(),
411 stream_scope,
412 StreamPermitClass::Auxiliary,
413 )?;
414 (permit, stream_lease)
415 },
416 };
417 validate_service_stream_pool_device(device.stream_pool(), stream_lease.device())?;
418 template.with_owned_auxiliary_stream(stream_lease, attempt_auxiliary_permit)
419 }
420
421 fn bind_root_execution_context(
427 &self,
428 cancellation: ExecutionCancellation,
429 stream_scope: RuntimeStreamLeaseScope,
430 device: &DeviceContext,
431 service: &QueryServiceInner,
432 resources: &QueryServiceResources,
433 ) -> Result<ExecutionContext> {
434 let device_runtime = resources.device_runtime(self.device_ordinal)?;
435 let (binding, memory_grant, allocation_domain, attempt) = self.lifetime.with_resources(|lifetime| {
436 let envelope = &lifetime.envelope;
437 if let Some(scope) = envelope.worker_scope() {
441 debug_assert_eq!(scope.device_ordinal(), self.device_ordinal);
442 debug_assert_eq!(scope.lane_index(), envelope.lane_index().expect("lane matches its scope"));
443 debug_assert_eq!(scope.pool_id(), device_runtime.gpu_execution_pool.snapshot().pool_id);
444 }
445 let binding = envelope.take_root_binding()?;
446 let memory_grant = envelope.memory_grant().clone();
447 let allocation_domain = envelope.domain().cloned().ok_or_else(|| self.lifetime.released_error())?;
448 let attempt = lifetime.attempt().ok_or_else(|| self.lifetime.released_error())?;
449 Ok((binding, memory_grant, allocation_domain, attempt))
450 })?;
451 #[cfg(test)]
452 self.lifetime.wait_at_root_context_binding_gate();
453 validate_service_stream_pool_device(device.stream_pool(), binding.stream_lease.device())?;
454 debug_assert_eq!(allocation_domain.grant_identity(), memory_grant.identity());
457 debug_assert_eq!(memory_grant.identity().attempt_id, self.lifetime.attempt_id);
458 debug_assert_eq!(memory_grant.device_ordinal(), binding.stream_lease.device().ordinal());
459 let runtime = binding.stream_lease.shared_runtime()?;
460 let stream_provider_route = Arc::new(
461 allocation_domain
462 .create_stream_provider_route(runtime.stream(), self.lifetime.attempt_auxiliary_slots.capacity())?,
463 );
464 let query_id = self.lifetime.observation.query_id().map(|query_id| QueryId::new(query_id.as_str()));
465 let trace_id = self.lifetime.observation.trace_id().map(|trace_id| TraceId::new(trace_id.as_str()));
466 let attempt = AdmittedAttempt {
467 attempt_id: self.lifetime.attempt_id,
468 service_epoch: service.service_epoch.clone(),
469 query_epoch: self.lifetime.query_epoch.clone(),
470 cancellation,
471 attempt,
472 #[cfg(feature = "cugraph")]
473 cache_scope_allocator_correlation: self.lifetime.cache_scope_allocator_correlation(),
474 plan_identities: Arc::clone(&self.lifetime.admitted_plan_identities),
475 memory_control_policy: memory_control_policy_for_grant(self.memory_control_policy(), &memory_grant),
476 auxiliary_slots: Arc::clone(&self.lifetime.attempt_auxiliary_slots),
477 allocation_domain,
478 memory_grant,
479 };
480 let context = ExecutionContext::admitted_root(
483 device_runtime,
484 attempt,
485 StreamBinding::AttemptRoot {
486 runtime,
487 member: binding.member_lease,
488 route: Arc::clone(&stream_provider_route),
489 lease: binding.stream_lease,
490 },
491 PlanScope::root(query_id, trace_id, self.lifetime.observation.clone(), stream_scope),
492 );
493 self.lifetime.with_resources(|lifetime| {
494 debug_assert!(
495 lifetime.attempt_stream_provider_route.is_none(),
496 "an admitted attempt can install only one stream-provider route"
497 );
498 lifetime.attempt_stream_provider_route = Some(Arc::clone(&stream_provider_route));
499 lifetime.root_context = Some(context.clone());
500 Ok(())
501 })?;
502 Ok(context)
503 }
504}
505
506#[derive(Debug)]
508pub struct AdmittedQueryContext {
509 context: ExecutionContext,
510}
511
512impl AdmittedQueryContext {
513 pub fn context(&self) -> &ExecutionContext {
515 &self.context
516 }
517}