#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{operation} failed: {message}")]
Cluster {
operation: String,
message: String,
code: Option<i32>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("{operation}: {message}")]
Transport {
operation: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("{operation} timed out")]
Timeout { operation: String },
#[error("{0}")]
Conversion(String),
#[error("{transport} does not support {what}")]
Unsupported {
transport: crate::Transport,
what: &'static str,
},
}
impl Error {
pub fn code(&self) -> Option<i32> {
match self {
Self::Cluster { code, .. } => *code,
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
matches!(self, Self::Transport { .. } | Self::Timeout { .. })
}
pub fn cluster(
operation: impl Into<String>,
message: impl Into<String>,
code: Option<i32>,
) -> Self {
Self::Cluster {
operation: operation.into(),
message: message.into(),
code,
source: None,
}
}
pub fn cluster_from(
operation: impl Into<String>,
code: Option<i32>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Cluster {
operation: operation.into(),
message: source.to_string(),
code,
source: Some(Box::new(source)),
}
}
pub fn transport_from(
operation: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Transport {
operation: operation.into(),
message: source.to_string(),
source: Some(Box::new(source)),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub mod codes {
pub const OK: i32 = 0;
pub const GENERIC: i32 = 1;
pub const TIMEOUT: i32 = 3;
pub const RESOLVE_ERROR: i32 = 500;
pub const AUTHENTICATION_ERROR: i32 = 900;
pub const NO_SUCH_TRANSACTION: i32 = 11000;
pub const TABLET_NOT_MOUNTED: i32 = 1702;
}
pub fn describe(error: &dyn std::error::Error) -> String {
let mut description = error.to_string();
let mut current = error.source();
while let Some(cause) = current {
description.push_str("\n caused by: ");
description.push_str(&cause.to_string());
current = cause.source();
}
description
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("the underlying thing broke")]
struct Underlying;
#[test]
fn a_cluster_refusal_keeps_its_code() {
let error = Error::cluster("lookup_rows", "no such table", Some(codes::RESOLVE_ERROR));
assert_eq!(error.code(), Some(codes::RESOLVE_ERROR));
assert!(!error.is_retryable(), "a refusal will refuse again");
assert!(error.to_string().contains("lookup_rows failed"));
}
#[test]
fn a_transport_failure_is_worth_retrying() {
let error = Error::transport_from("select_rows", Underlying);
assert!(error.is_retryable());
assert_eq!(error.code(), None);
}
#[test]
fn the_original_error_survives_underneath() {
let error = Error::cluster_from("insert_rows", Some(1), Underlying);
let described = describe(&error);
assert!(
described.contains("the underlying thing broke"),
"the transport's own error must not be thrown away: {described}"
);
}
#[test]
fn an_unsupported_operation_names_the_transport() {
let error = Error::Unsupported {
transport: crate::Transport::Rpc,
what: "operations",
};
assert_eq!(error.to_string(), "RPC does not support operations");
}
}