use std::fmt;
use crate::client::DisconnectReason;
use crate::config::ConfigError;
use crate::session::{ServerCause, SessionClosed};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TransportError {
#[error("cannot connect to {target}: {source}")]
Connect {
target: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("stream connection lost: {reason}")]
ConnectionLost {
reason: String,
},
#[error("cannot send control request `{name}`: {reason}")]
Send {
name: &'static str,
reason: String,
},
#[error("malformed frame: {reason}")]
MalformedFrame {
reason: String,
},
#[error("{reason}, exceeding this client's limit of {limit_bytes} bytes")]
Capacity {
limit_bytes: usize,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ServerError {
code: i64,
message: String,
}
impl ServerError {
#[must_use]
pub fn new(code: i64, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
#[must_use]
#[inline]
pub const fn code(&self) -> i64 {
self.code
}
#[must_use]
#[inline]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
#[inline]
pub const fn is_adapter_defined(&self) -> bool {
self.code <= 0
}
}
impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.message.is_empty() {
write!(f, "code {}", self.code)
} else {
write!(f, "code {}: {}", self.code, self.message)
}
}
}
impl From<ServerCause> for ServerError {
fn from(cause: ServerCause) -> Self {
Self {
code: cause.code,
message: cause.message,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("configuration error: {0}")]
Config(#[from] ConfigError),
#[error("session error: {0}")]
Session(ServerError),
#[error("request rejected: {0}")]
Request(ServerError),
#[error("gave up reconnecting after {attempts} consecutive failed attempts")]
ReconnectExhausted {
attempts: u32,
last: Option<Box<DisconnectReason>>,
},
#[error("the client is disconnected")]
Disconnected,
#[error("transport error: {0}")]
Transport(#[from] TransportError),
#[error("subscription {id} was not created by this client")]
ForeignSubscription {
id: u64,
},
#[error("internal error: {reason}")]
Internal {
reason: String,
},
}
impl Error {
#[must_use]
#[inline]
pub const fn server_error(&self) -> Option<&ServerError> {
match self {
Self::Session(cause) | Self::Request(cause) => Some(cause),
_ => None,
}
}
#[must_use]
#[inline]
pub const fn is_session_terminal(&self) -> bool {
matches!(
self,
Self::Session(_) | Self::ReconnectExhausted { .. } | Self::Disconnected
)
}
}
impl From<SessionClosed> for Error {
fn from(closed: SessionClosed) -> Self {
match closed {
SessionClosed::ByClient { .. } => Self::Disconnected,
SessionClosed::ByServer { cause } => Self::Session(cause.into()),
SessionClosed::RetriesExhausted { attempts, last } => Self::ReconnectExhausted {
attempts,
last: last.map(|reason| Box::new(DisconnectReason::from(reason))),
},
SessionClosed::Internal { reason } => Self::Internal { reason },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_error_preserves_the_code_as_a_number() {
let cause = ServerError::new(48, "maximum session duration reached");
assert_eq!(cause.code(), 48);
assert_eq!(cause.message(), "maximum session duration reached");
assert!(!cause.is_adapter_defined());
}
#[test]
fn test_server_error_recognizes_adapter_defined_codes() {
assert!(ServerError::new(0, "").is_adapter_defined());
assert!(ServerError::new(-7, "custom").is_adapter_defined());
assert!(!ServerError::new(1, "").is_adapter_defined());
}
#[test]
fn test_server_error_display_omits_an_empty_message() {
assert_eq!(ServerError::new(20, "").to_string(), "code 20");
assert_eq!(ServerError::new(20, "gone").to_string(), "code 20: gone");
}
#[test]
fn test_error_exposes_the_server_error_from_both_catalogs() {
let session = Error::Session(ServerError::new(1, "auth failed"));
let request = Error::Request(ServerError::new(23, "bad field schema"));
assert_eq!(session.server_error().map(ServerError::code), Some(1));
assert_eq!(request.server_error().map(ServerError::code), Some(23));
assert!(Error::Disconnected.server_error().is_none());
assert!(
Error::ReconnectExhausted {
attempts: 3,
last: None
}
.server_error()
.is_none()
);
}
#[test]
fn test_error_knows_which_conditions_end_the_session() {
assert!(Error::Session(ServerError::new(1, "")).is_session_terminal());
assert!(
Error::ReconnectExhausted {
attempts: 8,
last: None
}
.is_session_terminal()
);
assert!(Error::Disconnected.is_session_terminal());
assert!(!Error::Request(ServerError::new(19, "")).is_session_terminal());
assert!(!Error::Config(ConfigError::EmptyServerAddress).is_session_terminal());
}
#[test]
fn test_session_closed_maps_onto_the_public_taxonomy() {
let by_client = Error::from(SessionClosed::ByClient {
destroy_confirmed: true,
cause: None,
});
assert!(matches!(by_client, Error::Disconnected));
let by_server = Error::from(SessionClosed::ByServer {
cause: ServerCause {
code: 2,
message: "Requested Adapter Set not available".to_owned(),
},
});
assert!(matches!(by_server, Error::Session(cause) if cause.code() == 2));
let exhausted = Error::from(SessionClosed::RetriesExhausted {
attempts: 8,
last: None,
});
assert!(matches!(
exhausted,
Error::ReconnectExhausted {
attempts: 8,
last: None
}
));
let internal = Error::from(SessionClosed::Internal {
reason: "cannot encode".to_owned(),
});
assert!(matches!(internal, Error::Internal { .. }));
}
#[test]
fn test_exhaustion_keeps_the_last_thing_that_went_wrong() {
let closed = SessionClosed::RetriesExhausted {
attempts: 8,
last: Some(crate::session::UnbindReason::KeepaliveExpired {
budget: std::time::Duration::from_secs(8),
}),
};
match Error::from(closed) {
Error::ReconnectExhausted {
attempts: 8,
last: Some(reason),
} => assert!(matches!(*reason, DisconnectReason::Stalled { .. })),
other => panic!("the last cause was discarded: {other:?}"),
}
let refused = SessionClosed::RetriesExhausted {
attempts: 3,
last: Some(crate::session::UnbindReason::Rejected {
cause: ServerCause {
code: 1,
message: "User/password check failed".to_owned(),
},
}),
};
match Error::from(refused) {
Error::ReconnectExhausted {
last: Some(reason), ..
} => {
assert!(matches!(*reason, DisconnectReason::Refused(cause) if cause.code() == 1));
}
other => panic!("the last cause was discarded: {other:?}"),
}
}
#[test]
fn test_config_error_converts_into_the_public_error() {
let error: Error = ConfigError::EmptyServerAddress.into();
assert!(matches!(
error,
Error::Config(ConfigError::EmptyServerAddress)
));
}
}