1use std::fmt;
4
5use query_planning::error::{
6 AllocationSource, AttemptFailureCode, QueryError, QueryErrorCode, QueryErrorStatus, Result,
7};
8use serde::{
9 Deserialize, Serialize,
10 de::{self, SeqAccess, Visitor},
11};
12
13use crate::observability::{AttemptComposedScheduleProjection, AttemptId};
14
15pub const QUERY_ATTEMPT_REPORT_SCHEMA: &str = "query_engine.query_attempt_report";
17pub const SEALED_ATTEMPT_PROJECTION_SCHEMA: &str = "query_engine.sealed_attempt_projection";
23pub const MAX_ATTEMPT_QUERY_ID_BYTES: usize = 256;
25pub const MAX_ATTEMPT_TRACE_CONTEXT_BYTES: usize = 1024;
27
28macro_rules! attempt_correlation {
29 ($name:ident, $limit:ident, $field:literal, $doc:literal) => {
30 #[doc = $doc]
31 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
32 #[serde(transparent)]
33 pub struct $name(String);
34 impl $name {
35 pub fn new(value: impl Into<String>) -> Result<Self> {
41 let value = value.into();
42 if value.trim().is_empty() || value.len() > $limit {
43 return Err(QueryError::new(
44 QueryErrorCode::InvalidAttemptCorrelation,
45 "query_engine.attempt.correlation",
46 "attempt correlation must be non-blank and within its byte limit",
47 )
48 .with_fact("field", $field)
49 .with_fact("max_bytes", $limit)
50 .with_fact("reason", if value.trim().is_empty() { "blank" } else { "too_long" }));
51 }
52 Ok(Self(value))
53 }
54 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58 }
59 impl fmt::Display for $name {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(&self.0)
62 }
63 }
64 impl<'de> Deserialize<'de> for $name {
65 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
66 where D: serde::Deserializer<'de> {
67 let value = String::deserialize(deserializer)?;
70 Self::new(value).map_err(de::Error::custom)
71 }
72 }
73 };
74}
75attempt_correlation!(
76 AttemptQueryId,
77 MAX_ATTEMPT_QUERY_ID_BYTES,
78 "query_id",
79 "Opaque caller query correlation, limited to 256 UTF-8 bytes and required to be non-blank."
80);
81attempt_correlation!(
82 AttemptTraceContext,
83 MAX_ATTEMPT_TRACE_CONTEXT_BYTES,
84 "trace_context",
85 "Opaque caller trace correlation, limited to 1024 UTF-8 bytes and required to be non-blank."
86);
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum AttemptDisposition {
92 Unplanned,
94 CpuFallback,
96 PartialNative,
98 Native,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum AttemptCleanupDisposition {
106 NotRequired,
108 Completed,
110 CompletedWithFailures,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum AttemptCapacityDisposition {
118 NotAdmitted,
120 Released,
122 FrozenResident,
124 RetainedQuarantine,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum AttemptHealthTransition {
137 HealthyReuse,
139 DeviceQuarantined,
141 BackendUnhealthy,
143}
144
145pub(crate) const MAX_ATTEMPT_FAILURE_TEXT_BYTES: usize = 256;
150#[cfg(test)]
151const MAX_ATTEMPT_FAILURE_TEXT_INSPECTION_BYTES: usize = 1024;
152const MAX_ATTEMPT_FAILURE_FACTS: usize = 4;
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum AttemptFailureOrigin {
158 Native,
160 Datafusion,
162 AttemptControl,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum AttemptFailureStatus {
170 NotApplicable,
172 Permanent,
174 Temporary,
176 Persistent,
178}
179
180impl AttemptFailureStatus {
181 pub(crate) const fn from_query_error_status(status: QueryErrorStatus) -> Self {
182 match status {
183 QueryErrorStatus::Permanent => Self::Permanent,
184 QueryErrorStatus::Temporary => Self::Temporary,
185 QueryErrorStatus::Persistent => Self::Persistent,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193pub enum AttemptFailureOperation {
194 Planning,
196 Admission,
198 Execution,
200 Cleanup,
202 Datafusion,
204 AttemptControl,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum AttemptFailureFactKey {
212 Reason,
214 AllocationSource,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum AttemptFailureFactValue {
222 BackendClosing,
224 DeadlineExpired,
226 CallerCancelled,
228 AdmissionQueueFull,
230 QueueWaitElapsed,
232 MemoryGrantWaitTimeout,
234 RequiredFloorExceedsDeviceCapacity,
236 QueryCapExceedsResidentCapacity,
239 BackendUnhealthy,
241 CugraphRmmOutOfMemory,
243 CugraphRmmBadAlloc,
245 CugraphStdBadAlloc,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub struct AttemptFailureFact {
252 key: AttemptFailureFactKey,
253 value: AttemptFailureFactValue,
254}
255
256impl AttemptFailureFact {
257 pub const fn key(&self) -> AttemptFailureFactKey {
259 self.key
260 }
261
262 pub const fn value(&self) -> AttemptFailureFactValue {
264 self.value
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
270pub(crate) struct AttemptFailureText {
271 value: String,
272 truncated: bool,
273 redacted: bool,
274 invalid_utf8: bool,
275}
276
277#[derive(Deserialize)]
278struct AttemptFailureTextWire {
279 #[serde(deserialize_with = "deserialize_bounded_attempt_failure_text_value")]
280 value: String,
281 truncated: bool,
282 redacted: bool,
283 invalid_utf8: bool,
284}
285
286fn deserialize_bounded_attempt_failure_text_value<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
287where D: serde::Deserializer<'de> {
288 struct BoundedTextVisitor;
289
290 impl Visitor<'_> for BoundedTextVisitor {
291 type Value = String;
292
293 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(formatter, "a UTF-8 string no longer than {MAX_ATTEMPT_FAILURE_TEXT_BYTES} bytes")
295 }
296
297 fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
298 where E: de::Error {
299 if value.len() > MAX_ATTEMPT_FAILURE_TEXT_BYTES {
300 return Err(E::custom("attempt failure text exceeds its byte bound"));
301 }
302 Ok(value.to_owned())
303 }
304
305 fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
306 where E: de::Error {
307 if value.len() > MAX_ATTEMPT_FAILURE_TEXT_BYTES {
308 return Err(E::custom("attempt failure text exceeds its byte bound"));
309 }
310 Ok(value)
311 }
312 }
313
314 deserializer.deserialize_string(BoundedTextVisitor)
315}
316
317impl<'de> Deserialize<'de> for AttemptFailureText {
318 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
319 where D: serde::Deserializer<'de> {
320 let wire = AttemptFailureTextWire::deserialize(deserializer)?;
321 if wire.value.len() > MAX_ATTEMPT_FAILURE_TEXT_BYTES {
322 return Err(de::Error::custom("attempt failure text exceeds its byte bound"));
323 }
324 if wire.redacted {
325 if wire.value != "<redacted>" {
326 return Err(de::Error::custom("redacted attempt failure text must not retain source text"));
327 }
328 } else if attempt_failure_text_requires_redaction(&wire.value.to_ascii_lowercase()) {
329 return Err(de::Error::custom("attempt failure text requires redaction"));
330 }
331 Ok(Self {
332 value: wire.value,
333 truncated: wire.truncated,
334 redacted: wire.redacted,
335 invalid_utf8: wire.invalid_utf8,
336 })
337 }
338}
339
340impl AttemptFailureText {
341 #[cfg(test)]
343 pub(crate) fn from_bytes(bytes: &[u8]) -> Self {
344 let inspected_len = bytes.len().min(MAX_ATTEMPT_FAILURE_TEXT_INSPECTION_BYTES);
345 let inspected = &bytes[..inspected_len];
346 let invalid_utf8 = std::str::from_utf8(inspected).is_err();
347 let mut value = String::from_utf8_lossy(inspected).split_whitespace().collect::<Vec<_>>().join(" ");
348 let mut redacted = false;
349 let lowercase = value.to_ascii_lowercase();
350 if attempt_failure_text_requires_redaction(&lowercase) {
351 value = "<redacted>".to_string();
352 redacted = true;
353 }
354 let (value, truncated_by_bound) = truncate_utf8(&value, MAX_ATTEMPT_FAILURE_TEXT_BYTES);
355 Self { value, truncated: bytes.len() > inspected_len || truncated_by_bound, redacted, invalid_utf8 }
356 }
357
358 pub fn value(&self) -> &str {
359 &self.value
360 }
361
362 #[cfg(test)]
363 pub(crate) const fn truncated(&self) -> bool {
364 self.truncated
365 }
366
367 #[cfg(test)]
368 pub(crate) const fn redacted(&self) -> bool {
369 self.redacted
370 }
371
372 #[cfg(test)]
373 pub(crate) const fn invalid_utf8(&self) -> bool {
374 self.invalid_utf8
375 }
376}
377
378fn attempt_failure_text_requires_redaction(lowercase: &str) -> bool {
379 lowercase.contains("://")
380 || lowercase.contains("password")
381 || lowercase.contains("passwd")
382 || lowercase.contains("credential")
383 || lowercase.contains("authorization")
384 || lowercase.contains("token")
385 || lowercase.contains("secret")
386 || lowercase.contains("api_key")
387 || lowercase.contains("access_key")
388 || lowercase.contains("bearer ")
389 || looks_like_sql_statement(lowercase)
390}
391
392fn looks_like_sql_statement(value: &str) -> bool {
393 let value = value.trim_start();
394 ["select", "insert", "update", "delete", "merge", "with", "create", "alter", "drop"].into_iter().any(|keyword| {
395 value.strip_prefix(keyword).is_some_and(|suffix| {
396 suffix.is_empty()
397 || suffix
398 .chars()
399 .next()
400 .is_some_and(|character| character.is_whitespace() || matches!(character, '*' | '('))
401 })
402 })
403}
404
405#[cfg(test)]
406fn truncate_utf8(value: &str, max_bytes: usize) -> (String, bool) {
407 if value.len() <= max_bytes {
408 return (value.to_string(), false);
409 }
410 let mut end = max_bytes;
411 while !value.is_char_boundary(end) {
412 end -= 1;
413 }
414 (value[..end].to_string(), true)
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
419pub struct AttemptFailureSummary {
420 origin: AttemptFailureOrigin,
421 code: AttemptFailureCode,
422 status: AttemptFailureStatus,
423 operation: AttemptFailureOperation,
424 text: Option<AttemptFailureText>,
425 facts: Vec<AttemptFailureFact>,
426}
427
428#[derive(Deserialize)]
429struct AttemptFailureSummaryWire {
430 origin: AttemptFailureOrigin,
431 code: AttemptFailureCode,
432 status: AttemptFailureStatus,
433 operation: AttemptFailureOperation,
434 #[serde(default)]
435 text: Option<AttemptFailureText>,
436 #[serde(default, deserialize_with = "deserialize_bounded_attempt_failure_facts")]
437 facts: Vec<AttemptFailureFact>,
438}
439
440fn deserialize_bounded_attempt_failure_facts<'de, D>(
441 deserializer: D,
442) -> std::result::Result<Vec<AttemptFailureFact>, D::Error>
443where D: serde::Deserializer<'de> {
444 struct BoundedFactsVisitor;
445
446 impl<'de> Visitor<'de> for BoundedFactsVisitor {
447 type Value = Vec<AttemptFailureFact>;
448
449 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
450 write!(formatter, "at most {MAX_ATTEMPT_FAILURE_FACTS} attempt failure facts")
451 }
452
453 fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
454 where A: SeqAccess<'de> {
455 let mut facts = Vec::with_capacity(MAX_ATTEMPT_FAILURE_FACTS);
456 while let Some(fact) = sequence.next_element()? {
457 if facts.len() == MAX_ATTEMPT_FAILURE_FACTS {
458 return Err(de::Error::custom("attempt failure facts exceed their fixed bound"));
459 }
460 facts.push(fact);
461 }
462 Ok(facts)
463 }
464 }
465
466 deserializer.deserialize_seq(BoundedFactsVisitor)
467}
468
469impl<'de> Deserialize<'de> for AttemptFailureSummary {
470 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
471 where D: serde::Deserializer<'de> {
472 let wire = AttemptFailureSummaryWire::deserialize(deserializer)?;
473 Self::from_parts(wire.origin, wire.code, wire.status, wire.operation, wire.text, wire.facts)
474 .map_err(de::Error::custom)
475 }
476}
477
478impl AttemptFailureSummary {
479 fn from_parts(
480 origin: AttemptFailureOrigin,
481 code: AttemptFailureCode,
482 status: AttemptFailureStatus,
483 operation: AttemptFailureOperation,
484 text: Option<AttemptFailureText>,
485 facts: Vec<AttemptFailureFact>,
486 ) -> std::result::Result<Self, &'static str> {
487 if facts.len() > MAX_ATTEMPT_FAILURE_FACTS {
488 return Err("attempt failure facts exceed their fixed bound");
489 }
490 if facts.windows(2).any(|pair| pair[0].key >= pair[1].key) {
491 return Err("attempt failure facts must use unique deterministic key order");
492 }
493 Ok(Self { origin, code, status, operation, text, facts })
494 }
495
496 pub fn from_native_error(error: &QueryError, operation: AttemptFailureOperation) -> Self {
504 let mut facts = Vec::with_capacity(2);
505 let reason = match error.fact_value("reason") {
506 Some("backend_closing") => Some(AttemptFailureFactValue::BackendClosing),
507 Some("deadline_expired") => Some(AttemptFailureFactValue::DeadlineExpired),
508 Some("caller_cancelled") => Some(AttemptFailureFactValue::CallerCancelled),
509 Some("admission_queue_full") => Some(AttemptFailureFactValue::AdmissionQueueFull),
510 Some("queue_wait_elapsed") => Some(AttemptFailureFactValue::QueueWaitElapsed),
511 Some("memory_grant_wait_timeout") => Some(AttemptFailureFactValue::MemoryGrantWaitTimeout),
512 Some("required_floor_exceeds_device_capacity") | Some("impossible_requirement") => {
513 Some(AttemptFailureFactValue::RequiredFloorExceedsDeviceCapacity)
514 },
515 Some("query_cap_exceeds_resident_capacity") => {
516 Some(AttemptFailureFactValue::QueryCapExceedsResidentCapacity)
517 },
518 Some("backend_unhealthy") => Some(AttemptFailureFactValue::BackendUnhealthy),
519 _ => None,
520 };
521 if let Some(value) = reason {
522 facts.push(AttemptFailureFact { key: AttemptFailureFactKey::Reason, value });
523 }
524 let allocation_source = match error.context().allocation_source {
525 Some(AllocationSource::RmmOutOfMemory) => Some(AttemptFailureFactValue::CugraphRmmOutOfMemory),
526 Some(AllocationSource::RmmBadAlloc) => Some(AttemptFailureFactValue::CugraphRmmBadAlloc),
527 Some(AllocationSource::StdBadAlloc) => Some(AttemptFailureFactValue::CugraphStdBadAlloc),
528 Some(AllocationSource::Unknown) | None => None,
529 };
530 if let Some(value) = allocation_source {
531 facts.push(AttemptFailureFact { key: AttemptFailureFactKey::AllocationSource, value });
532 }
533 Self {
534 origin: AttemptFailureOrigin::Native,
535 code: AttemptFailureCode::from_query_error_code(error.code()),
536 status: AttemptFailureStatus::from_query_error_status(error.status()),
537 operation,
538 text: None,
539 facts,
540 }
541 }
542
543 pub const fn datafusion_fallback() -> Self {
546 Self {
547 origin: AttemptFailureOrigin::Datafusion,
548 code: AttemptFailureCode::DatafusionError,
549 status: AttemptFailureStatus::Permanent,
550 operation: AttemptFailureOperation::Datafusion,
551 text: None,
552 facts: Vec::new(),
553 }
554 }
555
556 pub const fn backend_closing() -> Self {
558 Self {
559 origin: AttemptFailureOrigin::AttemptControl,
560 code: AttemptFailureCode::BackendClosing,
561 status: AttemptFailureStatus::Permanent,
562 operation: AttemptFailureOperation::Admission,
563 text: None,
564 facts: Vec::new(),
565 }
566 }
567
568 const fn control(code: AttemptFailureCode) -> Self {
569 Self {
570 origin: AttemptFailureOrigin::AttemptControl,
571 code,
572 status: AttemptFailureStatus::NotApplicable,
573 operation: AttemptFailureOperation::AttemptControl,
574 text: None,
575 facts: Vec::new(),
576 }
577 }
578
579 pub const fn origin(&self) -> AttemptFailureOrigin {
581 self.origin
582 }
583
584 pub const fn code(&self) -> AttemptFailureCode {
586 self.code
587 }
588
589 pub const fn status(&self) -> AttemptFailureStatus {
591 self.status
592 }
593
594 pub const fn operation(&self) -> AttemptFailureOperation {
596 self.operation
597 }
598
599 pub(crate) fn text(&self) -> Option<&AttemptFailureText> {
600 self.text.as_ref()
601 }
602
603 pub fn facts(&self) -> &[AttemptFailureFact] {
606 &self.facts
607 }
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum CleanupFailurePhase {
614 SchedulingCycle,
616 RunnableTaskSelection,
618 TaskPreparation,
620 TaskExecution,
622 TaskPanic,
624 TaskCompletion,
626 CompletedResourceRelease,
628 FinalValidation,
630 QueryServiceStreamSynchronization,
632 QueryServiceCallbackAbandoned,
634 QueryServiceCleanup,
636}
637
638#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640pub struct CleanupFailureSummary {
641 phase: CleanupFailurePhase,
642 failure: AttemptFailureSummary,
643 complete: bool,
647}
648
649impl CleanupFailureSummary {
650 pub fn from_native_error(phase: CleanupFailurePhase, error: &QueryError) -> Self {
654 Self {
655 phase,
656 failure: AttemptFailureSummary::from_native_error(error, AttemptFailureOperation::Cleanup),
657 complete: true,
658 }
659 }
660
661 pub(crate) const fn stream_synchronization_failed() -> Self {
662 Self {
663 phase: CleanupFailurePhase::QueryServiceStreamSynchronization,
664 failure: AttemptFailureSummary::control(AttemptFailureCode::StreamSynchronizationFailed),
665 complete: true,
666 }
667 }
668
669 pub(crate) const fn callback_abandoned() -> Self {
670 Self {
671 phase: CleanupFailurePhase::QueryServiceCallbackAbandoned,
672 failure: AttemptFailureSummary::control(AttemptFailureCode::CleanupCallbackAbandoned),
673 complete: true,
674 }
675 }
676
677 pub const fn phase(&self) -> CleanupFailurePhase {
679 self.phase
680 }
681
682 pub const fn failure(&self) -> &AttemptFailureSummary {
684 &self.failure
685 }
686
687 pub const fn complete(&self) -> bool {
690 self.complete
691 }
692
693 pub(crate) fn mark_incomplete(&mut self) {
694 self.complete = false;
695 }
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
706pub struct AttemptFinalDomainUsage {
707 pub domain_id: u64,
709 pub(crate) active: bool,
710 pub(crate) active_members: u64,
711 pub(crate) ceiling_bytes: u64,
712 pub(crate) allocation_limit_bytes: u64,
713 pub current_bytes: u64,
715 pub peak_bytes: u64,
717 pub(crate) total_bytes: u64,
718 pub(crate) current_aligned_bytes: u64,
719 pub peak_aligned_bytes: u64,
721 pub(crate) total_allocations: u64,
722 pub(crate) current_allocations: u64,
723 pub(crate) peak_allocations: u64,
724 pub(crate) failed_allocation_count: u64,
725 pub(crate) largest_failed_allocation_bytes: u64,
726}
727
728impl From<cudf::QueryMemoryDomainUsage> for AttemptFinalDomainUsage {
729 fn from(usage: cudf::QueryMemoryDomainUsage) -> Self {
730 Self {
731 domain_id: usage.domain_id,
732 active: usage.active,
733 active_members: usage.active_members,
734 ceiling_bytes: usage.ceiling_bytes,
735 allocation_limit_bytes: usage.allocation_limit_bytes,
736 current_bytes: usage.current_bytes,
737 peak_bytes: usage.peak_bytes,
738 total_bytes: usage.total_bytes,
739 current_aligned_bytes: usage.current_aligned_bytes,
740 peak_aligned_bytes: usage.peak_aligned_bytes,
741 total_allocations: usage.total_allocations,
742 current_allocations: usage.current_allocations,
743 peak_allocations: usage.peak_allocations,
744 failed_allocation_count: usage.failed_allocation_count,
745 largest_failed_allocation_bytes: usage.largest_failed_allocation_bytes,
746 }
747 }
748}
749
750#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
758#[serde(rename_all = "snake_case")]
759pub(crate) enum AttemptFaultIsolation {
760 DeviceLocal,
761 Unknown,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
769#[serde(rename_all = "snake_case")]
770pub(crate) enum AttemptTeardownHealth {
771 Completed,
772 StreamSynchronizationFailed,
773 CallbackAbandoned,
774}
775
776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
779#[serde(rename_all = "snake_case")]
780pub(crate) enum AttemptDeviceHealthStatus {
781 Healthy,
782 Unhealthy,
783}
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(rename_all = "snake_case")]
792pub(crate) enum AttemptNativeFaultKind {
793 CurrentDeviceQueryFailed,
794 OwnerDeviceEntryFailed,
795 AmbientDeviceRestoreFailed,
796 FatalDeviceOperation,
797 TeardownQuarantined,
798 Unknown,
799}
800
801#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
807#[serde(rename_all = "snake_case")]
808pub(crate) enum AttemptNativeStatus {
809 Cudf(u32),
810 Cuda(i32),
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
815pub(crate) struct AttemptNativeFault {
816 pub(crate) kind: AttemptNativeFaultKind,
817 pub(crate) status: Option<AttemptNativeStatus>,
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
822pub(crate) struct AttemptDeviceHealth {
823 pub(crate) status: AttemptDeviceHealthStatus,
824 pub(crate) fault_count: u64,
825 pub(crate) rejected_operation_count: u64,
826 pub(crate) quarantined_teardown_count: u64,
827}
828
829#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
836pub struct AttemptNativeFaultEvidence {
837 pub(crate) isolation: AttemptFaultIsolation,
838 pub(crate) teardown_health: AttemptTeardownHealth,
839 pub(crate) device_health: Option<AttemptDeviceHealth>,
842 pub(crate) first_fault: Option<AttemptNativeFault>,
843}
844
845#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
851#[serde(rename_all = "snake_case")]
852pub enum AttemptEvidenceProvenance {
853 AttemptExact,
855 DeviceSnapshot,
857 SampledAtTerminalBoundary,
861 Unavailable,
863}
864
865#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
871pub struct AttemptEvidenceField<T> {
872 pub provenance: AttemptEvidenceProvenance,
874 pub value: Option<T>,
876}
877
878impl<T> AttemptEvidenceField<T> {
879 pub(crate) const fn attempt_exact(value: T) -> Self {
880 Self { provenance: AttemptEvidenceProvenance::AttemptExact, value: Some(value) }
881 }
882
883 pub(crate) const fn device_snapshot(value: T) -> Self {
884 Self { provenance: AttemptEvidenceProvenance::DeviceSnapshot, value: Some(value) }
885 }
886
887 pub(crate) const fn sampled_at_terminal_boundary(value: T) -> Self {
888 Self { provenance: AttemptEvidenceProvenance::SampledAtTerminalBoundary, value: Some(value) }
889 }
890
891 pub(crate) const fn unavailable() -> Self {
892 Self { provenance: AttemptEvidenceProvenance::Unavailable, value: None }
893 }
894}
895
896impl<T> Default for AttemptEvidenceField<T> {
897 fn default() -> Self {
898 Self::unavailable()
899 }
900}
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
904pub struct AttemptTimedDomainUsage {
905 pub(crate) sampled_ns: u64,
909 pub(crate) usage: AttemptFinalDomainUsage,
910 pub pool_usage: Option<AttemptAllocatorPoolUsage>,
912}
913
914#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
916#[serde(rename_all = "snake_case")]
917pub enum AttemptAllocatorEventSource {
918 Physical,
920 ReservationLimit,
922 AllocationLimit,
924 Unknown,
926}
927
928#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
930#[serde(rename_all = "snake_case")]
931pub(crate) enum AttemptAllocatorEventAction {
932 Decline,
933 Retry,
934 TrimAndRetry,
935 CallbackPanicked,
936 Unknown,
937}
938
939#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
941pub struct AttemptAllocatorPoolUsage {
942 pub(crate) reserved_current_bytes: u64,
943 pub reserved_high_bytes: u64,
945 pub(crate) used_current_bytes: u64,
946 pub used_high_bytes: u64,
948}
949
950#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
957pub struct AttemptAllocatorEvent {
958 pub sequence: u64,
960 pub(crate) monotonic_time_ns: AttemptEvidenceField<u64>,
961 pub requested_bytes: u64,
963 pub source: AttemptAllocatorEventSource,
965 pub(crate) action: AttemptEvidenceField<AttemptAllocatorEventAction>,
966 pub(crate) callback_attempt: u32,
967 pub(crate) trim_retry_count: u32,
968 pub(crate) pool_usage: AttemptEvidenceField<AttemptAllocatorPoolUsage>,
969}
970
971#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
974pub struct AttemptUnattributedAllocatorEvent {
975 pub(crate) device_ordinal: usize,
976 pub(crate) sampled_ns: u64,
979 pub(crate) event: AttemptAllocatorEvent,
980}
981
982#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
989pub struct AttemptResourceEvidence {
990 pub ledger_sequence: AttemptEvidenceField<u64>,
992 pub(crate) ledger_sample_ns: AttemptEvidenceField<u64>,
993 pub(crate) configured_limit_bytes: AttemptEvidenceField<u64>,
994 pub(crate) headroom_bytes: AttemptEvidenceField<u64>,
995 pub(crate) backend_reserve_bytes: AttemptEvidenceField<u64>,
996 pub(crate) managed_capacity_bytes: AttemptEvidenceField<u64>,
997 pub(crate) live_grants_bytes: AttemptEvidenceField<u64>,
998 pub(crate) resident_owner_bytes: AttemptEvidenceField<u64>,
999 pub cache_evictable_resident_bytes: AttemptEvidenceField<u64>,
1001 pub cache_pinned_in_use_bytes: AttemptEvidenceField<u64>,
1003 pub(crate) active_attempt_count: AttemptEvidenceField<u64>,
1004 pub grant_id: AttemptEvidenceField<u64>,
1006 pub(crate) grant_cap_bytes: AttemptEvidenceField<u64>,
1007 pub domain_id: AttemptEvidenceField<u64>,
1009 pub(crate) domain_cap_bytes: AttemptEvidenceField<u64>,
1010 pub outcome_terminal_usage: AttemptEvidenceField<AttemptTimedDomainUsage>,
1012 pub(crate) post_cleanup_usage: AttemptEvidenceField<AttemptTimedDomainUsage>,
1013 pub allocator_event: AttemptEvidenceField<AttemptAllocatorEvent>,
1015 pub unattributed_device_allocator_event: AttemptEvidenceField<AttemptUnattributedAllocatorEvent>,
1017}
1018
1019impl AttemptResourceEvidence {
1020 pub(crate) const fn unavailable() -> Self {
1022 Self {
1023 ledger_sequence: AttemptEvidenceField::unavailable(),
1024 ledger_sample_ns: AttemptEvidenceField::unavailable(),
1025 configured_limit_bytes: AttemptEvidenceField::unavailable(),
1026 headroom_bytes: AttemptEvidenceField::unavailable(),
1027 backend_reserve_bytes: AttemptEvidenceField::unavailable(),
1028 managed_capacity_bytes: AttemptEvidenceField::unavailable(),
1029 live_grants_bytes: AttemptEvidenceField::unavailable(),
1030 resident_owner_bytes: AttemptEvidenceField::unavailable(),
1031 cache_evictable_resident_bytes: AttemptEvidenceField::unavailable(),
1032 cache_pinned_in_use_bytes: AttemptEvidenceField::unavailable(),
1033 active_attempt_count: AttemptEvidenceField::unavailable(),
1034 grant_id: AttemptEvidenceField::unavailable(),
1035 grant_cap_bytes: AttemptEvidenceField::unavailable(),
1036 domain_id: AttemptEvidenceField::unavailable(),
1037 domain_cap_bytes: AttemptEvidenceField::unavailable(),
1038 outcome_terminal_usage: AttemptEvidenceField::unavailable(),
1039 post_cleanup_usage: AttemptEvidenceField::unavailable(),
1040 allocator_event: AttemptEvidenceField::unavailable(),
1041 unattributed_device_allocator_event: AttemptEvidenceField::unavailable(),
1042 }
1043 }
1044}
1045
1046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1053pub(crate) struct AttemptTerminalCommit {
1054 capacity_disposition: AttemptCapacityDisposition,
1055 final_domain_usage: Option<AttemptFinalDomainUsage>,
1056 health_transition: AttemptHealthTransition,
1057 native_fault_evidence: Option<AttemptNativeFaultEvidence>,
1058 resource_evidence: Option<AttemptResourceEvidence>,
1059 alternate_device_eligibility: AttemptAlternateDeviceEligibility,
1060}
1061
1062#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1066pub(crate) enum AttemptAlternateDeviceEligibility {
1067 NotApplicable,
1068 Eligible,
1069 NoEligibleAlternate,
1070 Unavailable,
1071}
1072
1073impl AttemptTerminalCommit {
1074 pub(crate) const fn new(
1075 capacity_disposition: AttemptCapacityDisposition,
1076 final_domain_usage: Option<AttemptFinalDomainUsage>,
1077 health_transition: AttemptHealthTransition,
1078 ) -> Self {
1079 Self {
1080 capacity_disposition,
1081 final_domain_usage,
1082 health_transition,
1083 native_fault_evidence: None,
1084 resource_evidence: None,
1085 alternate_device_eligibility: AttemptAlternateDeviceEligibility::NotApplicable,
1086 }
1087 }
1088
1089 pub(crate) const fn with_native_fault_evidence(mut self, evidence: AttemptNativeFaultEvidence) -> Self {
1092 self.native_fault_evidence = Some(evidence);
1093 self
1094 }
1095
1096 pub(crate) const fn with_resource_evidence(mut self, evidence: AttemptResourceEvidence) -> Self {
1099 self.resource_evidence = Some(evidence);
1100 self
1101 }
1102
1103 pub(crate) const fn with_alternate_device_eligibility(
1107 mut self,
1108 eligibility: AttemptAlternateDeviceEligibility,
1109 ) -> Self {
1110 self.alternate_device_eligibility = eligibility;
1111 self
1112 }
1113
1114 pub(crate) const fn capacity_disposition(self) -> AttemptCapacityDisposition {
1115 self.capacity_disposition
1116 }
1117
1118 pub(crate) const fn final_domain_usage(self) -> Option<AttemptFinalDomainUsage> {
1119 self.final_domain_usage
1120 }
1121
1122 pub(crate) const fn health_transition(self) -> AttemptHealthTransition {
1123 self.health_transition
1124 }
1125
1126 pub(crate) const fn native_fault_evidence(self) -> Option<AttemptNativeFaultEvidence> {
1127 self.native_fault_evidence
1128 }
1129
1130 pub(crate) const fn resource_evidence(self) -> Option<AttemptResourceEvidence> {
1131 self.resource_evidence
1132 }
1133
1134 pub(crate) const fn alternate_device_eligibility(self) -> AttemptAlternateDeviceEligibility {
1135 self.alternate_device_eligibility
1136 }
1137
1138 pub(crate) const fn has_consistent_capacity_health_transition(self) -> bool {
1139 matches!(
1140 (self.capacity_disposition, self.health_transition),
1141 (AttemptCapacityDisposition::Released, AttemptHealthTransition::HealthyReuse)
1142 | (AttemptCapacityDisposition::FrozenResident, AttemptHealthTransition::HealthyReuse)
1143 | (AttemptCapacityDisposition::RetainedQuarantine, AttemptHealthTransition::DeviceQuarantined)
1144 | (AttemptCapacityDisposition::RetainedQuarantine, AttemptHealthTransition::BackendUnhealthy)
1145 )
1146 }
1147
1148 pub(crate) const fn is_admitted_cleanup_commit(self) -> bool {
1149 !matches!(self.capacity_disposition, AttemptCapacityDisposition::NotAdmitted)
1150 && self.has_consistent_capacity_health_transition()
1151 }
1152}
1153
1154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1160#[serde(rename_all = "snake_case")]
1161pub enum AttemptRetryRecovery {
1162 NoRetryNeeded,
1164 DoNotRetry,
1167 Backoff,
1170 ChangeConfiguration,
1173 RetryAnotherEligibleDevice,
1176 RecoverBackend,
1178 Indeterminate,
1180}
1181
1182impl AttemptRetryRecovery {
1183 pub const fn as_str(self) -> &'static str {
1185 match self {
1186 Self::NoRetryNeeded => "no_retry_needed",
1187 Self::DoNotRetry => "do_not_retry",
1188 Self::Backoff => "backoff",
1189 Self::ChangeConfiguration => "change_configuration",
1190 Self::RetryAnotherEligibleDevice => "retry_another_eligible_device",
1191 Self::RecoverBackend => "recover_backend",
1192 Self::Indeterminate => "indeterminate",
1193 }
1194 }
1195}
1196
1197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1200#[serde(rename_all = "snake_case")]
1201pub enum AttemptRetryPrerequisite {
1202 None,
1204 CapacityAvailable,
1206 ConfigurationChanged,
1208 QuarantinedDeviceAndEligibleAlternate,
1210 BackendRecovered,
1212 EvidenceUnavailable,
1214}
1215
1216impl AttemptRetryPrerequisite {
1217 pub const fn as_str(self) -> &'static str {
1219 match self {
1220 Self::None => "none",
1221 Self::CapacityAvailable => "capacity_available",
1222 Self::ConfigurationChanged => "configuration_changed",
1223 Self::QuarantinedDeviceAndEligibleAlternate => "quarantined_device_and_eligible_alternate",
1224 Self::BackendRecovered => "backend_recovered",
1225 Self::EvidenceUnavailable => "evidence_unavailable",
1226 }
1227 }
1228}
1229
1230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1232pub struct AttemptRetryAdvice {
1233 pub recovery: AttemptRetryRecovery,
1235 pub prerequisite: AttemptRetryPrerequisite,
1237}
1238
1239#[derive(Deserialize)]
1240struct AttemptRetryAdviceWire {
1241 recovery: AttemptRetryRecovery,
1242 prerequisite: AttemptRetryPrerequisite,
1243}
1244
1245impl<'de> Deserialize<'de> for AttemptRetryAdvice {
1246 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1247 where D: serde::Deserializer<'de> {
1248 let wire = AttemptRetryAdviceWire::deserialize(deserializer)?;
1249 Self::from_parts(wire.recovery, wire.prerequisite).map_err(de::Error::custom)
1250 }
1251}
1252
1253impl AttemptRetryAdvice {
1254 fn from_parts(
1255 recovery: AttemptRetryRecovery,
1256 prerequisite: AttemptRetryPrerequisite,
1257 ) -> std::result::Result<Self, &'static str> {
1258 let valid = matches!(
1259 (recovery, prerequisite),
1260 (AttemptRetryRecovery::NoRetryNeeded, AttemptRetryPrerequisite::None)
1261 | (AttemptRetryRecovery::DoNotRetry, AttemptRetryPrerequisite::None)
1262 | (AttemptRetryRecovery::Backoff, AttemptRetryPrerequisite::CapacityAvailable)
1263 | (AttemptRetryRecovery::ChangeConfiguration, AttemptRetryPrerequisite::ConfigurationChanged)
1264 | (
1265 AttemptRetryRecovery::RetryAnotherEligibleDevice,
1266 AttemptRetryPrerequisite::QuarantinedDeviceAndEligibleAlternate
1267 )
1268 | (AttemptRetryRecovery::RecoverBackend, AttemptRetryPrerequisite::BackendRecovered)
1269 | (AttemptRetryRecovery::Indeterminate, AttemptRetryPrerequisite::EvidenceUnavailable)
1270 );
1271 valid.then_some(Self { recovery, prerequisite }).ok_or("retry recovery and prerequisite are inconsistent")
1272 }
1273
1274 pub const fn no_retry_needed() -> Self {
1276 Self { recovery: AttemptRetryRecovery::NoRetryNeeded, prerequisite: AttemptRetryPrerequisite::None }
1277 }
1278
1279 pub const fn do_not_retry() -> Self {
1281 Self { recovery: AttemptRetryRecovery::DoNotRetry, prerequisite: AttemptRetryPrerequisite::None }
1282 }
1283
1284 pub const fn indeterminate() -> Self {
1286 Self {
1287 recovery: AttemptRetryRecovery::Indeterminate,
1288 prerequisite: AttemptRetryPrerequisite::EvidenceUnavailable,
1289 }
1290 }
1291
1292 fn backoff() -> Self {
1293 Self { recovery: AttemptRetryRecovery::Backoff, prerequisite: AttemptRetryPrerequisite::CapacityAvailable }
1294 }
1295
1296 fn change_configuration() -> Self {
1297 Self {
1298 recovery: AttemptRetryRecovery::ChangeConfiguration,
1299 prerequisite: AttemptRetryPrerequisite::ConfigurationChanged,
1300 }
1301 }
1302
1303 fn retry_another_eligible_device() -> Self {
1304 Self {
1305 recovery: AttemptRetryRecovery::RetryAnotherEligibleDevice,
1306 prerequisite: AttemptRetryPrerequisite::QuarantinedDeviceAndEligibleAlternate,
1307 }
1308 }
1309
1310 fn recover_backend() -> Self {
1311 Self {
1312 recovery: AttemptRetryRecovery::RecoverBackend,
1313 prerequisite: AttemptRetryPrerequisite::BackendRecovered,
1314 }
1315 }
1316
1317 pub(crate) fn from_sealed_attempt(
1322 outcome: &RuntimeTerminalOutcome,
1323 primary_failure: Option<&AttemptFailureSummary>,
1324 capacity_disposition: AttemptCapacityDisposition,
1325 health_transition: Option<AttemptHealthTransition>,
1326 native_fault_evidence: Option<AttemptNativeFaultEvidence>,
1327 resource_evidence: Option<AttemptResourceEvidence>,
1328 alternate_device_eligibility: AttemptAlternateDeviceEligibility,
1329 ) -> Self {
1330 if requires_backend_recovery(primary_failure, health_transition, native_fault_evidence) {
1331 return Self::recover_backend();
1332 }
1333
1334 match outcome {
1335 RuntimeTerminalOutcome::Succeeded => Self::no_retry_needed(),
1336 RuntimeTerminalOutcome::Cancelled | RuntimeTerminalOutcome::Dropped | RuntimeTerminalOutcome::Panicked => {
1337 Self::do_not_retry()
1338 },
1339 RuntimeTerminalOutcome::Failed { .. } => {
1340 if has_proven_device_local_fault(capacity_disposition, health_transition, native_fault_evidence) {
1341 return match alternate_device_eligibility {
1342 AttemptAlternateDeviceEligibility::Eligible => Self::retry_another_eligible_device(),
1343 AttemptAlternateDeviceEligibility::NoEligibleAlternate => Self::do_not_retry(),
1344 AttemptAlternateDeviceEligibility::NotApplicable
1345 | AttemptAlternateDeviceEligibility::Unavailable => Self::indeterminate(),
1346 };
1347 }
1348
1349 let Some(failure) = primary_failure else {
1350 return Self::indeterminate();
1351 };
1352 if is_configuration_failure(failure) {
1353 return Self::change_configuration();
1354 }
1355 if has_reason(failure, AttemptFailureFactValue::RequiredFloorExceedsDeviceCapacity)
1356 || has_reason(failure, AttemptFailureFactValue::QueryCapExceedsResidentCapacity)
1357 {
1358 return Self::change_configuration();
1362 }
1363 if is_queue_overload(failure) {
1364 return Self::backoff();
1365 }
1366 if matches!(failure.code(), AttemptFailureCode::Query(QueryErrorCode::MemoryReservationFailed)) {
1367 return match exact_allocator_event_source(resource_evidence) {
1368 Some(
1369 AttemptAllocatorEventSource::ReservationLimit
1370 | AttemptAllocatorEventSource::AllocationLimit,
1371 ) => Self::change_configuration(),
1372 Some(AttemptAllocatorEventSource::Physical) => Self::indeterminate(),
1377 Some(AttemptAllocatorEventSource::Unknown) | None => Self::indeterminate(),
1378 };
1379 }
1380 Self::indeterminate()
1381 },
1382 }
1383 }
1384}
1385
1386fn has_reason(failure: &AttemptFailureSummary, expected: AttemptFailureFactValue) -> bool {
1387 failure.facts().iter().any(|fact| fact.key() == AttemptFailureFactKey::Reason && fact.value() == expected)
1388}
1389
1390fn is_configuration_failure(failure: &AttemptFailureSummary) -> bool {
1391 matches!(
1392 failure.code(),
1393 AttemptFailureCode::Query(QueryErrorCode::UnsupportedPipelineContract)
1394 | AttemptFailureCode::Query(QueryErrorCode::UnsupportedOperator)
1395 | AttemptFailureCode::Query(QueryErrorCode::UnsupportedExpression)
1396 | AttemptFailureCode::Query(QueryErrorCode::UnsupportedType)
1397 | AttemptFailureCode::Query(QueryErrorCode::CudfUnsupported)
1398 | AttemptFailureCode::Query(QueryErrorCode::QueryMemoryLimitExceeded)
1399 )
1400}
1401
1402fn is_queue_overload(failure: &AttemptFailureSummary) -> bool {
1403 failure.code() == AttemptFailureCode::Query(QueryErrorCode::ServiceOverloaded)
1404 && failure.facts().iter().any(|fact| {
1405 fact.key() == AttemptFailureFactKey::Reason
1406 && matches!(
1407 fact.value(),
1408 AttemptFailureFactValue::AdmissionQueueFull
1409 | AttemptFailureFactValue::QueueWaitElapsed
1410 | AttemptFailureFactValue::MemoryGrantWaitTimeout
1411 )
1412 })
1413}
1414
1415fn requires_backend_recovery(
1416 primary_failure: Option<&AttemptFailureSummary>,
1417 health_transition: Option<AttemptHealthTransition>,
1418 native_fault_evidence: Option<AttemptNativeFaultEvidence>,
1419) -> bool {
1420 matches!(health_transition, Some(AttemptHealthTransition::BackendUnhealthy))
1421 || native_fault_evidence.is_some_and(|evidence| evidence.isolation == AttemptFaultIsolation::Unknown)
1422 || primary_failure.is_some_and(|failure| has_reason(failure, AttemptFailureFactValue::BackendUnhealthy))
1423}
1424
1425fn has_proven_device_local_fault(
1426 capacity_disposition: AttemptCapacityDisposition,
1427 health_transition: Option<AttemptHealthTransition>,
1428 native_fault_evidence: Option<AttemptNativeFaultEvidence>,
1429) -> bool {
1430 matches!(capacity_disposition, AttemptCapacityDisposition::RetainedQuarantine)
1431 && matches!(health_transition, Some(AttemptHealthTransition::DeviceQuarantined))
1432 && native_fault_evidence.as_ref().is_some_and(has_proven_device_local_fault_evidence)
1433}
1434
1435pub(crate) fn has_proven_device_local_fault_evidence(evidence: &AttemptNativeFaultEvidence) -> bool {
1440 if evidence.isolation != AttemptFaultIsolation::DeviceLocal {
1441 return false;
1442 }
1443 matches!(evidence.teardown_health, AttemptTeardownHealth::StreamSynchronizationFailed)
1444 || (evidence.device_health.is_some_and(|health| health.status == AttemptDeviceHealthStatus::Unhealthy)
1445 && evidence.first_fault.is_some())
1446}
1447
1448fn exact_allocator_event_source(
1449 resource_evidence: Option<AttemptResourceEvidence>,
1450) -> Option<AttemptAllocatorEventSource> {
1451 let allocator_event = resource_evidence?.allocator_event;
1452 (allocator_event.provenance == AttemptEvidenceProvenance::AttemptExact)
1453 .then_some(allocator_event.value?)
1454 .map(|event| event.source)
1455}
1456
1457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1458pub struct AttemptAdmissionFact {
1460 pub admission_sequence: u64,
1462 pub admission_wait_ns: u64,
1464 pub device_ordinal: usize,
1466 pub grant_bytes: u64,
1468}
1469
1470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1477pub struct AttemptAdmissionWaitFact {
1478 pub submission_sequence: u64,
1480}
1481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1482pub struct AttemptTimestamps {
1486 pub submitted_ns: u64,
1488 pub planning_started_ns: Option<u64>,
1490 pub planning_finished_ns: Option<u64>,
1492 pub admission_requested_ns: Option<u64>,
1495 pub admitted_ns: Option<u64>,
1497 pub physical_execution_started_ns: Option<u64>,
1499 pub gpu_execution_started_ns: Option<u64>,
1501 pub outcome_terminal_ns: u64,
1503 pub cleanup_complete_ns: Option<u64>,
1505 pub report_ready_ns: Option<u64>,
1507}
1508fn deserialize_query_attempt_report_schema<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
1509where D: serde::Deserializer<'de> {
1510 let schema = String::deserialize(deserializer)?;
1511 if schema != QUERY_ATTEMPT_REPORT_SCHEMA {
1512 return Err(de::Error::custom(format!(
1513 "unsupported attempt report schema {schema:?}; expected {QUERY_ATTEMPT_REPORT_SCHEMA:?}"
1514 )));
1515 }
1516 Ok(schema)
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1520pub struct QueryAttemptReport {
1527 #[serde(deserialize_with = "deserialize_query_attempt_report_schema")]
1528 pub schema: String,
1530 pub attempt_id: AttemptId,
1532 pub query_id: Option<AttemptQueryId>,
1534 pub trace_context: Option<AttemptTraceContext>,
1536 pub disposition: AttemptDisposition,
1538 pub outcome: RuntimeTerminalOutcome,
1540 pub primary_failure: Option<AttemptFailureSummary>,
1543 pub retry_advice: AttemptRetryAdvice,
1547 pub device_ordinal: Option<usize>,
1549 pub admission: Option<AttemptAdmissionFact>,
1551 pub admission_wait: Option<AttemptAdmissionWaitFact>,
1555 #[serde(default)]
1559 pub composed_schedule: Option<AttemptComposedScheduleProjection>,
1560 pub timestamps: AttemptTimestamps,
1562 pub cleanup_disposition: AttemptCleanupDisposition,
1564 pub cleanup_failure_count: u64,
1567 pub cleanup_failure_count_complete: bool,
1569 pub first_cleanup_failure: Option<CleanupFailureSummary>,
1572 pub capacity_disposition: AttemptCapacityDisposition,
1574 pub final_domain_usage: Option<AttemptFinalDomainUsage>,
1577 pub health_transition: Option<AttemptHealthTransition>,
1581 pub native_fault_evidence: Option<AttemptNativeFaultEvidence>,
1585 pub resource_evidence: Option<AttemptResourceEvidence>,
1589 pub observation_lost_records: u64,
1591 pub observation_retained_records: u64,
1593 pub observation_retained_bytes: u64,
1595 pub observation_truncated: bool,
1597 pub observation_degraded: bool,
1599 pub unix_epoch_ns: Option<u64>,
1601}
1602
1603impl QueryAttemptReport {
1604 pub fn sealed_projection(&self) -> SealedAttemptProjection {
1609 SealedAttemptProjection::from(self)
1610 }
1611}
1612#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1621pub struct SealedAttemptProjection {
1622 pub schema: &'static str,
1624 pub attempt_id: AttemptId,
1626 pub disposition: AttemptDisposition,
1628 pub outcome: RuntimeTerminalOutcome,
1630 pub cleanup_disposition: AttemptCleanupDisposition,
1632 pub cleanup_failure_count: u64,
1634 pub cleanup_failure_count_complete: bool,
1636 pub capacity_disposition: AttemptCapacityDisposition,
1638 pub health_transition: Option<AttemptHealthTransition>,
1640 pub retry_advice: AttemptRetryAdvice,
1642 pub observation_truncated: bool,
1644 pub observation_degraded: bool,
1646 pub native_fault_evidence_present: bool,
1648 pub resource_evidence_present: bool,
1650}
1651
1652#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1658pub struct SealedAttemptLabels {
1659 pub disposition: &'static str,
1661 pub outcome: &'static str,
1663 pub cleanup_disposition: &'static str,
1665 pub cleanup_failure_count_complete: &'static str,
1667 pub capacity_disposition: &'static str,
1669 pub health_transition: &'static str,
1671 pub retry_recovery: &'static str,
1673 pub retry_prerequisite: &'static str,
1675 pub observation_truncated: &'static str,
1677 pub observation_degraded: &'static str,
1679 pub native_fault_evidence: &'static str,
1681 pub resource_evidence: &'static str,
1683}
1684
1685impl SealedAttemptProjection {
1686 pub const fn labels(&self) -> SealedAttemptLabels {
1688 SealedAttemptLabels {
1689 disposition: match self.disposition {
1690 AttemptDisposition::Unplanned => "unplanned",
1691 AttemptDisposition::CpuFallback => "cpu_fallback",
1692 AttemptDisposition::PartialNative => "partial_native",
1693 AttemptDisposition::Native => "native",
1694 },
1695 outcome: self.outcome.stable_label(),
1696 cleanup_disposition: match self.cleanup_disposition {
1697 AttemptCleanupDisposition::NotRequired => "not_required",
1698 AttemptCleanupDisposition::Completed => "completed",
1699 AttemptCleanupDisposition::CompletedWithFailures => "completed_with_failures",
1700 },
1701 cleanup_failure_count_complete: if self.cleanup_failure_count_complete { "true" } else { "false" },
1702 capacity_disposition: match self.capacity_disposition {
1703 AttemptCapacityDisposition::NotAdmitted => "not_admitted",
1704 AttemptCapacityDisposition::Released => "released",
1705 AttemptCapacityDisposition::FrozenResident => "frozen_resident",
1706 AttemptCapacityDisposition::RetainedQuarantine => "retained_quarantine",
1707 },
1708 health_transition: match self.health_transition {
1709 None => "none",
1710 Some(AttemptHealthTransition::HealthyReuse) => "healthy_reuse",
1711 Some(AttemptHealthTransition::DeviceQuarantined) => "device_quarantined",
1712 Some(AttemptHealthTransition::BackendUnhealthy) => "backend_unhealthy",
1713 },
1714 retry_recovery: self.retry_advice.recovery.as_str(),
1715 retry_prerequisite: self.retry_advice.prerequisite.as_str(),
1716 observation_truncated: if self.observation_truncated { "true" } else { "false" },
1717 observation_degraded: if self.observation_degraded { "true" } else { "false" },
1718 native_fault_evidence: if self.native_fault_evidence_present { "present" } else { "absent" },
1719 resource_evidence: if self.resource_evidence_present { "present" } else { "absent" },
1720 }
1721 }
1722}
1723
1724impl From<&QueryAttemptReport> for SealedAttemptProjection {
1725 fn from(report: &QueryAttemptReport) -> Self {
1726 Self {
1727 schema: SEALED_ATTEMPT_PROJECTION_SCHEMA,
1728 attempt_id: report.attempt_id,
1729 disposition: report.disposition,
1730 outcome: report.outcome.clone(),
1731 cleanup_disposition: report.cleanup_disposition,
1732 cleanup_failure_count: report.cleanup_failure_count,
1733 cleanup_failure_count_complete: report.cleanup_failure_count_complete,
1734 capacity_disposition: report.capacity_disposition,
1735 health_transition: report.health_transition,
1736 retry_advice: report.retry_advice,
1737 observation_truncated: report.observation_truncated,
1738 observation_degraded: report.observation_degraded,
1739 native_fault_evidence_present: report.native_fault_evidence.is_some(),
1740 resource_evidence_present: report.resource_evidence.is_some(),
1741 }
1742 }
1743}
1744impl QueryAttemptReport {
1745 pub fn total_ns(&self) -> Option<u64> {
1748 self.timestamps.report_ready_ns.map(|ready| ready.saturating_sub(self.timestamps.submitted_ns))
1749 }
1750 pub fn planning_ns(&self) -> Option<u64> {
1753 self.timestamps
1754 .planning_started_ns
1755 .zip(self.timestamps.planning_finished_ns)
1756 .map(|(start, end)| end.saturating_sub(start))
1757 }
1758 pub fn queue_wait_ns(&self) -> Option<u64> {
1761 self.timestamps
1762 .admission_requested_ns
1763 .zip(self.timestamps.admitted_ns)
1764 .map(|(start, end)| end.saturating_sub(start))
1765 }
1766 pub fn post_planning_pre_admission_ns(&self) -> Option<u64> {
1769 self.timestamps
1770 .planning_finished_ns
1771 .zip(self.timestamps.admission_requested_ns)
1772 .map(|(start, end)| end.saturating_sub(start))
1773 }
1774 pub fn admitted_to_physical_execution_ns(&self) -> Option<u64> {
1777 self.timestamps
1778 .admitted_ns
1779 .zip(self.timestamps.physical_execution_started_ns)
1780 .map(|(start, end)| end.saturating_sub(start))
1781 }
1782 pub fn physical_to_gpu_dispatch_ns(&self) -> Option<u64> {
1785 self.timestamps
1786 .physical_execution_started_ns
1787 .zip(self.timestamps.gpu_execution_started_ns)
1788 .map(|(start, end)| end.saturating_sub(start))
1789 }
1790 pub fn admitted_to_gpu_dispatch_ns(&self) -> Option<u64> {
1793 self.timestamps
1794 .admitted_ns
1795 .zip(self.timestamps.gpu_execution_started_ns)
1796 .map(|(start, end)| end.saturating_sub(start))
1797 }
1798 pub fn submitted_to_gpu_dispatch_ns(&self) -> Option<u64> {
1801 self.timestamps.gpu_execution_started_ns.map(|end| end.saturating_sub(self.timestamps.submitted_ns))
1802 }
1803 pub fn gpu_dispatch_to_terminal_ns(&self) -> Option<u64> {
1806 self.timestamps.gpu_execution_started_ns.map(|start| self.timestamps.outcome_terminal_ns.saturating_sub(start))
1807 }
1808 pub fn physical_execution_ns(&self) -> Option<u64> {
1811 self.timestamps
1812 .physical_execution_started_ns
1813 .map(|start| self.timestamps.outcome_terminal_ns.saturating_sub(start))
1814 }
1815 pub fn cleanup_ns(&self) -> Option<u64> {
1818 self.timestamps.cleanup_complete_ns.map(|end| end.saturating_sub(self.timestamps.outcome_terminal_ns))
1819 }
1820}
1821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1823#[serde(tag = "kind", rename_all = "snake_case")]
1824pub enum RuntimeTerminalOutcome {
1825 Succeeded,
1827 Failed {
1829 error_code: AttemptFailureCode,
1831 error_status: AttemptFailureStatus,
1833 },
1834 Cancelled,
1836 Dropped,
1838 Panicked,
1840}
1841
1842impl RuntimeTerminalOutcome {
1843 pub const fn stable_label(&self) -> &'static str {
1845 match self {
1846 Self::Succeeded => "succeeded",
1847 Self::Failed { .. } => "failed",
1848 Self::Cancelled => "cancelled",
1849 Self::Dropped => "dropped",
1850 Self::Panicked => "panicked",
1851 }
1852 }
1853
1854 pub const fn failed(summary: &AttemptFailureSummary) -> Self {
1856 Self::Failed { error_code: summary.code(), error_status: summary.status() }
1857 }
1858
1859 pub fn from_query_error(error: &QueryError) -> Self {
1863 if error.code() == QueryErrorCode::Cancelled {
1864 Self::Cancelled
1865 } else {
1866 Self::Failed {
1867 error_code: AttemptFailureCode::from_query_error_code(error.code()),
1868 error_status: AttemptFailureStatus::from_query_error_status(error.status()),
1869 }
1870 }
1871 }
1872
1873 pub(crate) fn primary_failure_from_query_error(
1878 error: &QueryError,
1879 operation: AttemptFailureOperation,
1880 ) -> Option<AttemptFailureSummary> {
1881 match error.code() {
1882 QueryErrorCode::Cancelled if error.fact_value("reason") != Some("deadline_expired") => None,
1883 _ => Some(AttemptFailureSummary::from_native_error(error, operation)),
1884 }
1885 }
1886
1887 pub(crate) const fn from_primary_failure(summary: &AttemptFailureSummary) -> Self {
1888 match summary.code() {
1889 AttemptFailureCode::Query(QueryErrorCode::Cancelled) => Self::Cancelled,
1890 _ => Self::failed(summary),
1891 }
1892 }
1893
1894 pub(crate) fn fallback_failure_summary(&self) -> Option<AttemptFailureSummary> {
1895 match self {
1896 Self::Succeeded => None,
1897 Self::Failed { error_code, error_status } => Some(AttemptFailureSummary {
1898 origin: AttemptFailureOrigin::Native,
1899 code: *error_code,
1900 status: *error_status,
1901 operation: AttemptFailureOperation::Execution,
1902 text: None,
1903 facts: Vec::new(),
1904 }),
1905 Self::Cancelled => {
1906 Some(AttemptFailureSummary::control(AttemptFailureCode::Query(QueryErrorCode::Cancelled)))
1907 },
1908 Self::Dropped => Some(AttemptFailureSummary::control(AttemptFailureCode::Dropped)),
1909 Self::Panicked => Some(AttemptFailureSummary::control(AttemptFailureCode::Panicked)),
1910 }
1911 }
1912}
1913#[cfg(test)]
1914mod tests;