use std::fmt;
use std::io;
pub const NOT_LEADER: &str = "not_leader";
pub const OUTCOME_UNKNOWN: &str = "outcome_unknown";
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
Io,
Timeout,
Protocol,
Auth,
Handshake,
Server,
Refused,
FeatureNotGranted,
InvalidArgument,
Tls,
Closed,
Pool,
TransactionLeftOpen,
}
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Error {
pub kind: ErrorKind,
pub code: Option<String>,
pub message: String,
pub leader_hint: Option<String>,
}
impl Error {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
code: None,
message: message.into(),
leader_hint: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub(crate) fn from_io(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => Error::new(
ErrorKind::Timeout,
format!("no reply within the read deadline: {e}"),
),
io::ErrorKind::UnexpectedEof => Error::new(
ErrorKind::Io,
"the server closed the connection mid-frame".to_string(),
),
_ => Error::new(ErrorKind::Io, e.to_string()),
}
}
pub(crate) fn with_code_opt(mut self, code: Option<String>) -> Self {
self.code = code;
self
}
pub(crate) fn feature_refusal(message: impl Into<String>) -> Self {
Error::new(ErrorKind::FeatureNotGranted, message).with_code("feature_not_granted")
}
pub(crate) fn was_refused_locally(&self) -> bool {
matches!(
self.kind,
ErrorKind::InvalidArgument | ErrorKind::FeatureNotGranted
)
}
pub(crate) fn protocol(message: impl Into<String>) -> Self {
Error::new(ErrorKind::Protocol, message)
}
pub(crate) fn invalid(message: impl Into<String>) -> Self {
Error::new(ErrorKind::InvalidArgument, message)
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn code(&self) -> Option<&str> {
self.code.as_deref()
}
pub fn leader_hint(&self) -> Option<&str> {
self.leader_hint.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn is_redirect(&self) -> bool {
self.code.as_deref() == Some(NOT_LEADER)
}
pub fn is_connection_fatal(&self) -> bool {
matches!(
self.kind,
ErrorKind::Io
| ErrorKind::Timeout
| ErrorKind::Protocol
| ErrorKind::Refused
| ErrorKind::Closed
| ErrorKind::Tls
| ErrorKind::Handshake
)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Error")
.field("kind", &self.kind)
.field("code", &self.code)
.field("message", &self.message)
.field("leader_hint", &self.leader_hint)
.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.code {
Some(code) => write!(f, "{:?} [{code}]: {}", self.kind, self.message)?,
None => write!(f, "{:?}: {}", self.kind, self.message)?,
}
if let Some(hint) = &self.leader_hint {
write!(f, " (leader at {hint})")?;
}
Ok(())
}
}
impl std::error::Error for Error {}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::from_io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_not_leader_code_is_a_redirect_with_or_without_a_hint() {
let mut e = Error::new(ErrorKind::Server, "x").with_code(NOT_LEADER);
assert!(e.is_redirect());
assert_eq!(e.leader_hint(), None);
e.leader_hint = Some("10.0.0.2:8427".into());
assert!(e.is_redirect());
assert_eq!(e.leader_hint(), Some("10.0.0.2:8427"));
assert!(e.to_string().contains("leader at 10.0.0.2:8427"));
}
#[test]
fn other_codes_are_not_redirects() {
assert!(!Error::new(ErrorKind::Server, "x")
.with_code("request.invalid")
.is_redirect());
assert!(!Error::new(ErrorKind::Io, "x").is_redirect());
}
#[test]
fn a_server_refusal_keeps_the_connection_usable() {
assert!(!Error::new(ErrorKind::Server, "bad sql").is_connection_fatal());
assert!(!Error::new(ErrorKind::FeatureNotGranted, "x").is_connection_fatal());
assert!(Error::new(ErrorKind::Timeout, "x").is_connection_fatal());
assert!(Error::new(ErrorKind::Refused, "x").is_connection_fatal());
}
#[test]
fn io_timeouts_map_to_timeout() {
let e = Error::from_io(io::Error::new(io::ErrorKind::TimedOut, "t"));
assert_eq!(e.kind, ErrorKind::Timeout);
let e = Error::from_io(io::Error::new(io::ErrorKind::WouldBlock, "t"));
assert_eq!(e.kind, ErrorKind::Timeout);
let e = Error::from_io(io::Error::new(io::ErrorKind::ConnectionReset, "r"));
assert_eq!(e.kind, ErrorKind::Io);
}
#[test]
fn a_missing_feature_is_named_and_carries_a_code() {
let e = Error::feature_refusal("begin() needs SESSION_TXN");
assert_eq!(e.kind, ErrorKind::FeatureNotGranted);
assert_eq!(e.code(), Some("feature_not_granted"));
assert!(e.was_refused_locally());
assert!(!Error::new(ErrorKind::Server, "x").was_refused_locally());
}
}