use thiserror::Error;
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum OxiaError {
#[error("key not found")]
KeyNotFound,
#[error("unexpected version id")]
UnexpectedVersionId,
#[error("session no longer exists")]
SessionExpired,
#[error("request is too large")]
RequestTooLarge,
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("no leader available for shard {shard}")]
LeaderNotFound {
shard: i64,
},
#[error("no shard owns key {key:?} (assignments not ready)")]
NoShardForKey {
key: String,
},
#[error("disconnected: {0}")]
Disconnected(String),
#[error("request timed out")]
Timeout,
#[error("grpc status {code:?}: {message}")]
Grpc {
code: tonic::Code,
message: String,
},
#[error("failed to decode server response: {0}")]
Decode(String),
#[error("shard was split or merged")]
ShardMoved,
#[error("client is closed")]
Closed,
}
impl OxiaError {
pub fn is_retryable(&self) -> bool {
match self {
OxiaError::LeaderNotFound { .. }
| OxiaError::NoShardForKey { .. }
| OxiaError::ShardMoved
| OxiaError::Disconnected(_) => true,
OxiaError::Grpc { code, .. } => {
matches!(
code,
tonic::Code::Unavailable | tonic::Code::Aborted | tonic::Code::Cancelled
)
}
_ => false,
}
}
}
impl From<tonic::Status> for OxiaError {
fn from(status: tonic::Status) -> Self {
if status.code() == tonic::Code::Unknown && std::error::Error::source(&status).is_some() {
return OxiaError::Disconnected(format!("transport error: {}", status.message()));
}
OxiaError::Grpc {
code: status.code(),
message: status.message().to_string(),
}
}
}
impl From<tonic::transport::Error> for OxiaError {
fn from(err: tonic::transport::Error) -> Self {
OxiaError::Disconnected(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retryable_classification() {
let msg = String::new();
assert!(OxiaError::LeaderNotFound { shard: 1 }.is_retryable());
assert!(OxiaError::NoShardForKey { key: "k".into() }.is_retryable());
assert!(OxiaError::Disconnected("x".into()).is_retryable());
assert!(
OxiaError::Grpc {
code: tonic::Code::Unavailable,
message: msg.clone(),
}
.is_retryable()
);
assert!(
OxiaError::Grpc {
code: tonic::Code::Aborted,
message: msg.clone(),
}
.is_retryable()
);
assert!(
OxiaError::Grpc {
code: tonic::Code::Cancelled,
message: msg.clone(),
}
.is_retryable()
);
assert!(!OxiaError::KeyNotFound.is_retryable());
assert!(!OxiaError::UnexpectedVersionId.is_retryable());
assert!(!OxiaError::SessionExpired.is_retryable());
assert!(!OxiaError::RequestTooLarge.is_retryable());
assert!(OxiaError::ShardMoved.is_retryable());
assert!(!OxiaError::Timeout.is_retryable());
assert!(!OxiaError::Closed.is_retryable());
assert!(
!OxiaError::Grpc {
code: tonic::Code::NotFound,
message: msg,
}
.is_retryable()
);
}
#[test]
fn from_tonic_status_maps_to_grpc() {
let err = OxiaError::from(tonic::Status::unavailable("down"));
assert!(matches!(
err,
OxiaError::Grpc {
code: tonic::Code::Unavailable,
..
}
));
assert!(err.is_retryable());
}
}