#[cfg(any(test, feature = "test-hooks"))]
use std::sync::Mutex;
use std::{
collections::BTreeMap,
sync::{Arc, MutexGuard, atomic::Ordering},
time::Duration,
};
use query_planning::{
admission::AttemptCapabilityEvidence,
error::{QueryError, QueryErrorCode, Result},
};
use super::{
super::{
admission::{AdmissionState, AdmissionTicket, PlacedAdmission, PlacedQuery, QueryCompletion},
admission_rejection,
handle::{QueryHandle, QueryHandleLifetime},
record_admission_lifecycle,
},
QueryService, QueryServiceInner,
};
#[cfg(any(test, feature = "test-hooks"))]
use crate::runtime::{deadline::block_until, gpu_execution_pool::GpuExecutionPool};
use crate::{
attempt::AttemptLifecycleError,
observability::{
AdmissionLifecycleState, AdmissionRequestId, AttemptAdmissionFact, AttemptAdmissionWaitFact, AttemptId,
RuntimeObservationHandle,
},
runtime::{
ExecutionCancellation, GpuExecutionJobKind,
gpu_execution_pool::GpuExecutionJobClass,
query_service::{
controller::admission_mutex_poisoned_error,
service::transaction::{
AdmissionEnvelope, AdmissionTransactionProbe, check_publication_cancellation,
pause_after_rollback_accounting,
},
},
},
};
mod enqueue;
struct EnvelopeRequest {
placed_query: PlacedQuery,
lifecycle_ticket: AdmissionTicket,
execution_cancellation: ExecutionCancellation,
observation: RuntimeObservationHandle,
}
impl EnvelopeRequest {
fn abandon(self, service: &Arc<QueryServiceInner>, error: QueryError) -> QueryError {
AdmissionEnvelope::abandon(service, self.placed_query, self.lifecycle_ticket);
error
}
fn acquire(
self,
service: &Arc<QueryServiceInner>,
probe: Option<&AdmissionTransactionProbe>,
) -> Result<AdmissionEnvelope> {
AdmissionEnvelope::acquire(
service,
self.placed_query,
self.lifecycle_ticket,
&self.execution_cancellation,
&self.observation,
probe,
)
}
}
struct AdmissionPublication {
observation: RuntimeObservationHandle,
attempt_id: AttemptId,
completion: QueryCompletion,
admitted_device_requirements: BTreeMap<usize, AttemptCapabilityEvidence>,
submitted_plan_capability_evidence: Option<AttemptCapabilityEvidence>,
admitted_compatible_devices: Vec<usize>,
admission_request_id: AdmissionRequestId,
admission_wait: Duration,
execution_cancellation: ExecutionCancellation,
admission_wait_fact: Option<AttemptAdmissionWaitFact>,
}
impl QueryService {
pub(in crate::runtime::query_service) async fn commit_placed_admission_async(
&self,
commit: PlacedAdmission,
probe: Option<Arc<AdmissionTransactionProbe>>,
) -> Result<QueryHandle> {
let (request, publication) = self.begin_placed_commit(commit);
let envelope = self.acquire_envelope_on_gpu_pool(request, probe.clone()).await;
self.finish_placed_commit(envelope, publication, &probe)
}
#[cfg(any(test, feature = "test-hooks"))]
pub(in crate::runtime::query_service) fn commit_placed_admission(
&self,
commit: PlacedAdmission,
probe: Option<Arc<AdmissionTransactionProbe>>,
) -> Result<QueryHandle> {
let pool = self.inner.resources.gpu_execution_pool_for_device(commit.placed_query.device_ordinal).ok();
let _runtime = pool.as_ref().map(GpuExecutionPool::enter_runtime);
block_until(self.commit_placed_admission_async(commit, probe), None)
.expect("a commit wait without a deadline yields its result")
}
async fn acquire_envelope_on_gpu_pool(
&self,
request: EnvelopeRequest,
probe: Option<Arc<AdmissionTransactionProbe>>,
) -> Result<AdmissionEnvelope> {
let pool = match self.inner.resources.gpu_execution_pool_for_device(request.placed_query.device_ordinal) {
Ok(pool) => pool,
Err(error) => return Err(request.abandon(&self.inner, error)),
};
let kind = GpuExecutionJobKind::ContextSetup;
let class_permit = match pool
.wait_for_class_permit(kind, GpuExecutionJobClass::Lifecycle, &request.execution_cancellation)
.await
{
Ok(class_permit) => class_permit,
Err(error) => return Err(request.abandon(&self.inner, error)),
};
let service = Arc::clone(&self.inner);
let observation = request.observation.clone();
pool.run_observed_scoped_with_class_permit(kind, class_permit, observation, move |_scope| async move {
request.acquire(&service, probe.as_deref())
})
.await
.and_then(|envelope| envelope)
}
fn begin_placed_commit(&self, commit: PlacedAdmission) -> (EnvelopeRequest, AdmissionPublication) {
let PlacedAdmission {
placed_query,
admitted_device_requirements,
submitted_plan_capability_evidence,
admitted_compatible_devices,
observation,
attempt_id,
completion,
admission_request_id,
admission_wait,
execution_cancellation,
admission_wait_fact,
lifecycle_ticket,
} = commit;
let request = EnvelopeRequest {
placed_query,
lifecycle_ticket,
execution_cancellation: execution_cancellation.clone(),
observation: observation.clone(),
};
let publication = AdmissionPublication {
observation,
attempt_id,
completion,
admitted_device_requirements,
submitted_plan_capability_evidence,
admitted_compatible_devices,
admission_request_id,
admission_wait,
execution_cancellation,
admission_wait_fact,
};
(request, publication)
}
fn finish_placed_commit(
&self,
envelope: Result<AdmissionEnvelope>,
publication: AdmissionPublication,
probe: &Option<Arc<AdmissionTransactionProbe>>,
) -> Result<QueryHandle> {
let envelope = envelope.map(|mut envelope| {
envelope.register_allocator_domain_attempt(publication.attempt_id);
envelope
});
let (state, admission_poisoned) = match self.inner.admission.state.lock() {
Ok(state) => (state, false),
Err(error) => (error.into_inner(), true),
};
let envelope = match envelope {
Ok(envelope) => envelope,
Err(primary) => {
return Err(self.finish_rejected_envelope(
state,
primary,
&publication.observation,
publication.admission_request_id,
publication.completion,
));
},
};
self.publish_admission_envelope(state, admission_poisoned, envelope, publication, probe)
}
fn finish_rejected_envelope(
&self,
state: MutexGuard<'_, AdmissionState>,
primary: QueryError,
observation: &RuntimeObservationHandle,
admission_request_id: AdmissionRequestId,
completion: QueryCompletion,
) -> QueryError {
if primary.code() == QueryErrorCode::Cancelled {
record_admission_lifecycle(
observation,
admission_request_id,
AdmissionLifecycleState::CancellationRequested,
state.queued.len(),
None,
None,
);
record_admission_lifecycle(
observation,
admission_request_id,
AdmissionLifecycleState::Cancelled,
state.queued.len(),
None,
None,
);
} else if primary.fact_value("reason") == Some("queue_wait_elapsed") {
record_admission_lifecycle(
observation,
admission_request_id,
AdmissionLifecycleState::TimedOut,
state.queued.len(),
None,
None,
);
} else {
record_admission_lifecycle(
observation,
admission_request_id,
admission_rejection(&primary),
state.queued.len(),
None,
None,
);
}
self.inner.admission.notify_admission_available();
drop(state);
if primary.fact_value("reason") == Some("queue_wait_elapsed") {
completion.finish_timeout(&primary);
} else {
completion.finish_error(&primary);
}
primary
}
fn publish_admission_envelope(
&self,
mut state: MutexGuard<'_, AdmissionState>,
admission_poisoned: bool,
mut envelope: AdmissionEnvelope,
publication: AdmissionPublication,
probe: &Option<Arc<AdmissionTransactionProbe>>,
) -> Result<QueryHandle> {
let AdmissionPublication {
observation,
attempt_id,
completion,
admitted_device_requirements,
submitted_plan_capability_evidence,
admitted_compatible_devices,
admission_request_id,
admission_wait,
execution_cancellation,
admission_wait_fact,
} = publication;
let admitted_capability_evidence = submitted_plan_capability_evidence;
if let Err(error) = check_publication_cancellation(&execution_cancellation, probe.as_deref()) {
envelope.preaccount_rollback_cleanup(&mut state);
record_admission_lifecycle(
&observation,
admission_request_id,
AdmissionLifecycleState::CancellationRequested,
state.queued.len(),
None,
None,
);
record_admission_lifecycle(
&observation,
admission_request_id,
AdmissionLifecycleState::Cancelled,
state.queued.len(),
None,
None,
);
self.inner.admission.notify_admission_available();
drop(state);
pause_after_rollback_accounting(probe.as_deref());
drop(envelope);
self.inner.admission.notify_admission_available();
completion.finish_error(&error);
return Err(error);
}
let rejection = if admission_poisoned {
Some(admission_mutex_poisoned_error())
} else {
self.inner.admission.admission_terminal_rejection(&state)
};
if let Some(error) = rejection {
envelope.preaccount_rollback_cleanup(&mut state);
record_admission_lifecycle(
&observation,
admission_request_id,
admission_rejection(&error),
state.queued.len(),
None,
None,
);
self.inner.admission.notify_admission_available();
drop(state);
pause_after_rollback_accounting(probe.as_deref());
drop(envelope);
self.inner.admission.notify_admission_available();
completion.finish_error(&error);
return Err(error);
}
if let Err(error) = envelope.activate_lifecycle_ticket() {
envelope.preaccount_rollback_cleanup(&mut state);
self.inner.admission.notify_admission_available();
drop(state);
pause_after_rollback_accounting(probe.as_deref());
drop(envelope);
self.inner.admission.notify_admission_available();
completion.finish_error(&error);
return Err(error);
}
let admission_sequence = self.inner.admission.next_admission_sequence.fetch_add(1, Ordering::Relaxed);
let device_ordinal = envelope.device_ordinal();
let device_budget_bytes = envelope.memory_grant().budget_bytes();
let admitted_fact = AttemptAdmissionFact {
admission_sequence,
admission_wait_ns: u64::try_from(admission_wait.as_nanos()).unwrap_or(u64::MAX),
device_ordinal,
grant_bytes: device_budget_bytes,
};
if let Err(lifecycle_error) = completion.record_attempt_admitted(admitted_fact, admission_wait_fact) {
let error = attempt_terminal_before_admission_error(&lifecycle_error);
envelope.preaccount_rollback_cleanup(&mut state);
record_admission_lifecycle(
&observation,
admission_request_id,
admission_rejection(&error),
state.queued.len(),
None,
None,
);
self.inner.admission.notify_admission_available();
drop(state);
pause_after_rollback_accounting(probe.as_deref());
drop(envelope);
self.inner.admission.notify_admission_available();
completion.finish_error(&error);
return Err(error);
}
let completion = completion.into_admitted(&mut envelope);
record_admission_lifecycle(
&observation,
admission_request_id,
AdmissionLifecycleState::Admitted,
state.queued.len(),
Some(device_ordinal),
Some(device_budget_bytes),
);
drop(state);
Ok(QueryHandle {
lifetime: QueryHandleLifetime::new(
Arc::clone(&self.inner),
admission_request_id,
observation,
attempt_id,
completion,
envelope,
admitted_capability_evidence,
admitted_device_requirements,
admitted_compatible_devices,
),
config: self.inner.config.clone(),
queue_bounds: self.inner.admission.queue_bounds.clone(),
admission_request_id,
admission_sequence,
admission_wait,
admission_wait_fact,
device_budget_bytes,
device_ordinal,
#[cfg(any(test, feature = "test-hooks"))]
test_attempt: Mutex::new(None),
})
}
}
fn attempt_terminal_before_admission_error(error: &AttemptLifecycleError) -> QueryError {
QueryError::new(
QueryErrorCode::Cancelled,
"query_engine.query_service.admission_transaction.publish",
"the attempt ended before its admission was published",
)
.with_fact("reason", "attempt_terminal_before_admission")
.with_source(error.clone())
}