1#[cfg(test)]
4use std::time::Duration;
5use std::{fmt, time::Instant};
6
7#[cfg(test)]
8use query_planning::error::QueryErrorCode;
9use query_planning::{
10 admission::AttemptCapabilityEvidence,
11 error::{QueryError, Result},
12};
13use serde::Serialize;
14
15#[cfg(any(test, feature = "test-hooks"))]
16use crate::attempt::AttemptOwner;
17#[cfg(test)]
18use crate::runtime::query_service::config::QueueBounds;
19use crate::{
20 attempt::{AttemptAdmission, AttemptAdmissionBinding, AttemptLifecycleError},
21 observability::{AttemptId, RuntimeObservationHandle},
22 runtime::{
23 ExecutionCancellation,
24 cache::{CacheSourceEventSnapshot, DeviceCacheReport},
25 gpu::{DeviceLedgerReport, DeviceMemoryFailureReport},
26 gpu_execution_pool::{GpuExecutionPoolSnapshot, GpuExecutionQuerySnapshot},
27 },
28};
29
30#[cfg(feature = "qualification")]
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct DeviceQuarantineInjection {
39 device_ordinal: usize,
40 device_health_generation: u64,
41 newly_quarantined: bool,
42}
43
44#[cfg(feature = "qualification")]
45impl DeviceQuarantineInjection {
46 pub(crate) fn new(device_ordinal: usize, device_health_generation: u64, newly_quarantined: bool) -> Self {
47 Self { device_ordinal, device_health_generation, newly_quarantined }
48 }
49
50 pub fn device_ordinal(self) -> usize {
52 self.device_ordinal
53 }
54
55 pub fn device_health_generation(self) -> u64 {
57 self.device_health_generation
58 }
59
60 pub fn newly_quarantined(self) -> bool {
62 self.newly_quarantined
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
68pub struct QueryServiceAdmissionSnapshot {
69 pub active_queries: usize,
71 pub queued_queries: usize,
73 pub queue_high_watermark: usize,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct BackendDiagnostics {
85 pub admission: QueryServiceAdmissionSnapshot,
87 pub ledgers: Vec<DeviceLedgerReport>,
89 pub caches: Vec<DeviceCacheReport>,
92 pub cache_source_events: Vec<CacheSourceEventSnapshot>,
94 pub memory_failures: Vec<DeviceMemoryFailureReport>,
96 pub execution_pools: Vec<GpuExecutionPoolSnapshot>,
98 pub execution_queries: GpuExecutionQuerySnapshot,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
140#[cfg(test)]
141pub(crate) enum QueryAdmissionOutcome {
142 Capacity { queued_queries: Option<usize>, queue_capacity: Option<usize>, max_queue_wait: Option<Duration> },
146 BudgetUnavailable { requested_cap_bytes: Option<u64>, largest_eligible_cap_bytes: Option<u64> },
153 CapabilityRejected,
156 InvalidInput,
158 Cancelled,
160 ExecutionFailure,
167}
168
169#[cfg(test)]
170impl QueryAdmissionOutcome {
171 #[cfg(test)]
177 pub(crate) fn from_error(error: &QueryError) -> Self {
178 Self::from_error_with_optional_queue_bounds(error, None)
179 }
180
181 #[cfg(test)]
184 pub(crate) fn from_error_with_queue_bounds(error: &QueryError, queue_bounds: &QueueBounds) -> Self {
185 Self::from_error_with_optional_queue_bounds(error, Some(queue_bounds))
186 }
187
188 #[cfg(test)]
195 pub(crate) fn from_execution_error(error: &QueryError) -> Self {
196 if error.code() == QueryErrorCode::Cancelled { Self::Cancelled } else { Self::ExecutionFailure }
197 }
198
199 #[cfg(test)]
200 fn from_error_with_optional_queue_bounds(error: &QueryError, queue_bounds: Option<&QueueBounds>) -> Self {
201 match error.code() {
202 QueryErrorCode::QueryBudgetUnavailable => Self::BudgetUnavailable {
203 requested_cap_bytes: fact_u64(error, "requested_cap_bytes"),
204 largest_eligible_cap_bytes: fact_u64(error, "largest_eligible_cap_bytes"),
205 },
206 QueryErrorCode::ServiceOverloaded | QueryErrorCode::MemoryReservationFailed => Self::Capacity {
207 queued_queries: fact_usize(error, "queued_queries"),
208 queue_capacity: fact_usize(error, "max_waiters")
209 .or_else(|| queue_bounds.map(|bounds| bounds.max_waiters)),
210 max_queue_wait: queue_bounds.map(|bounds| bounds.max_wait),
211 },
212 QueryErrorCode::UnsupportedPipelineContract
213 | QueryErrorCode::UnsupportedOperator
214 | QueryErrorCode::UnsupportedExpression
215 | QueryErrorCode::UnsupportedType
216 | QueryErrorCode::CudfUnsupported => Self::CapabilityRejected,
217 QueryErrorCode::InvalidPlan
218 | QueryErrorCode::InvalidAttemptCorrelation
219 | QueryErrorCode::InvalidConfiguration
220 | QueryErrorCode::GraphInputInvalid
221 | QueryErrorCode::CudfInvalidInput => Self::InvalidInput,
222 QueryErrorCode::Cancelled => Self::Cancelled,
223 QueryErrorCode::CudfTemporaryFailure
224 | QueryErrorCode::CudfPersistentFailure
225 | QueryErrorCode::AlgorithmDidNotConverge
226 | QueryErrorCode::CudfDependencyFailure
227 | QueryErrorCode::SourceAccessDenied
228 | QueryErrorCode::SourceUnauthenticated
229 | QueryErrorCode::SourceObjectNotFound
230 | QueryErrorCode::SourceTemporaryFailure
231 | QueryErrorCode::CudfInvariantViolation
232 | QueryErrorCode::QueryMemoryLimitExceeded
233 | QueryErrorCode::DeviceOutOfMemory
234 | QueryErrorCode::DeviceAllocationFailed
235 | QueryErrorCode::DeviceFault
236 | QueryErrorCode::CleanupFailed
237 | QueryErrorCode::InternalInvariantViolation
238 | QueryErrorCode::SizeOverflow
239 | QueryErrorCode::HostMaterializationLimitExceeded
240 | QueryErrorCode::Internal => Self::ExecutionFailure,
241 }
242 }
243
244 #[cfg(test)]
246 pub(crate) fn queued_queries(&self) -> Option<usize> {
247 match self {
248 Self::Capacity { queued_queries, .. } => *queued_queries,
249 _ => None,
250 }
251 }
252
253 #[cfg(test)]
256 pub(crate) fn queue_capacity(&self) -> Option<usize> {
257 match self {
258 Self::Capacity { queue_capacity, .. } => *queue_capacity,
259 _ => None,
260 }
261 }
262
263 #[cfg(test)]
265 pub(crate) fn max_queue_wait(&self) -> Option<Duration> {
266 match self {
267 Self::Capacity { max_queue_wait, .. } => *max_queue_wait,
268 _ => None,
269 }
270 }
271}
272
273#[cfg(test)]
274fn fact_usize(error: &QueryError, key: &str) -> Option<usize> {
275 error.fact_value(key)?.parse().ok()
276}
277
278#[cfg(test)]
279fn fact_u64(error: &QueryError, key: &str) -> Option<u64> {
280 error.fact_value(key)?.parse().ok()
281}
282
283#[cfg(test)]
284mod outcome_tests {
285 use super::*;
286
287 fn error(code: QueryErrorCode) -> QueryError {
288 QueryError::new(code, "query_engine.test", "test error")
289 }
290
291 #[test]
292 fn capacity_projection_preserves_queue_facts_and_configured_wait_bound() {
293 let error = error(QueryErrorCode::ServiceOverloaded)
294 .with_fact("reason", "admission_queue_full")
295 .with_fact("queued_queries", 7)
296 .with_fact("max_waiters", 8);
297 let bounds = QueueBounds { max_waiters: 8, max_wait: Duration::from_secs(11) };
298
299 let outcome = QueryAdmissionOutcome::from_error_with_queue_bounds(&error, &bounds);
300 assert_eq!(outcome.queued_queries(), Some(7));
301 assert_eq!(outcome.queue_capacity(), Some(8));
302 assert_eq!(outcome.max_queue_wait(), Some(Duration::from_secs(11)));
303 assert!(matches!(outcome, QueryAdmissionOutcome::Capacity { .. }));
304 }
305
306 #[test]
307 fn admission_projection_is_exhaustive_and_does_not_parse_reason_strings() {
308 let cases = [
309 (QueryErrorCode::MemoryReservationFailed, "capacity"),
310 (QueryErrorCode::QueryBudgetUnavailable, "budget_unavailable"),
313 (QueryErrorCode::UnsupportedPipelineContract, "capability"),
314 (QueryErrorCode::InvalidPlan, "invalid"),
315 (QueryErrorCode::InvalidConfiguration, "invalid"),
316 (QueryErrorCode::Cancelled, "cancelled"),
317 (QueryErrorCode::CudfInvariantViolation, "execution"),
318 ];
319 for (code, expected) in cases {
320 let outcome = QueryAdmissionOutcome::from_error(&error(code).with_fact("reason", "unrelated_reason_name"));
321 let actual = match outcome {
322 QueryAdmissionOutcome::Capacity { .. } => "capacity",
323 QueryAdmissionOutcome::BudgetUnavailable { .. } => "budget_unavailable",
324 QueryAdmissionOutcome::CapabilityRejected => "capability",
325 QueryAdmissionOutcome::InvalidInput => "invalid",
326 QueryAdmissionOutcome::Cancelled => "cancelled",
327 QueryAdmissionOutcome::ExecutionFailure => "execution",
328 };
329 assert_eq!(actual, expected, "classification for {code:?}");
330 }
331 let bounds = QueueBounds { max_waiters: 8, max_wait: Duration::from_secs(11) };
334 let budget = QueryAdmissionOutcome::from_error_with_queue_bounds(
335 &error(QueryErrorCode::QueryBudgetUnavailable).with_fact("queued_queries", 7).with_fact("max_waiters", 8),
336 &bounds,
337 );
338 assert_eq!(budget.queued_queries(), None);
339 assert_eq!(budget.queue_capacity(), None);
340 assert_eq!(budget.max_queue_wait(), None);
341 }
342
343 #[test]
344 fn capacity_projection_leaves_unknown_queue_metrics_unknown() {
345 let outcome = QueryAdmissionOutcome::from_error(&error(QueryErrorCode::MemoryReservationFailed));
346 assert!(matches!(
347 outcome,
348 QueryAdmissionOutcome::Capacity { queued_queries: None, queue_capacity: None, max_queue_wait: None }
349 ));
350
351 let bounds = QueueBounds { max_waiters: 4, max_wait: Duration::from_secs(3) };
352 let outcome = QueryAdmissionOutcome::from_error_with_queue_bounds(
353 &error(QueryErrorCode::MemoryReservationFailed),
354 &bounds,
355 );
356 assert_eq!(outcome.queued_queries(), None);
357 assert_eq!(outcome.queue_capacity(), Some(4));
358 assert_eq!(outcome.max_queue_wait(), Some(Duration::from_secs(3)));
359 }
360
361 #[test]
362 fn execution_projection_does_not_reclassify_post_admission_resource_errors() {
363 for code in [QueryErrorCode::MemoryReservationFailed, QueryErrorCode::DeviceAllocationFailed] {
364 assert_eq!(
365 QueryAdmissionOutcome::from_execution_error(&error(code)),
366 QueryAdmissionOutcome::ExecutionFailure,
367 "post-admission {code:?} is an execution failure"
368 );
369 }
370 assert_eq!(
371 QueryAdmissionOutcome::from_execution_error(&error(QueryErrorCode::Cancelled)),
372 QueryAdmissionOutcome::Cancelled
373 );
374 }
375}
376
377#[derive(Debug)]
379#[non_exhaustive]
380pub struct QueryAdmissionRequest {
381 requested_query_cap_bytes: Option<u64>,
384 pub(in crate::runtime::query_service) attempt_requirement: Option<AttemptCapabilityEvidence>,
391 planned_capability_evidence: Option<AttemptCapabilityEvidence>,
393 pub(crate) queue_deadline: Option<Instant>,
396 pub(crate) execution_cancellation: ExecutionCancellation,
401 attempt: AttemptAdmissionBinding,
403 runtime_observation_activity: Option<Result<QueryActivityLease>>,
404 #[cfg(any(test, feature = "test-hooks"))]
407 pub(in crate::runtime::query_service) test_attempt: Option<AttemptOwner>,
408}
409
410pub(in crate::runtime::query_service) struct AdmissionObservation {
411 pub(in crate::runtime::query_service) handle: RuntimeObservationHandle,
412 pub(in crate::runtime::query_service) activity: Option<QueryActivityLease>,
413 pub(in crate::runtime::query_service) activity_error: Option<QueryError>,
414 pub(in crate::runtime::query_service) attempt_id: AttemptId,
417}
418
419pub struct QueryActivityLease {
425 _lease: Box<dyn Send + Sync>,
426}
427
428impl QueryActivityLease {
429 pub fn new(lease: impl Send + Sync + 'static) -> Self {
430 Self { _lease: Box::new(lease) }
431 }
432}
433
434impl fmt::Debug for QueryActivityLease {
435 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
436 formatter.debug_struct("QueryActivityLease").finish_non_exhaustive()
437 }
438}
439
440impl QueryAdmissionRequest {
441 pub fn new(attempt: AttemptAdmissionBinding) -> Self {
448 Self {
449 requested_query_cap_bytes: None,
450 attempt_requirement: None,
451 planned_capability_evidence: None,
452 queue_deadline: None,
453 execution_cancellation: ExecutionCancellation::default(),
454 attempt,
455 runtime_observation_activity: None,
456 #[cfg(any(test, feature = "test-hooks"))]
457 test_attempt: None,
458 }
459 }
460
461 #[cfg(any(test, feature = "test-hooks"))]
468 #[doc(hidden)]
469 pub fn test_attempt() -> Self {
470 let owner = AttemptOwner::native_for_test();
471 let mut request = Self::new(owner.admission_binding());
472 request.test_attempt = Some(owner);
473 request
474 }
475
476 #[cfg(any(test, feature = "test-hooks"))]
479 pub(in crate::runtime::query_service) fn split_test_attempt(mut self) -> (Self, Option<AttemptOwner>) {
480 let owner = self.test_attempt.take();
481 (self, owner)
482 }
483
484 pub fn with_query_cap_bytes(mut self, query_cap_bytes: u64) -> Self {
486 self.requested_query_cap_bytes = Some(query_cap_bytes);
487 self
488 }
489
490 pub(in crate::runtime::query_service) const fn requested_query_cap_bytes(&self) -> Option<u64> {
491 self.requested_query_cap_bytes
492 }
493
494 pub fn with_query_plan_capability_evidence(mut self, evidence: AttemptCapabilityEvidence) -> Self {
501 self.planned_capability_evidence = Some(evidence);
502 self
503 }
504
505 #[cfg(test)]
510 pub(crate) fn with_attempt_requirement(mut self, attempt_requirement: AttemptCapabilityEvidence) -> Self {
511 self.attempt_requirement = Some(attempt_requirement);
512 self
513 }
514
515 pub(in crate::runtime::query_service) fn resolved_attempt_requirement(&self) -> Option<AttemptCapabilityEvidence> {
516 self.planned_capability_evidence.clone().or_else(|| self.attempt_requirement.clone())
517 }
518
519 pub(in crate::runtime::query_service) fn submitted_plan_capability_evidence(
524 &self,
525 ) -> Option<AttemptCapabilityEvidence> {
526 self.planned_capability_evidence.clone()
527 }
528
529 pub fn with_queue_deadline(mut self, queue_deadline: Instant) -> Self {
531 self.queue_deadline = Some(queue_deadline);
532 self
533 }
534
535 pub fn with_execution_cancellation(mut self, execution_cancellation: ExecutionCancellation) -> Self {
537 self.execution_cancellation = execution_cancellation;
538 self
539 }
540
541 #[cfg(test)]
543 pub(crate) fn with_runtime_observation_activity(mut self, activity: QueryActivityLease) -> Self {
544 self.runtime_observation_activity = Some(Ok(activity));
545 self
546 }
547
548 #[cfg(test)]
554 pub(crate) fn with_runtime_observation_activity_result(mut self, activity: Result<QueryActivityLease>) -> Self {
555 self.runtime_observation_activity = Some(activity);
556 self
557 }
558
559 pub(in crate::runtime::query_service) fn into_admission_observation(self) -> AdmissionObservation {
560 let (activity, activity_error) = match self.runtime_observation_activity {
561 Some(Ok(activity)) => (Some(activity), None),
562 Some(Err(error)) => (None, Some(error)),
563 None => (None, None),
564 };
565 AdmissionObservation {
566 handle: self.attempt.observation_handle(),
567 activity,
568 activity_error,
569 attempt_id: self.attempt.attempt_id(),
570 }
571 }
572
573 pub(in crate::runtime::query_service) fn claim_attempt_admission(
575 &self,
576 ) -> std::result::Result<AttemptAdmission, AttemptLifecycleError> {
577 self.attempt.claim()
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use std::sync::Arc;
584
585 use super::*;
586 use crate::{
587 attempt::{AttemptBackendCounters, BeginAttemptRequest},
588 observability::{AttemptDisposition, RuntimeObservationConfig},
589 runtime::BackendLifecycle,
590 };
591
592 #[test]
593 fn attempt_binding_attaches_observation_and_release_together() {
594 let lifecycle = BackendLifecycle::new();
595 let attempt = AttemptOwner::begin(
596 BeginAttemptRequest {
597 observation: RuntimeObservationConfig::bounded(8, 8 * 1024),
598 ..BeginAttemptRequest::default()
599 },
600 lifecycle.try_begin_observation().expect("attempt observation permit"),
601 Arc::new(AttemptBackendCounters::default()),
602 None,
603 );
604 let attempt_id = attempt.owner.attempt_id();
605 attempt.owner.handle().set_disposition(AttemptDisposition::Native).expect("native disposition");
606 let request = QueryAdmissionRequest::new(attempt.owner.admission_binding());
607
608 assert!(request.claim_attempt_admission().is_ok());
609 assert!(matches!(request.claim_attempt_admission(), Err(AttemptLifecycleError::AdmissionAlreadyClaimed)));
610 let observation = request.into_admission_observation();
611 assert_eq!(observation.attempt_id, attempt_id);
612 }
613}