use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
error::Error as StdError,
fmt,
sync::Arc,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use serde_json::{Value, json};
pub use self::context::{AllocationSource, FailureContext, MemoryFailure, MemoryUsage, ResourceBoundary};
use crate::{
correlation::{QueryId, TraceId},
triage::TriageCode,
};
mod context;
type SharedSourceError = Arc<dyn StdError + Send + Sync + 'static>;
pub type Result<T> = std::result::Result<T, QueryError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum QueryErrorCode {
Cancelled,
UnsupportedPipelineContract,
UnsupportedOperator,
UnsupportedExpression,
UnsupportedType,
InvalidPlan,
InvalidAttemptCorrelation,
InvalidConfiguration,
GraphInputInvalid,
CudfInvalidInput,
CudfUnsupported,
SizeOverflow,
HostMaterializationLimitExceeded,
CudfTemporaryFailure,
CudfPersistentFailure,
CudfDependencyFailure,
AlgorithmDidNotConverge,
SourceAccessDenied,
SourceUnauthenticated,
SourceObjectNotFound,
SourceTemporaryFailure,
CudfInvariantViolation,
MemoryReservationFailed,
ServiceOverloaded,
QueryBudgetUnavailable,
QueryMemoryLimitExceeded,
DeviceOutOfMemory,
DeviceAllocationFailed,
DeviceFault,
CleanupFailed,
InternalInvariantViolation,
Internal,
}
impl QueryErrorCode {
pub const ALL: &'static [Self] = &[
Self::Cancelled,
Self::UnsupportedPipelineContract,
Self::UnsupportedOperator,
Self::UnsupportedExpression,
Self::UnsupportedType,
Self::InvalidPlan,
Self::InvalidAttemptCorrelation,
Self::InvalidConfiguration,
Self::GraphInputInvalid,
Self::CudfInvalidInput,
Self::CudfUnsupported,
Self::SizeOverflow,
Self::HostMaterializationLimitExceeded,
Self::CudfTemporaryFailure,
Self::CudfPersistentFailure,
Self::CudfDependencyFailure,
Self::AlgorithmDidNotConverge,
Self::SourceAccessDenied,
Self::SourceUnauthenticated,
Self::SourceObjectNotFound,
Self::SourceTemporaryFailure,
Self::CudfInvariantViolation,
Self::MemoryReservationFailed,
Self::ServiceOverloaded,
Self::QueryBudgetUnavailable,
Self::QueryMemoryLimitExceeded,
Self::DeviceOutOfMemory,
Self::DeviceAllocationFailed,
Self::DeviceFault,
Self::CleanupFailed,
Self::InternalInvariantViolation,
Self::Internal,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Cancelled => "cancelled",
Self::UnsupportedPipelineContract => "unsupported_pipeline_contract",
Self::UnsupportedOperator => "unsupported_operator",
Self::UnsupportedExpression => "unsupported_expression",
Self::UnsupportedType => "unsupported_type",
Self::InvalidPlan => "invalid_plan",
Self::InvalidAttemptCorrelation => "invalid_attempt_correlation",
Self::InvalidConfiguration => "invalid_configuration",
Self::GraphInputInvalid => "graph_input_invalid",
Self::CudfInvalidInput => "cudf_invalid_input",
Self::CudfUnsupported => "cudf_unsupported",
Self::SizeOverflow => "size_overflow",
Self::HostMaterializationLimitExceeded => "host_materialization_limit_exceeded",
Self::CudfTemporaryFailure => "cudf_temporary_failure",
Self::CudfPersistentFailure => "cudf_persistent_failure",
Self::CudfDependencyFailure => "cudf_dependency_failure",
Self::AlgorithmDidNotConverge => "algorithm_did_not_converge",
Self::SourceAccessDenied => "source_access_denied",
Self::SourceUnauthenticated => "source_unauthenticated",
Self::SourceObjectNotFound => "source_object_not_found",
Self::SourceTemporaryFailure => "source_temporary_failure",
Self::CudfInvariantViolation => "cudf_invariant_violation",
Self::MemoryReservationFailed => "memory_reservation_failed",
Self::ServiceOverloaded => "service_overloaded",
Self::QueryBudgetUnavailable => "query_budget_unavailable",
Self::QueryMemoryLimitExceeded => "query_memory_limit_exceeded",
Self::DeviceOutOfMemory => "device_out_of_memory",
Self::DeviceAllocationFailed => "device_allocation_failed",
Self::DeviceFault => "device_fault",
Self::CleanupFailed => "cleanup_failed",
Self::InternalInvariantViolation => "internal_invariant_violation",
Self::Internal => "internal",
}
}
pub const fn kind(self) -> QueryErrorKind {
kind_for_code(self)
}
pub const fn status(self) -> QueryErrorStatus {
status_for_code(self)
}
}
impl fmt::Display for QueryErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum QueryErrorKind {
Cancelled,
Planning,
InvalidInput,
Unsupported,
ResourceExhausted,
DependencyFailure,
InvariantViolation,
Internal,
}
impl QueryErrorKind {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Cancelled => "cancelled",
Self::Planning => "planning",
Self::InvalidInput => "invalid_input",
Self::Unsupported => "unsupported",
Self::ResourceExhausted => "resource_exhausted",
Self::DependencyFailure => "dependency_failure",
Self::InvariantViolation => "invariant_violation",
Self::Internal => "internal",
}
}
}
impl fmt::Display for QueryErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryErrorStatus {
Permanent,
Temporary,
Persistent,
}
impl QueryErrorStatus {
pub const fn as_str(self) -> &'static str {
match self {
Self::Permanent => "permanent",
Self::Temporary => "temporary",
Self::Persistent => "persistent",
}
}
}
impl fmt::Display for QueryErrorStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub(crate) const fn kind_for_code(code: QueryErrorCode) -> QueryErrorKind {
match code {
QueryErrorCode::Cancelled => QueryErrorKind::Cancelled,
QueryErrorCode::UnsupportedPipelineContract
| QueryErrorCode::UnsupportedOperator
| QueryErrorCode::UnsupportedExpression
| QueryErrorCode::UnsupportedType => QueryErrorKind::Unsupported,
QueryErrorCode::InvalidPlan => QueryErrorKind::Planning,
QueryErrorCode::InvalidAttemptCorrelation => QueryErrorKind::InvalidInput,
QueryErrorCode::InvalidConfiguration => QueryErrorKind::InvalidInput,
QueryErrorCode::GraphInputInvalid => QueryErrorKind::InvalidInput,
QueryErrorCode::CudfInvalidInput => QueryErrorKind::InvalidInput,
QueryErrorCode::CudfUnsupported => QueryErrorKind::Unsupported,
QueryErrorCode::SizeOverflow
| QueryErrorCode::HostMaterializationLimitExceeded
| QueryErrorCode::MemoryReservationFailed
| QueryErrorCode::ServiceOverloaded
| QueryErrorCode::QueryBudgetUnavailable
| QueryErrorCode::QueryMemoryLimitExceeded
| QueryErrorCode::DeviceOutOfMemory
| QueryErrorCode::DeviceAllocationFailed => QueryErrorKind::ResourceExhausted,
QueryErrorCode::CudfTemporaryFailure
| QueryErrorCode::CudfPersistentFailure
| QueryErrorCode::CudfDependencyFailure
| QueryErrorCode::AlgorithmDidNotConverge
| QueryErrorCode::SourceAccessDenied
| QueryErrorCode::SourceUnauthenticated
| QueryErrorCode::SourceObjectNotFound
| QueryErrorCode::SourceTemporaryFailure
| QueryErrorCode::DeviceFault
| QueryErrorCode::CleanupFailed => QueryErrorKind::DependencyFailure,
QueryErrorCode::CudfInvariantViolation | QueryErrorCode::InternalInvariantViolation => {
QueryErrorKind::InvariantViolation
},
QueryErrorCode::Internal => QueryErrorKind::Internal,
}
}
pub(crate) const fn status_for_code(code: QueryErrorCode) -> QueryErrorStatus {
match code {
QueryErrorCode::Cancelled => QueryErrorStatus::Permanent,
QueryErrorCode::CudfTemporaryFailure
| QueryErrorCode::SourceTemporaryFailure
| QueryErrorCode::MemoryReservationFailed
| QueryErrorCode::ServiceOverloaded
| QueryErrorCode::QueryBudgetUnavailable
| QueryErrorCode::DeviceOutOfMemory
| QueryErrorCode::DeviceAllocationFailed => QueryErrorStatus::Temporary,
QueryErrorCode::CudfPersistentFailure => QueryErrorStatus::Persistent,
QueryErrorCode::UnsupportedPipelineContract
| QueryErrorCode::UnsupportedOperator
| QueryErrorCode::UnsupportedExpression
| QueryErrorCode::UnsupportedType
| QueryErrorCode::InvalidPlan
| QueryErrorCode::InvalidAttemptCorrelation
| QueryErrorCode::InvalidConfiguration
| QueryErrorCode::GraphInputInvalid
| QueryErrorCode::CudfInvalidInput
| QueryErrorCode::CudfUnsupported
| QueryErrorCode::SizeOverflow
| QueryErrorCode::HostMaterializationLimitExceeded
| QueryErrorCode::CudfDependencyFailure
| QueryErrorCode::AlgorithmDidNotConverge
| QueryErrorCode::SourceAccessDenied
| QueryErrorCode::SourceUnauthenticated
| QueryErrorCode::SourceObjectNotFound
| QueryErrorCode::CudfInvariantViolation
| QueryErrorCode::QueryMemoryLimitExceeded
| QueryErrorCode::InternalInvariantViolation => QueryErrorStatus::Permanent,
QueryErrorCode::DeviceFault | QueryErrorCode::CleanupFailed => QueryErrorStatus::Persistent,
QueryErrorCode::Internal => QueryErrorStatus::Permanent,
}
}
pub(crate) const fn triage_code_for_query_error(code: QueryErrorCode) -> TriageCode {
match code {
QueryErrorCode::Cancelled => TriageCode::Cancelled,
QueryErrorCode::UnsupportedPipelineContract => TriageCode::UnsupportedPlanShape,
QueryErrorCode::UnsupportedOperator => TriageCode::UnsupportedOperator,
QueryErrorCode::UnsupportedExpression => TriageCode::UnsupportedExpression,
QueryErrorCode::UnsupportedType => TriageCode::UnsupportedType,
QueryErrorCode::InvalidPlan => TriageCode::InvalidPlan,
QueryErrorCode::InvalidAttemptCorrelation
| QueryErrorCode::InvalidConfiguration
| QueryErrorCode::GraphInputInvalid
| QueryErrorCode::CudfInvalidInput => TriageCode::InvalidInput,
QueryErrorCode::CudfUnsupported => TriageCode::UnsupportedOperator,
QueryErrorCode::SizeOverflow
| QueryErrorCode::HostMaterializationLimitExceeded
| QueryErrorCode::QueryMemoryLimitExceeded
| QueryErrorCode::DeviceOutOfMemory
| QueryErrorCode::DeviceAllocationFailed => TriageCode::ResourceExhausted,
QueryErrorCode::CudfTemporaryFailure
| QueryErrorCode::CudfPersistentFailure
| QueryErrorCode::CudfDependencyFailure
| QueryErrorCode::AlgorithmDidNotConverge => TriageCode::DependencyFailure,
QueryErrorCode::CudfInvariantViolation => TriageCode::InvariantViolation,
QueryErrorCode::ServiceOverloaded => TriageCode::ServiceOverloaded,
QueryErrorCode::Internal => TriageCode::Internal,
QueryErrorCode::SourceAccessDenied
| QueryErrorCode::SourceUnauthenticated
| QueryErrorCode::SourceObjectNotFound
| QueryErrorCode::SourceTemporaryFailure => TriageCode::SourceFailure,
QueryErrorCode::MemoryReservationFailed => TriageCode::MemoryReservationDenied,
QueryErrorCode::QueryBudgetUnavailable => TriageCode::MemoryReservationDenied,
QueryErrorCode::DeviceFault | QueryErrorCode::CleanupFailed => TriageCode::DependencyFailure,
QueryErrorCode::InternalInvariantViolation => TriageCode::InvariantViolation,
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AttemptFailureCode {
Query(QueryErrorCode),
BackendClosing,
DatafusionError,
Dropped,
Panicked,
StreamSynchronizationFailed,
CleanupCallbackAbandoned,
}
impl AttemptFailureCode {
const LIFECYCLE: [Self; 6] = [
Self::BackendClosing,
Self::DatafusionError,
Self::Dropped,
Self::Panicked,
Self::StreamSynchronizationFailed,
Self::CleanupCallbackAbandoned,
];
pub const fn from_query_error_code(code: QueryErrorCode) -> Self {
Self::Query(code)
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Query(code) => code.as_str(),
Self::BackendClosing => "backend_closing",
Self::DatafusionError => "datafusion_error",
Self::Dropped => "dropped",
Self::Panicked => "panicked",
Self::StreamSynchronizationFailed => "stream_synchronization_failed",
Self::CleanupCallbackAbandoned => "cleanup_callback_abandoned",
}
}
fn from_label(label: &str) -> Option<Self> {
QueryErrorCode::ALL
.iter()
.map(|code| Self::Query(*code))
.chain(Self::LIFECYCLE)
.find(|code| code.as_str() == label)
}
}
impl fmt::Debug for AttemptFailureCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Query(code) => fmt::Debug::fmt(code, f),
Self::BackendClosing => f.write_str("BackendClosing"),
Self::DatafusionError => f.write_str("DatafusionError"),
Self::Dropped => f.write_str("Dropped"),
Self::Panicked => f.write_str("Panicked"),
Self::StreamSynchronizationFailed => f.write_str("StreamSynchronizationFailed"),
Self::CleanupCallbackAbandoned => f.write_str("CleanupCallbackAbandoned"),
}
}
}
impl Serialize for AttemptFailureCode {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for AttemptFailureCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
let label = <Cow<'de, str>>::deserialize(deserializer)?;
Self::from_label(&label).ok_or_else(|| D::Error::custom(format!("unknown attempt failure code `{label}`")))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Facts {
values: BTreeMap<&'static str, String>,
collision_keys: BTreeSet<&'static str>,
}
pub(crate) const FACT_COLLISION_KEYS: &str = "fact_collision_keys";
pub(crate) const EMPTY_FACT_KEY: &str = "<empty>";
impl Facts {
pub(crate) fn new() -> Self {
Self::default()
}
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = (&'static str, &str)> {
self.values.iter().map(|(key, value)| (*key, value.as_str()))
}
fn insert(&mut self, key: &'static str, value: String) {
if key.is_empty() {
self.record_collision(EMPTY_FACT_KEY);
return;
}
if key == FACT_COLLISION_KEYS || self.values.contains_key(key) {
self.record_collision(key);
return;
}
self.values.insert(key, value);
}
fn record_collision(&mut self, key: &'static str) {
self.collision_keys.insert(key);
let value = self.collision_keys.iter().copied().collect::<Vec<_>>().join(",");
self.values.insert(FACT_COLLISION_KEYS, value);
}
fn set(&mut self, key: &'static str, value: String) {
assert!(!key.is_empty(), "query-engine error fact key must not be empty");
self.values.insert(key, value);
}
}
#[derive(Clone)]
pub struct QueryError(Box<QueryErrorRepr>);
#[derive(Clone)]
struct QueryErrorRepr {
query_id: Option<QueryId>,
trace_id: Option<TraceId>,
code: QueryErrorCode,
operation: &'static str,
message: String,
facts: Box<Facts>,
context: FailureContext,
source: Option<SharedSourceError>,
}
impl QueryError {
pub fn new(code: QueryErrorCode, operation: &'static str, message: impl Into<String>) -> Self {
let message = message.into();
assert!(!operation.is_empty(), "query-engine error operation must not be empty");
assert!(!message.is_empty(), "query-engine error message must not be empty");
Self(Box::new(QueryErrorRepr {
query_id: None,
trace_id: None,
code,
operation,
message,
facts: Box::new(Facts::new()),
context: FailureContext::default(),
source: None,
}))
}
#[must_use]
#[doc(hidden)]
pub fn with_cleanup_deadline_elapsed(mut self) -> Self {
self.0.context.cleanup_deadline_elapsed = true;
self
}
#[doc(hidden)]
pub fn is_cleanup_deadline_elapsed(&self) -> bool {
self.0.context.cleanup_deadline_elapsed
}
#[must_use]
pub fn with_fact(mut self, key: &'static str, value: impl ToString) -> Self {
self.0.facts.insert(key, value.to_string());
self
}
#[must_use]
#[doc(hidden)]
pub fn with_fact_if_absent(mut self, key: &'static str, value: impl ToString) -> Self {
if self.0.facts.get(key).is_none() && !self.0.context.projects(key) {
self.0.facts.insert(key, value.to_string());
}
self
}
pub fn context(&self) -> &FailureContext {
&self.0.context
}
#[must_use]
pub fn with_memory_failure(mut self, memory: MemoryFailure) -> Self {
let candidate = FailureContext { memory: Some(memory), ..FailureContext::default() };
match &self.0.context.memory {
None => self.0.context.memory = candidate.memory,
Some(_) => self.record_context_collisions(&candidate),
}
self
}
#[must_use]
pub fn with_resource_boundary(mut self, boundary: ResourceBoundary) -> Self {
let candidate = FailureContext { boundary: Some(boundary), ..FailureContext::default() };
match &self.0.context.boundary {
None => self.0.context.boundary = candidate.boundary,
Some(_) => self.record_context_collisions(&candidate),
}
self
}
#[must_use]
#[doc(hidden)]
pub fn with_allocation_source(mut self, source: AllocationSource) -> Self {
let candidate = FailureContext { allocation_source: Some(source), ..FailureContext::default() };
match self.0.context.allocation_source {
None => self.0.context.allocation_source = candidate.allocation_source,
Some(_) => self.record_context_collisions(&candidate),
}
self
}
#[must_use]
#[doc(hidden)]
pub fn with_graph_cache_scope_peak_bytes(mut self, peak_bytes: u64) -> Self {
self.0.context.graph_cache_scope_peak_bytes.get_or_insert(peak_bytes);
self
}
fn record_context_collisions(&mut self, candidate: &FailureContext) {
let current = self.0.context.projection();
for (key, value) in candidate.projection() {
if current.iter().any(|(existing, current)| *existing == key && *current != value) {
self.0.facts.record_collision(key);
}
}
}
pub fn wire_facts(&self) -> impl Iterator<Item = (&'static str, Cow<'_, str>)> {
let mut facts = self
.0
.facts
.iter()
.filter(|(key, _)| *key != FACT_COLLISION_KEYS)
.map(|(key, value)| (key, Cow::Borrowed(value)))
.collect::<BTreeMap<_, _>>();
let mut collisions = self.0.facts.collision_keys.clone();
for (key, value) in self.0.context.projection() {
if facts.insert(key, value.clone()).is_some_and(|stored| stored != value) {
collisions.insert(key);
}
}
if !collisions.is_empty() {
facts.insert(FACT_COLLISION_KEYS, Cow::Owned(collisions.into_iter().collect::<Vec<_>>().join(",")));
}
facts.into_iter()
}
pub fn wire_fact(&self, key: &str) -> Option<Cow<'_, str>> {
self.wire_facts().find_map(|(candidate, value)| (candidate == key).then_some(value))
}
#[must_use]
pub fn with_query_id(mut self, query_id: impl Into<QueryId>) -> Self {
let query_id = query_id.into();
self.0.facts.set("query_id", query_id.to_string());
self.0.query_id = Some(query_id);
self
}
#[must_use]
pub fn with_trace_id(mut self, trace_id: impl Into<TraceId>) -> Self {
let trace_id = trace_id.into();
self.0.facts.set("trace_id", trace_id.to_string());
self.0.trace_id = Some(trace_id);
self
}
#[must_use]
pub fn with_source<E>(mut self, source: E) -> Self
where E: StdError + Send + Sync + 'static {
if self.0.source.is_none() {
self.0.source = Some(Arc::new(source));
}
self
}
pub fn code(&self) -> QueryErrorCode {
self.0.code
}
pub fn kind(&self) -> QueryErrorKind {
self.0.code.kind()
}
pub fn status(&self) -> QueryErrorStatus {
self.0.code.status()
}
pub fn query_id(&self) -> Option<&QueryId> {
self.0.query_id.as_ref()
}
pub fn trace_id(&self) -> Option<&TraceId> {
self.0.trace_id.as_ref()
}
pub fn operation(&self) -> &'static str {
self.0.operation
}
pub fn message(&self) -> &str {
&self.0.message
}
pub fn facts(&self) -> &Facts {
self.0.facts.as_ref()
}
pub fn fact_value(&self, key: &str) -> Option<&str> {
self.0.facts.get(key)
}
pub fn source_error(&self) -> Option<&(dyn StdError + 'static)> {
self.0.source.as_deref().map(|source| source as &(dyn StdError + 'static))
}
pub fn to_json(&self) -> Value {
let query_id = self.0.query_id.as_ref().map(QueryId::as_str).or_else(|| self.fact_value("query_id"));
let trace_id = self.0.trace_id.as_ref().map(TraceId::as_str).or_else(|| self.fact_value("trace_id"));
json!({
"schema": "query_engine.query_error",
"query_id": query_id,
"trace_id": trace_id,
"triage": self.triage_summary().to_json(),
"code": self.0.code.as_str(),
"kind": self.kind().as_str(),
"status": self.status().as_str(),
"operation": self.0.operation,
"message": self.0.message,
"facts": self.wire_facts().collect::<BTreeMap<_, _>>(),
"source": self.0.source.as_ref().map(|source| source.to_string()),
})
}
}
pub fn redact_uri_tokens(message: &str) -> String {
message
.split_whitespace()
.map(|token| if token.contains("://") { "<redacted-uri>" } else { token })
.collect::<Vec<_>>()
.join(" ")
}
pub fn bytes_with_gib(bytes: u64) -> String {
const GIB: u128 = 1 << 30;
let hundredths = u128::from(bytes) * 100 / GIB;
format!("{bytes} bytes ({}.{:02} GiB)", hundredths / 100, hundredths % 100)
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"code={} kind={} status={} operation={} message={}",
self.code(),
self.kind(),
self.status(),
self.operation(),
quote(self.message()),
)?;
f.write_str(" facts={")?;
for (index, (key, value)) in self.wire_facts().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{key}={}", quote(&value))?;
}
f.write_str("}")?;
if let Some(source) = &self.0.source {
write!(f, " source={}", quote(&source.to_string()))?;
}
Ok(())
}
}
impl fmt::Debug for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("QueryError")
.field("query_id", &self.0.query_id)
.field("trace_id", &self.0.trace_id)
.field("code", &self.0.code)
.field("kind", &self.kind())
.field("status", &self.status())
.field("operation", &self.0.operation)
.field("message", &self.0.message)
.field("facts", &self.0.facts)
.field("context", &self.0.context)
.field("source", &self.0.source.as_ref().map(|source| source.to_string()))
.finish()
}
}
impl StdError for QueryError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source_error()
}
}
fn quote(value: &str) -> String {
if value.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ':' | '/' | '(' | ')')) {
value.to_string()
} else {
format!("{value:?}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plan::NodeId;
#[test]
fn bytes_with_gib_prints_exact_bytes_and_truncated_gib() {
assert_eq!(bytes_with_gib(0), "0 bytes (0.00 GiB)");
assert_eq!(bytes_with_gib(700), "700 bytes (0.00 GiB)");
assert_eq!(bytes_with_gib(95 * (1 << 30)), "102005473280 bytes (95.00 GiB)");
assert_eq!(bytes_with_gib(101_973_819_392), "101973819392 bytes (94.97 GiB)");
assert_eq!(bytes_with_gib(95 * (1 << 30) - 1), "102005473279 bytes (94.99 GiB)");
assert_eq!(bytes_with_gib(u64::MAX), "18446744073709551615 bytes (17179869183.99 GiB)");
}
#[test]
fn redact_uri_tokens_replaces_uri_like_tokens_only() {
assert_eq!(
redact_uri_tokens("S3: must provide `aws_access_key` if AWS_ACCESS_KEY_ID isn't set."),
"S3: must provide `aws_access_key` if AWS_ACCESS_KEY_ID isn't set."
);
assert_eq!(
redact_uri_tokens("failed to open s3://bucket/secret/key.parquet for read"),
"failed to open <redacted-uri> for read"
);
assert_eq!(redact_uri_tokens(" spaced message "), "spaced message");
}
#[test]
fn graph_cache_scope_peak_is_a_typed_sideband_not_an_error_fact() {
let error = QueryError::new(
QueryErrorCode::DeviceAllocationFailed,
"query_engine.test.cache_scope",
"cache scope failed",
)
.with_graph_cache_scope_peak_bytes(4_096);
assert_eq!(error.context().graph_cache_scope_peak_bytes, Some(4_096));
assert_eq!(error.fact_value("graph_cache_scope_peak_bytes"), None);
assert!(error.to_json()["facts"].get("graph_cache_scope_peak_bytes").is_none());
}
#[test]
fn wire_facts_render_typed_context_over_disagreeing_stored_facts() {
let node = NodeId::from_index_for_test(4);
let error = QueryError::new(
QueryErrorCode::QueryMemoryLimitExceeded,
"query_engine.test.wire_facts",
"typed context projection probe",
)
.with_fact("budget_bytes", 1)
.with_fact("node_id", node)
.with_memory_failure(MemoryFailure {
boundary: "inner".into(),
operation: "cudf.test.op".into(),
usage: Some(MemoryUsage {
limit_bytes: 4096,
peak_bytes: 3,
peak_aligned_bytes: 256,
current_bytes: 2,
current_aligned_bytes: 256,
failed_allocation_count: 1,
largest_failed_allocation_bytes: 8192,
}),
})
.with_memory_failure(MemoryFailure { boundary: "outer".into(), operation: "cudf.test.op".into(), usage: None })
.with_resource_boundary(ResourceBoundary::new("source_chunk_read", node).with_resume("source_chunk"))
.with_resource_boundary(ResourceBoundary::new("outer_stage", node))
.with_fact_if_absent("resume_boundary", "outer_resume");
let wire = error.wire_facts().map(|(key, value)| (key, value.into_owned())).collect::<BTreeMap<_, _>>();
assert_eq!(wire["budget_bytes"], "4096", "the allocator limit wins over a stale stored fact");
assert_eq!(wire["budget_boundary"], "inner", "the innermost scope wins");
assert_eq!(wire["memory_boundary"], "source_chunk_read", "the first boundary wins as a unit");
assert_eq!(wire["resume_boundary"], "source_chunk", "a typed key counts as present");
assert_eq!(wire["node_id"], "n4");
assert_eq!(wire[FACT_COLLISION_KEYS], "budget_boundary,budget_bytes,memory_boundary,stage");
assert_eq!(error.wire_facts().collect::<Vec<_>>(), error.wire_facts().collect::<Vec<_>>());
assert!(error.to_string().contains("budget_bytes=4096"), "{error}");
}
#[test]
fn attempt_failure_codes_round_trip_through_their_labels() {
let codes = QueryErrorCode::ALL
.iter()
.map(|code| AttemptFailureCode::Query(*code))
.chain(AttemptFailureCode::LIFECYCLE)
.collect::<Vec<_>>();
for code in codes {
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, format!("\"{}\"", code.as_str()));
assert_eq!(serde_json::from_str::<AttemptFailureCode>(&json).unwrap(), code);
}
assert_eq!(AttemptFailureCode::Query(QueryErrorCode::Internal).as_str(), "internal");
assert!(serde_json::from_str::<AttemptFailureCode>("\"unsupported_plan_shape\"").is_err());
}
#[test]
fn invalid_configuration_is_a_permanent_invalid_input_code_distinct_from_invalid_plan() {
let code = QueryErrorCode::InvalidConfiguration;
assert_eq!(code.as_str(), "invalid_configuration");
assert_eq!(code.kind(), QueryErrorKind::InvalidInput);
assert_eq!(code.status(), QueryErrorStatus::Permanent);
assert_eq!(TriageCode::from(code), TriageCode::InvalidInput);
assert_eq!(QueryErrorCode::InvalidPlan.kind(), QueryErrorKind::Planning);
assert!(QueryErrorCode::ALL.contains(&code));
}
#[test]
fn duplicate_fact_decoration_preserves_first_value_and_records_collision() {
let error = QueryError::new(
QueryErrorCode::CudfInvalidInput,
"query_engine.test.fact_collision",
"fact collision probe",
)
.with_fact("layer", "outer")
.with_fact("layer", "inner")
.with_fact("", "empty key is ignored");
assert_eq!(error.fact_value("layer"), Some("outer"));
assert_eq!(error.fact_value(FACT_COLLISION_KEYS), Some("<empty>,layer"));
assert_eq!(error.code(), QueryErrorCode::CudfInvalidInput);
}
#[test]
fn repeated_source_decoration_preserves_first_cause() {
let first =
QueryError::new(QueryErrorCode::CudfInvalidInput, "query_engine.test.first_cause", "the original cause");
let expected_cause = first.to_string();
let error = QueryError::new(
QueryErrorCode::CudfInvalidInput,
"query_engine.test.source_preservation",
"source preservation probe",
)
.with_source(first)
.with_source(QueryError::new(
QueryErrorCode::DeviceFault,
"query_engine.test.decoration",
"decoration must not replace the cause",
));
let cause = error.source_error().expect("first source must be retained").to_string();
assert_eq!(cause, expected_cause, "{cause}");
assert!(!cause.contains("decoration must not replace the cause"), "{cause}");
}
}