use std::error::Error as StdError;
use std::time::Duration;
use crate::api_key::ApiKey;
use crate::error_code::ErrorCode;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{context}: {source}")]
Transport {
context: &'static str,
#[source]
source: std::io::Error,
},
#[error("connection to {peer} closed")]
ConnectionClosed {
peer: String,
},
#[error("{api_key} timed out after {elapsed:?}")]
Timeout {
api_key: ApiKey,
elapsed: Duration,
},
#[error("authentication failed: {0}")]
Authentication(String),
#[error("not authorized: {0}")]
Authorization(ErrorCode),
#[error("broker returned {code}{}", .message.as_ref().map(|m| format!(": {m}")).unwrap_or_default())]
Broker {
code: ErrorCode,
message: Option<String>,
},
#[error("{context}: {source}")]
Decode {
context: &'static str,
#[source]
source: Box<dyn StdError + Send + Sync>,
},
#[error("client is read-only and {api_key} mutates cluster state")]
ReadOnly {
api_key: ApiKey,
},
#[error("no usable version of {api_key}: broker offers {broker:?}, we speak {ours:?}")]
UnsupportedApi {
api_key: ApiKey,
broker: Option<(i16, i16)>,
ours: Option<(i16, i16)>,
},
#[error("unsupported: {0}")]
Unsupported(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
}
impl Error {
pub fn transport(context: &'static str, source: std::io::Error) -> Self {
Error::Transport { context, source }
}
pub fn decode(
context: &'static str,
source: impl Into<Box<dyn StdError + Send + Sync>>,
) -> Self {
Error::Decode {
context,
source: source.into(),
}
}
pub fn from_code(code: ErrorCode, message: Option<String>) -> Self {
if code.is_authentication() {
Error::Authentication(message.unwrap_or_else(|| code.to_string()))
} else if code.is_authorization() {
Error::Authorization(code)
} else {
Error::Broker { code, message }
}
}
pub fn code(&self) -> Option<ErrorCode> {
match self {
Error::Broker { code, .. } | Error::Authorization(code) => Some(*code),
_ => None,
}
}
pub fn retriable(&self) -> bool {
match self {
Error::Transport { .. } | Error::ConnectionClosed { .. } | Error::Timeout { .. } => {
true
}
Error::Broker { code, .. } => code.retriable(),
Error::Authorization(_)
| Error::Authentication(_)
| Error::Decode { .. }
| Error::ReadOnly { .. }
| Error::UnsupportedApi { .. }
| Error::Unsupported(_)
| Error::InvalidRequest(_) => false,
}
}
pub fn needs_metadata_refresh(&self) -> bool {
match self {
Error::Transport { .. } | Error::ConnectionClosed { .. } => true,
Error::Broker { code, .. } => code.needs_metadata_refresh(),
_ => false,
}
}
pub fn needs_coordinator_refresh(&self) -> bool {
match self {
Error::Broker { code, .. } => code.needs_coordinator_refresh(),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_codes_do_not_hide_inside_broker() {
let authn = Error::from_code(ErrorCode::SaslAuthenticationFailed, None);
assert!(matches!(authn, Error::Authentication(_)));
let authz = Error::from_code(ErrorCode::TopicAuthorizationFailed, None);
assert!(matches!(authz, Error::Authorization(_)));
assert!(!authz.retriable());
}
#[test]
fn decode_failures_are_never_retried() {
let err = Error::decode("test", std::io::Error::other("boom"));
assert!(!err.retriable());
assert!(!err.needs_metadata_refresh());
}
#[test]
fn transport_failures_invalidate_metadata() {
let err = Error::transport("connect", std::io::Error::other("boom"));
assert!(err.retriable());
assert!(err.needs_metadata_refresh());
}
#[test]
fn broker_errors_delegate_both_axes() {
let err = Error::from_code(ErrorCode::NotLeaderOrFollower, None);
assert!(err.retriable());
assert!(err.needs_metadata_refresh());
assert!(!err.needs_coordinator_refresh());
let err = Error::from_code(ErrorCode::NotCoordinator, None);
assert!(err.needs_coordinator_refresh());
assert!(!err.needs_metadata_refresh());
}
}