#[cfg(any(test, feature = "test-hooks"))]
use std::sync::{Mutex, PoisonError};
use std::{
sync::Arc,
time::{Duration, Instant},
};
#[cfg(test)]
use query_planning::policy::PlanningPolicy;
use query_planning::{correlation::TraceId, error::QueryErrorCode};
#[cfg(any(test, feature = "test-hooks"))]
use super::RuntimeTerminalOutcome;
use super::{
AdmissionRequestId, AdmittedAttempt, AggregateStrategyExecutionOptions, AttemptCapabilityEvidence,
ExecutionCancellation, ExecutionContext, GpuExecutionJobKind, GpuExecutionPool, MemoryControlPolicy, PlanScope,
QueryError, QueryExecutionConfig, QueryId, QueryService, QueryServiceInner, QueueBounds, Result,
SourceChunkPlanningMode, SourceChunkPlanningOptions, SourceSchedulerPolicy, StatisticalAggregateExecutionMode,
StreamBinding, memory_control_policy_for_grant, resources::QueryServiceResources,
validate_service_stream_pool_device,
};
#[cfg(any(test, feature = "test-hooks"))]
use crate::attempt::AttemptOwner;
use crate::{
observability::{AttemptAdmissionWaitFact, RuntimeStreamLeaseScope},
runtime::{
GpuExecutorLease, StreamPermitClass,
context::{AttemptAuxiliaryPermit, QueryServiceResourceEpoch},
gpu::{DeviceContext, GrantIdentity},
gpu_execution_pool::GpuExecutionJobClass,
service_stream::StreamSlot,
},
};
mod lifetime;
mod release;
mod report_commit;
pub(super) use lifetime::QueryHandleLifetime;
use lifetime::{ContextSetupClaim, RootContextSetupClaimGuard};
#[derive(Debug)]
struct AuxiliaryReservation {
permit: AttemptAuxiliaryPermit,
stream: StreamSlot,
}
#[derive(Debug)]
pub struct QueryHandle {
pub(super) config: QueryExecutionConfig,
pub(super) queue_bounds: QueueBounds,
pub(super) admission_request_id: AdmissionRequestId,
pub(super) admission_sequence: u64,
pub(super) admission_wait: Duration,
pub(super) admission_wait_fact: Option<AttemptAdmissionWaitFact>,
pub(super) device_budget_bytes: u64,
pub(super) device_ordinal: usize,
pub(super) lifetime: Arc<QueryHandleLifetime>,
#[cfg(any(test, feature = "test-hooks"))]
pub(super) test_attempt: Mutex<Option<AttemptOwner>>,
}
impl QueryHandle {
pub fn admission_request_id(&self) -> AdmissionRequestId {
self.admission_request_id
}
pub fn admission_sequence(&self) -> u64 {
self.admission_sequence
}
pub fn admission_wait(&self) -> Duration {
self.admission_wait
}
pub fn admission_wait_fact(&self) -> Option<AttemptAdmissionWaitFact> {
self.admission_wait_fact
}
pub fn device_budget_bytes(&self) -> u64 {
self.device_budget_bytes
}
pub fn device_ordinal(&self) -> usize {
self.device_ordinal
}
#[cfg(any(test, feature = "test-hooks"))]
pub(in crate::runtime::query_service) fn own_test_attempt(&self, owner: Option<AttemptOwner>) {
*self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner) = owner;
}
#[cfg(any(test, feature = "test-hooks"))]
#[doc(hidden)]
pub fn end_test_attempt(&self) {
let owner = self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner).take();
drop(owner);
}
#[cfg(any(test, feature = "test-hooks"))]
#[doc(hidden)]
pub fn commit_test_attempt(&self, outcome: RuntimeTerminalOutcome) {
let owner = self.test_attempt.lock().unwrap_or_else(PoisonError::into_inner).take();
if let Some(owner) = owner {
owner.commit_terminal(outcome).expect("a test attempt commits one terminal outcome");
}
}
pub fn join_fragment(&self, fragment: &AttemptCapabilityEvidence) -> Result<()> {
let service = QueryService { inner: self.lifetime.service()? };
service.inner.admission.join_admitted_fragment(
self.device_ordinal,
&self.lifetime.admitted_plan_identities,
fragment,
)
}
pub fn grant_identity(&self) -> Result<GrantIdentity> {
self.lifetime.with_resources(|resources| Ok(resources.envelope.memory_grant().identity()))
}
pub fn memory_control_policy(&self) -> MemoryControlPolicy {
self.config.memory_policy_for_admitted_query(self.device_budget_bytes)
}
pub fn source_scheduler_policy(&self) -> SourceSchedulerPolicy {
self.config.source_scheduler_policy
}
pub fn source_chunk_planning_mode(&self) -> SourceChunkPlanningMode {
self.config.source_chunk_planning_mode
}
pub fn statistical_aggregate_execution_mode(&self) -> StatisticalAggregateExecutionMode {
self.config.statistical_aggregate_execution_mode
}
#[cfg(test)]
pub(crate) fn planning_policy(&self) -> PlanningPolicy {
self.config.planning_policy
}
pub fn aggregate_strategy_execution_options(&self) -> AggregateStrategyExecutionOptions {
self.config.aggregate_strategy_execution_options
}
pub fn source_chunk_planning_options(&self) -> SourceChunkPlanningOptions {
self.config.source_chunk_planning_options
}
async fn reserve_auxiliary_stream(
&self,
cancellation: &ExecutionCancellation,
deadline: Option<Instant>,
) -> Result<StreamSlot> {
let operation = "query_engine.query_service.context.stream_acquire";
let service = self.lifetime.service()?;
let resources = &service.resources;
let pool = resources.devices.context_by_ordinal(self.device_ordinal)?.stream_pool().clone();
pool.reserve_async(
cancellation,
deadline,
operation,
self.lifetime.observation.clone(),
RuntimeStreamLeaseScope::default(),
StreamPermitClass::Auxiliary,
)
.await
}
fn context_stream_acquire_deadline(&self, cancellation: &ExecutionCancellation) -> Option<Instant> {
self.queue_bounds.wait_deadline(Instant::now(), cancellation.deadline())
}
pub fn acquire_context(&self) -> Result<AdmittedQueryContext> {
self.acquire_context_with_cancellation(ExecutionCancellation::new())
}
#[cfg(test)]
pub(crate) async fn acquire_context_async(self: &Arc<Self>) -> Result<AdmittedQueryContext> {
self.acquire_context_with_cancellation_async(ExecutionCancellation::new()).await
}
pub async fn acquire_context_with_cancellation_async(
self: &Arc<Self>,
cancellation: ExecutionCancellation,
) -> Result<AdmittedQueryContext> {
let operation = "query_engine.query_service.context";
let setup_claim = self.lifetime.reserve_context_setup_async(&cancellation, operation).await?;
let root_setup_claim = (setup_claim == ContextSetupClaim::Root)
.then(|| RootContextSetupClaimGuard::new(Arc::clone(&self.lifetime)));
let stream_acquire_deadline = self.context_stream_acquire_deadline(&cancellation);
let auxiliary_reservation = match setup_claim {
ContextSetupClaim::Auxiliary => {
let permit = self
.lifetime
.attempt_auxiliary_slots
.acquire_with_cancellation_deadline_async(
&cancellation,
stream_acquire_deadline,
"query_engine.query_service.context.attempt_auxiliary_acquire",
)
.await?;
let stream = self.reserve_auxiliary_stream(&cancellation, stream_acquire_deadline).await?;
Some(AuxiliaryReservation { permit, stream })
},
ContextSetupClaim::Root => None,
ContextSetupClaim::WaitForRoot => {
unreachable!("context setup reservation resolves waits before scheduling")
},
};
let pool = self.gpu_execution_pool()?;
let job_class = match setup_claim {
ContextSetupClaim::Root => GpuExecutionJobClass::Lifecycle,
ContextSetupClaim::Auxiliary => GpuExecutionJobClass::Execution,
ContextSetupClaim::WaitForRoot => {
unreachable!("context setup reservation resolves waits before scheduling")
},
};
let observation = self.lifetime.observation.clone();
let handle = Arc::clone(self);
pool.run_observed_scoped_in_class(
GpuExecutionJobKind::ContextSetup,
job_class,
observation,
move |stream_scope| async move {
let _root_setup_claim = root_setup_claim;
handle.acquire_context_with_cancellation_and_stream_scope(
cancellation,
stream_scope,
setup_claim,
stream_acquire_deadline,
auxiliary_reservation,
)
},
)
.await
.and_then(|result| result)
}
pub fn gpu_executor(&self) -> Result<GpuExecutorLease> {
self.gpu_execution_pool().map(GpuExecutorLease::from_pool)
}
pub(crate) fn gpu_execution_pool(&self) -> Result<GpuExecutionPool> {
let operation = "query_engine.query_service.gpu_execution_pool";
let query_anchor = self.lifetime.acquire_resource_anchor(operation)?;
let service = self.lifetime.service()?;
let resources = &service.resources;
let pool = resources.gpu_execution_pool_for_device(self.device_ordinal)?;
let resource_epoch =
QueryServiceResourceEpoch::acquire(&service.service_epoch, Some(&query_anchor), operation)?;
Ok(pool.with_resource_epoch(resource_epoch))
}
pub fn acquire_context_with_cancellation(
&self,
cancellation: ExecutionCancellation,
) -> Result<AdmittedQueryContext> {
let operation = "query_engine.query_service.context";
let setup_claim = self.lifetime.reserve_context_setup_sync(&cancellation, operation)?;
let _root_setup_claim = (setup_claim == ContextSetupClaim::Root)
.then(|| RootContextSetupClaimGuard::new(Arc::clone(&self.lifetime)));
let stream_acquire_deadline = self.context_stream_acquire_deadline(&cancellation);
self.acquire_context_with_cancellation_and_stream_scope(
cancellation,
RuntimeStreamLeaseScope::default(),
setup_claim,
stream_acquire_deadline,
None,
)
}
fn acquire_context_with_cancellation_and_stream_scope(
&self,
cancellation: ExecutionCancellation,
stream_scope: RuntimeStreamLeaseScope,
setup_claim: ContextSetupClaim,
stream_acquire_deadline: Option<Instant>,
auxiliary_reservation: Option<AuxiliaryReservation>,
) -> Result<AdmittedQueryContext> {
let operation = "query_engine.query_service.context";
cancellation.check(operation)?;
let query_anchor = self.lifetime.acquire_resource_anchor(operation)?;
let service = self.lifetime.service()?;
let resources = &service.resources;
let device = resources.devices.context_by_ordinal(self.device_ordinal)?;
let template = self.lifetime.with_resources(|lifetime| Ok(lifetime.root_context.clone()))?;
if let Some(template) = template {
if setup_claim != ContextSetupClaim::Auxiliary {
return Err(QueryError::new(
QueryErrorCode::Internal,
operation,
"root context was ready without an auxiliary context-setup reservation",
)
.with_fact("reason", "context_setup_claim_mismatch")
.with_fact("context_setup_claim", "root"));
}
let context = self.clone_auxiliary_context(
&cancellation,
device,
template,
stream_scope,
stream_acquire_deadline,
auxiliary_reservation,
)?;
let context = context.with_query_resource_anchor(query_anchor);
self.lifetime.query_epoch.check_open(operation).map_err(|_error| self.lifetime.released_error())?;
return Ok(AdmittedQueryContext { context });
}
if setup_claim != ContextSetupClaim::Root {
return Err(QueryError::new(
QueryErrorCode::Internal,
operation,
"auxiliary context setup ran before the admitted root context was ready",
)
.with_fact("reason", "context_setup_claim_mismatch")
.with_fact("context_setup_claim", "auxiliary"));
}
debug_assert!(auxiliary_reservation.is_none(), "root context setup cannot carry an auxiliary reservation");
let context = self.bind_root_execution_context(cancellation, stream_scope, device, &service, resources)?;
self.lifetime.complete_root_context_setup();
let context = context.with_query_resource_anchor(query_anchor);
self.lifetime.query_epoch.check_open(operation).map_err(|_error| self.lifetime.released_error())?;
Ok(AdmittedQueryContext { context })
}
fn clone_auxiliary_context(
&self,
cancellation: &ExecutionCancellation,
device: &DeviceContext,
template: ExecutionContext,
stream_scope: RuntimeStreamLeaseScope,
stream_acquire_deadline: Option<Instant>,
auxiliary_reservation: Option<AuxiliaryReservation>,
) -> Result<ExecutionContext> {
let operation = "query_engine.query_service.context.stream_acquire";
let (attempt_auxiliary_permit, stream_lease) = match auxiliary_reservation {
Some(AuxiliaryReservation { permit, stream }) => (permit, stream.into_lease(operation)?),
None => {
let permit = self.lifetime.attempt_auxiliary_slots.acquire_with_cancellation_deadline(
cancellation,
stream_acquire_deadline,
"query_engine.query_service.context.attempt_auxiliary_acquire",
)?;
let stream_lease = device.stream_pool().acquire_observed_with_cancellation_deadline_and_class(
cancellation,
stream_acquire_deadline,
operation,
self.lifetime.observation.clone(),
stream_scope,
StreamPermitClass::Auxiliary,
)?;
(permit, stream_lease)
},
};
validate_service_stream_pool_device(device.stream_pool(), stream_lease.device())?;
template.with_owned_auxiliary_stream(stream_lease, attempt_auxiliary_permit)
}
fn bind_root_execution_context(
&self,
cancellation: ExecutionCancellation,
stream_scope: RuntimeStreamLeaseScope,
device: &DeviceContext,
service: &QueryServiceInner,
resources: &QueryServiceResources,
) -> Result<ExecutionContext> {
let device_runtime = resources.device_runtime(self.device_ordinal)?;
let (binding, memory_grant, allocation_domain, attempt) = self.lifetime.with_resources(|lifetime| {
let envelope = &lifetime.envelope;
if let Some(scope) = envelope.worker_scope() {
debug_assert_eq!(scope.device_ordinal(), self.device_ordinal);
debug_assert_eq!(scope.lane_index(), envelope.lane_index().expect("lane matches its scope"));
debug_assert_eq!(scope.pool_id(), device_runtime.gpu_execution_pool.snapshot().pool_id);
}
let binding = envelope.take_root_binding()?;
let memory_grant = envelope.memory_grant().clone();
let allocation_domain = envelope.domain().cloned().ok_or_else(|| self.lifetime.released_error())?;
let attempt = lifetime.attempt().ok_or_else(|| self.lifetime.released_error())?;
Ok((binding, memory_grant, allocation_domain, attempt))
})?;
#[cfg(test)]
self.lifetime.wait_at_root_context_binding_gate();
validate_service_stream_pool_device(device.stream_pool(), binding.stream_lease.device())?;
debug_assert_eq!(allocation_domain.grant_identity(), memory_grant.identity());
debug_assert_eq!(memory_grant.identity().attempt_id, self.lifetime.attempt_id);
debug_assert_eq!(memory_grant.device_ordinal(), binding.stream_lease.device().ordinal());
let runtime = binding.stream_lease.shared_runtime()?;
let stream_provider_route = Arc::new(
allocation_domain
.create_stream_provider_route(runtime.stream(), self.lifetime.attempt_auxiliary_slots.capacity())?,
);
let query_id = self.lifetime.observation.query_id().map(|query_id| QueryId::new(query_id.as_str()));
let trace_id = self.lifetime.observation.trace_id().map(|trace_id| TraceId::new(trace_id.as_str()));
let attempt = AdmittedAttempt {
attempt_id: self.lifetime.attempt_id,
service_epoch: service.service_epoch.clone(),
query_epoch: self.lifetime.query_epoch.clone(),
cancellation,
attempt,
#[cfg(feature = "cugraph")]
cache_scope_allocator_correlation: self.lifetime.cache_scope_allocator_correlation(),
plan_identities: Arc::clone(&self.lifetime.admitted_plan_identities),
memory_control_policy: memory_control_policy_for_grant(self.memory_control_policy(), &memory_grant),
auxiliary_slots: Arc::clone(&self.lifetime.attempt_auxiliary_slots),
allocation_domain,
memory_grant,
};
let context = ExecutionContext::admitted_root(
device_runtime,
attempt,
StreamBinding::AttemptRoot {
runtime,
member: binding.member_lease,
route: Arc::clone(&stream_provider_route),
lease: binding.stream_lease,
},
PlanScope::root(query_id, trace_id, self.lifetime.observation.clone(), stream_scope),
);
self.lifetime.with_resources(|lifetime| {
debug_assert!(
lifetime.attempt_stream_provider_route.is_none(),
"an admitted attempt can install only one stream-provider route"
);
lifetime.attempt_stream_provider_route = Some(Arc::clone(&stream_provider_route));
lifetime.root_context = Some(context.clone());
Ok(())
})?;
Ok(context)
}
}
#[derive(Debug)]
pub struct AdmittedQueryContext {
context: ExecutionContext,
}
impl AdmittedQueryContext {
pub fn context(&self) -> &ExecutionContext {
&self.context
}
}