Skip to main content

query_runtime/
native_error.rs

1//! cuDF error projection at the native boundary.
2//!
3//! Provides the canonical translation of `cudf::Error` into [`QueryError`]. Both
4//! the runtime and query engine invoke this projection to ensure unified classification,
5//! consistent fact metadata, and uniform redaction across native failures.
6//!
7//! Allocation attribution derives from `cudf`. The typed FFI failure source
8//! establishes whether physical exhaustion or query-limit rejection occurred,
9//! whereas budget-scope annotations record usage snapshots of traversed scopes
10//! (see `cudf::allocation_attribution`). Attempt retry decisions follow sealed attempt
11//! advice in coordination with error status.
12
13use std::borrow::Cow;
14
15use query_planning::error::{MemoryFailure, MemoryUsage, QueryError, QueryErrorCode, redact_uri_tokens};
16
17/// Wraps a `cudf::Error` at the native-engine boundary.
18///
19/// `operation` should be the stable dotted id for the native operation that
20/// called cuDF, and `message` should describe the failure for humans. cuDF
21/// classification and a URI-redacted message are preserved as `cudf_*` facts.
22pub fn cudf_query_error(operation: &'static str, message: impl Into<String>, source: cudf::Error) -> QueryError {
23    let error = cudf_query_error_without_source(operation, message, &source);
24    error.with_source(source)
25}
26
27/// Wraps a `cudf::Error` without attaching its display text as a source.
28///
29/// Use this when source text may contain sensitive reader locations. It keeps
30/// cuDF classification and redacted `cudf_message` facts, but omits the
31/// unsanitized source chain.
32pub fn cudf_query_error_without_source(
33    operation: &'static str,
34    message: impl Into<String>,
35    source: &cudf::Error,
36) -> QueryError {
37    cudf_query_error_with_code(query_error_code_for_cudf_error(source), operation, message, source)
38}
39
40/// Wraps a rejected Arrow-to-cuDF dtype conversion as
41/// [`QueryErrorCode::UnsupportedType`].
42///
43/// Native type support reports unsupported Arrow types as `UnsupportedType`.
44/// [`query_error_code_for_cudf_error`] maps cuDF `UnsupportedDtype` to the
45/// `CudfUnsupported` code. The `cudf_*` facts and source chain match
46/// [`cudf_query_error`], plus `cudf_reason` when cuDF attached one.
47pub fn cudf_unsupported_type_error(
48    operation: &'static str,
49    message: impl Into<String>,
50    source: cudf::Error,
51) -> QueryError {
52    let mut error = cudf_query_error_with_code(QueryErrorCode::UnsupportedType, operation, message, &source);
53    if let Some(reason) = source.detail_value("reason") {
54        error = error.with_fact("cudf_reason", reason);
55    }
56    error.with_source(source)
57}
58
59fn cudf_query_error_with_code(
60    code: QueryErrorCode,
61    operation: &'static str,
62    message: impl Into<String>,
63    source: &cudf::Error,
64) -> QueryError {
65    let mut error = QueryError::new(code, operation, message)
66        .with_fact("cudf_code", source.code())
67        .with_fact("cudf_kind", source.kind())
68        .with_fact("cudf_status", source.status())
69        .with_fact("cudf_operation", source.operation());
70    error = with_cudf_ffi_detail_facts(error, source);
71    if let Some(detail) = source.budget_exhaustion() {
72        error = error.with_memory_failure(memory_failure(detail));
73    }
74    match cudf_message_fact(source) {
75        Some(cudf_message) => error.with_fact("cudf_message", cudf_message),
76        None => error,
77    }
78}
79
80/// Classifies a `cudf::Error` into the native-engine error taxonomy.
81///
82/// Notable permanent mappings: `NotFound` is treated as invalid input,
83/// `ExecutionContext` is treated as an invariant violation, and unknown kinds
84/// fall back to dependency failure.
85pub fn query_error_code_for_cudf_error(error: &cudf::Error) -> QueryErrorCode {
86    match cudf::allocation_attribution(error) {
87        cudf::CudfAllocationAttribution::ConfirmedQueryLimit => {
88            return QueryErrorCode::QueryMemoryLimitExceeded;
89        },
90        cudf::CudfAllocationAttribution::ConfirmedPhysical => return QueryErrorCode::DeviceOutOfMemory,
91        cudf::CudfAllocationAttribution::UnattributedAllocationFailure => {
92            return QueryErrorCode::DeviceAllocationFailed;
93        },
94        cudf::CudfAllocationAttribution::NonAllocationFailure => {},
95    }
96    match cudf::classify_cudf_error(error) {
97        cudf::CudfErrorClassification::SizeOverflow => QueryErrorCode::SizeOverflow,
98        cudf::CudfErrorClassification::HostMaterializationLimitExceeded => {
99            QueryErrorCode::HostMaterializationLimitExceeded
100        },
101        cudf::CudfErrorClassification::ResourceExhausted => QueryErrorCode::DeviceAllocationFailed,
102        cudf::CudfErrorClassification::TemporaryFailure => QueryErrorCode::CudfTemporaryFailure,
103        cudf::CudfErrorClassification::PersistentFailure => QueryErrorCode::DeviceFault,
104        cudf::CudfErrorClassification::InvalidInput => QueryErrorCode::CudfInvalidInput,
105        cudf::CudfErrorClassification::Unsupported => QueryErrorCode::CudfUnsupported,
106        cudf::CudfErrorClassification::DependencyFailure => QueryErrorCode::CudfDependencyFailure,
107        cudf::CudfErrorClassification::InvariantViolation => QueryErrorCode::InternalInvariantViolation,
108    }
109}
110
111/// Converts the budget scope a cuDF error passed through into planning's
112/// backend-neutral memory context.
113pub fn memory_failure(detail: &cudf::BudgetExhaustionDetail) -> MemoryFailure {
114    MemoryFailure {
115        boundary: Cow::Owned(detail.boundary().to_owned()),
116        operation: Cow::Borrowed(detail.operation()),
117        usage: detail.usage().map(|usage| MemoryUsage {
118            limit_bytes: usage.allocation_limit_bytes,
119            peak_bytes: usage.peak_bytes,
120            peak_aligned_bytes: usage.peak_aligned_bytes,
121            current_bytes: usage.current_bytes,
122            current_aligned_bytes: usage.current_aligned_bytes,
123            failed_allocation_count: usage.failed_allocation_count,
124            largest_failed_allocation_bytes: usage.largest_failed_allocation_bytes,
125        }),
126    }
127}
128
129/// Returns the trimmed foreign cuDF message, redacting URI-like tokens.
130pub fn cudf_message_fact(source: &cudf::Error) -> Option<String> {
131    let message = source.message().trim();
132    if message.is_empty() {
133        return None;
134    }
135    Some(redact_uri_tokens(message))
136}
137
138fn with_cudf_ffi_detail_facts(mut error: QueryError, source: &cudf::Error) -> QueryError {
139    let Some(detail) = source.ffi_detail() else {
140        return error;
141    };
142    error = error
143        .with_fact("failure_source", detail.failure_source)
144        .with_fact("requested_allocation_bytes", detail.requested_bytes)
145        .with_fact("allocation_alignment", detail.alignment)
146        .with_fact("cuda_status", detail.cuda_error);
147    if detail.domain_id != 0 {
148        error = error.with_fact("allocation_domain_id", detail.domain_id);
149    }
150    if detail.member_id != 0 {
151        error = error.with_fact("allocation_member_id", detail.member_id);
152    }
153    if detail.generation != 0 {
154        error = error.with_fact("allocation_domain_generation", detail.generation);
155    }
156    error
157}
158
159#[cfg(test)]
160mod tests;