use thiserror::Error;
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum ZerobusError {
#[error("Failed to open a channel: {0}.")]
ChannelCreationError(String),
#[error("Failed to create stream: {0}.")]
CreateStreamError(tonic::Status),
#[error("Failed to establish TLS connection.")]
FailedToEstablishTlsConnectionError,
#[error("The specified Zerobus endpoint is in invalid format: {0}.")]
InvalidZerobusEndpointError(String),
#[error("Specified UC table name is invalid: {0}.")]
InvalidTableName(String),
#[error("Specified UC endpoint is in invalid format: {0}.")]
InvalidUCEndpointError(String),
#[error("Specified UC token is in invalid format: {0}.")]
InvalidUCTokenError(String),
#[error("Stream is closed: {0}")]
StreamClosedError(tonic::Status),
#[error("Invalid argument: {0}.")]
InvalidArgument(String),
#[error("Unexpected response from server. Response: {0}")]
UnexpectedStreamResponseError(String),
#[error("Stream is in invalid state: {0}")]
InvalidStateError(String),
#[error("Connection timeout: {0}")]
ConnectionTimeout(String),
#[error("Token fetch failed: {0}")]
TokenFetchError(String),
}
const UNRETRIABLE_STATUS_CODES: &[tonic::Code] = &[
tonic::Code::InvalidArgument,
tonic::Code::Unauthenticated,
tonic::Code::PermissionDenied,
tonic::Code::OutOfRange,
tonic::Code::Unimplemented,
tonic::Code::NotFound,
];
impl ZerobusError {
pub fn is_retryable(&self) -> bool {
match self {
ZerobusError::InvalidArgument(_) => false,
ZerobusError::StreamClosedError(status) => {
!UNRETRIABLE_STATUS_CODES.contains(&status.code())
}
ZerobusError::CreateStreamError(status) => {
!UNRETRIABLE_STATUS_CODES.contains(&status.code())
}
ZerobusError::ChannelCreationError(_) => true,
ZerobusError::FailedToEstablishTlsConnectionError => true,
ZerobusError::InvalidZerobusEndpointError(_) => false,
ZerobusError::InvalidTableName(_) => false,
ZerobusError::InvalidUCEndpointError(_) => false,
ZerobusError::InvalidUCTokenError(_) => false,
ZerobusError::UnexpectedStreamResponseError(_) => true,
ZerobusError::InvalidStateError(_) => false,
ZerobusError::ConnectionTimeout(_) => true,
ZerobusError::TokenFetchError(_) => true,
}
}
pub(crate) fn is_auth_rejection(&self) -> bool {
matches!(
self,
ZerobusError::CreateStreamError(status) | ZerobusError::StreamClosedError(status)
if matches!(
status.code(),
tonic::Code::Unauthenticated | tonic::Code::PermissionDenied
)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_rejection_classification() {
assert!(
ZerobusError::CreateStreamError(tonic::Status::unauthenticated("x"))
.is_auth_rejection()
);
assert!(
ZerobusError::CreateStreamError(tonic::Status::permission_denied("x"))
.is_auth_rejection()
);
assert!(
ZerobusError::StreamClosedError(tonic::Status::unauthenticated("x"))
.is_auth_rejection()
);
assert!(!ZerobusError::CreateStreamError(tonic::Status::internal("x")).is_auth_rejection());
assert!(
!ZerobusError::CreateStreamError(tonic::Status::unavailable("x")).is_auth_rejection()
);
assert!(!ZerobusError::TokenFetchError("x".to_string()).is_auth_rejection());
}
#[cfg(feature = "arrow-flight")]
#[test]
fn auth_rejection_survives_flight_error_conversion() {
use arrow_flight::error::FlightError;
let auth: tonic::Status =
FlightError::Tonic(Box::new(tonic::Status::permission_denied("denied"))).into();
assert!(ZerobusError::CreateStreamError(auth).is_auth_rejection());
let non_auth: tonic::Status =
FlightError::Tonic(Box::new(tonic::Status::unavailable("blip"))).into();
assert!(!ZerobusError::CreateStreamError(non_auth).is_auth_rejection());
}
}