use datafusion::common::DataFusionError;
use query_planning::error::bytes_with_gib;
use super::{
ClientDetail, DataFusionErrorExt, Error, ErrorCode, ErrorKind, FailedStatement, classify,
datafusion_error_human_message, truncate_with_marker,
};
const FOREIGN_FAILURE_OPERATION: &str = "algeon_datafusion.error.client_failure";
pub(crate) const MASKED_INTERNAL_ERROR_MESSAGE: &str = "internal server error while processing the request; the full diagnostic is available through the GetQueryStats action for the query id in the x-algeon-query-id metadata";
pub(crate) const MAX_CLIENT_DATAFUSION_MESSAGE_BYTES: usize = 2_048;
const MAX_SERVER_DATAFUSION_TRAVERSAL_DEPTH: usize = 32;
const BLOCKING_SUBJECT_POINTER: &str = "EXPLAIN GPU lists every blocking candidate";
pub(crate) struct ClientMessage {
#[cfg_attr(not(feature = "server"), expect(dead_code, reason = "only the Flight boundary logs the message"))]
pub(crate) logged: String,
pub(crate) text: String,
#[cfg_attr(not(feature = "server"), expect(dead_code, reason = "only the Flight boundary appends the pointer"))]
pointer: Option<&'static str>,
}
impl ClientMessage {
#[cfg_attr(not(feature = "server"), expect(dead_code, reason = "only the Flight boundary builds a status"))]
pub(crate) fn status_message(&self) -> String {
match self.pointer {
Some(pointer) => format!("{}; {pointer}", self.text),
None => self.text.clone(),
}
}
}
const NATIVE_REQUIRED_REJECTION: &str = "native_required mode rejected a plan that needs DataFusion CPU: ";
pub(crate) fn client_message(error: &Error) -> ClientMessage {
let mut logged = client_status_message(error);
let detail = client_actionable_kind(error).then(|| error.client_detail()).flatten();
let mut text = detail.map_or_else(|| logged.clone(), |detail| client_schema_detail_message(detail, &logged));
if error.code() == ErrorCode::FinalPlanRequirementUnsatisfied {
logged.insert_str(0, NATIVE_REQUIRED_REJECTION);
text.insert_str(0, NATIVE_REQUIRED_REJECTION);
}
let pointer = matches!(detail, Some(ClientDetail::BlockingSubject { .. })).then_some(BLOCKING_SUBJECT_POINTER);
ClientMessage { logged, text, pointer }
}
pub(crate) fn client_failure(error: &DataFusionError, statement: FailedStatement) -> (ErrorCode, String) {
if let Some(adapter) = error.find_algeon_error() {
return (adapter.code(), client_message(adapter).text);
}
let code = classify(error, statement);
let foreign = Error::new(code, FOREIGN_FAILURE_OPERATION, client_datafusion_error_message(error));
(code, client_message(&foreign).text)
}
fn client_status_message(error: &Error) -> String {
match error.code() {
ErrorCode::AlgorithmDidNotConverge => format!(
"{} did not converge after {} iterations (max_iterations={}, epsilon={}); {} and retry",
error.fact_value("algorithm").unwrap_or("native algorithm"),
error.fact_value("iterations").unwrap_or("unknown"),
error.fact_value("max_iterations").unwrap_or("unknown"),
error.fact_value("epsilon").unwrap_or("unknown"),
error.fact_value("remedy").unwrap_or("change the convergence parameters"),
),
ErrorCode::SqlParse | ErrorCode::FieldNotFound | ErrorCode::ObjectNotFound | ErrorCode::DataFusionPlanning => {
error.message().to_string()
},
ErrorCode::SourceAccessDenied => "source object storage access denied while processing the request".to_string(),
ErrorCode::SourceUnauthenticated => {
"source object storage credentials are missing or invalid while processing the request".to_string()
},
ErrorCode::SourceObjectNotFound => {
"source object storage object was not found while processing the request".to_string()
},
ErrorCode::SourceTemporaryFailure => {
"temporary source object storage failure while processing the request; retry later".to_string()
},
ErrorCode::ServiceOverloaded => "service overloaded; retry later".to_string(),
ErrorCode::QueryMemoryLimitExceeded => format!(
"query memory cap exceeded{}. Reduce what the failing operation holds (filter or project earlier, split the query) or raise `admission.device_profiles.default_query_limit_bytes` for the device; resubmitting the request unchanged fails again",
memory_failure_detail(error).unwrap_or_else(|| format!(": {}", error.message()))
),
ErrorCode::QueryBudgetUnavailable => format!(
"query budget cannot fit on any eligible device: {}. Request a smaller query cap or wait for device capacity to free; do not retry the same request unchanged",
error.message()
),
ErrorCode::SizeOverflow => format!(
"result size computation overflowed the platform range: {}. Change the request shape to produce a smaller result; retrying the same request unchanged cannot succeed",
error.message()
),
ErrorCode::HostMaterializationLimitExceeded => format!(
"one result row exceeds the configured host materialization limit: {}. Narrow the request or raise the limit; retrying the same request unchanged cannot succeed",
error.message()
),
ErrorCode::HostStagingLimitExceeded => format!(
"host staging exceeded its configured byte ceiling: {}. Narrow the request or raise the ceiling; retrying the same request unchanged cannot succeed",
error.message()
),
ErrorCode::FlightFrameTooLarge => format!(
"one row exceeds the Flight message limit: {}. Project a smaller value; retrying the same request unchanged cannot succeed",
error.message()
),
ErrorCode::HostMemoryExhausted => format!(
"host memory pool exhausted while processing the request: {}. Reduce query resource usage; retrying the same request unchanged may fail again",
error.message()
),
ErrorCode::DeviceOutOfMemory | ErrorCode::DeviceAllocationFailed => format!(
"device memory exhausted{}. Reduce what the failing operation holds (filter or project earlier, split the query) and consult the sealed attempt report for retry guidance; resubmitting the request unchanged may fail again",
memory_failure_detail(error)
.unwrap_or_else(|| format!(" while processing the request: {}", error.message()))
),
ErrorCode::MemoryReservationFailed => format!(
"device memory exhausted while processing the request: {}. Reduce query resource usage and consult the sealed attempt report for retry guidance; retrying the same request unchanged may fail again",
error.message()
),
_ if error.kind() == ErrorKind::ResourceExhausted => format!(
"resource exhausted while processing the request: {}. Change the request to use fewer resources; do not retry the same request unchanged",
error.message()
),
_ if matches!(
error.kind(),
ErrorKind::Execution | ErrorKind::DependencyFailure | ErrorKind::InvariantViolation | ErrorKind::Internal
) =>
{
MASKED_INTERNAL_ERROR_MESSAGE.to_string()
},
_ => error.message().to_string(),
}
}
fn memory_failure_detail(error: &Error) -> Option<String> {
let bytes = |key: &str| error.fact_value(key).and_then(|value| value.parse::<u64>().ok());
let operation = error.fact_value("native_operation").or_else(|| error.fact_value("budget_operation"));
let clauses = [
bytes("requested_allocation_bytes").map(|value| format!("it requested {}", bytes_with_gib(value))),
bytes("budget_peak_bytes").map(|value| format!("the allocation peak was {}", bytes_with_gib(value))),
bytes("budget_bytes").map(|value| format!("the query grant is {}", bytes_with_gib(value))),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
let location = operation.map(|operation| format!(" in {operation}"));
if clauses.is_empty() && location.is_none() {
return None;
}
let figures = match clauses.as_slice() {
[] => String::new(),
[only] => format!(": {only}"),
[first, second] => format!(": {first} and {second}"),
[init @ .., last] => format!(": {}, and {last}", init.join(", ")),
};
Some(format!("{}{figures}", location.unwrap_or_default()))
}
fn client_schema_detail_message(detail: &ClientDetail, client_message: &str) -> String {
match detail {
ClientDetail::UnsupportedColumn { name, data_type, role } => {
format!("GPU execution does not support {role} column `{name}` with Arrow type `{data_type}`")
},
ClientDetail::RelationField { field, data_type } => {
format!("{client_message} (field '{field}' has Arrow type `{data_type}`)")
},
ClientDetail::Field { name } => format!("{client_message} (field '{name}')"),
ClientDetail::Column { name } => format!("{client_message} (column '{name}')"),
ClientDetail::OrderingKey { name } => format!("{client_message} (ORDER BY key '{name}')"),
ClientDetail::BlockingSubject { text } => format!("GPU execution does not support {text}"),
}
}
pub(crate) fn client_actionable_kind(error: &Error) -> bool {
matches!(
error.kind(),
ErrorKind::Cancelled
| ErrorKind::NotFound
| ErrorKind::PermissionDenied
| ErrorKind::Unauthenticated
| ErrorKind::Planning
| ErrorKind::InvalidInput
| ErrorKind::Unsupported
)
}
pub(crate) fn client_datafusion_error_message(error: &DataFusionError) -> String {
let effective = effective_datafusion_error(error);
let human_message = datafusion_error_human_message(effective);
let message = match effective {
DataFusionError::SQL(_, _) => format!("invalid SQL statement: {human_message}"),
_ => human_message,
};
truncate_with_marker(message.as_str(), MAX_CLIENT_DATAFUSION_MESSAGE_BYTES).0
}
fn effective_datafusion_error(error: &DataFusionError) -> &DataFusionError {
let mut current = error;
for _ in 0..MAX_SERVER_DATAFUSION_TRAVERSAL_DEPTH {
match current {
DataFusionError::Context(_, source) | DataFusionError::Diagnostic(_, source) => current = source.as_ref(),
DataFusionError::Collection(errors) => match errors.first() {
Some(first) => current = first,
None => break,
},
DataFusionError::Shared(source) => current = source.as_ref(),
_ => break,
}
}
current
}