use std::convert::Infallible;
use std::fmt::{Display, Formatter};
macro_rules! fmt {
($code:ident, $($arg:tt)*) => {
crate::error::Error::new(
crate::error::ErrorCode::$code,
format!($($arg)*))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
CouldNotResolveAddr,
InvalidApiCall,
SocketError,
ConnectTimeout,
InvalidUtf8,
InvalidName,
InvalidTimestamp,
AuthError,
TlsError,
HttpNotSupported,
ServerFlushError,
ConfigError,
ArrayError,
ProtocolVersionError,
InvalidDecimal,
ServerRejection,
ArrowUnsupportedColumnKind,
ArrowIngest,
FailoverRetry,
RoleMismatch,
HandshakeError,
UnsupportedServer,
ProtocolError,
InvalidBind,
ServerSchemaMismatch,
ServerParseError,
ServerInternalError,
ServerSecurityError,
LimitExceeded,
ServerLimitExceeded,
Cancelled,
FailoverWouldDuplicate,
SchemaDrift,
NoSchema,
ArrowExport,
BatchTooLarge,
StoreResendRequired,
SymbolDictFull,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Error(Box<ErrorInner>);
#[derive(Debug, PartialEq, Eq, Clone)]
struct ErrorInner {
code: ErrorCode,
msg: String,
in_doubt: bool,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_rejection: Option<Box<crate::ingress::QwpWsSenderError>>,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_role_reject: Option<crate::ingress::QwpWsRoleReject>,
#[cfg(feature = "_egress")]
upgrade_reject: Option<crate::egress::server_event::UpgradeReject>,
#[cfg(feature = "_egress")]
server_info: Option<crate::egress::server_event::ServerInfo>,
}
impl Error {
pub fn new<S: Into<String>>(code: ErrorCode, msg: S) -> Error {
Error(Box::new(ErrorInner {
code,
msg: msg.into(),
in_doubt: false,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_rejection: None,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_role_reject: None,
#[cfg(feature = "_egress")]
upgrade_reject: None,
#[cfg(feature = "_egress")]
server_info: None,
}))
}
#[must_use]
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) fn with_in_doubt(mut self, in_doubt: bool) -> Self {
self.0.in_doubt = in_doubt;
self
}
#[must_use]
pub fn in_doubt(&self) -> bool {
self.0.in_doubt
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn with_qwp_ws_rejection(mut self, rejection: crate::ingress::QwpWsSenderError) -> Self {
self.0.qwp_ws_rejection = Some(Box::new(rejection));
self
}
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) fn with_qwp_ws_role_reject(
mut self,
role_reject: crate::ingress::QwpWsRoleReject,
) -> Self {
self.0.qwp_ws_role_reject = Some(role_reject);
self
}
#[cfg(feature = "_egress")]
pub fn with_upgrade_reject(
mut self,
reject: crate::egress::server_event::UpgradeReject,
) -> Self {
self.0.upgrade_reject = Some(reject);
self
}
#[cfg(feature = "_egress")]
pub fn with_server_info(mut self, info: crate::egress::server_event::ServerInfo) -> Self {
self.0.server_info = Some(info);
self
}
#[cfg(feature = "sync-sender-http")]
pub(crate) fn from_ureq_error(err: ureq::Error, url: &str) -> Error {
match err {
ureq::Error::StatusCode(code) => {
if code == 404 {
fmt!(
HttpNotSupported,
"Could not flush buffer: HTTP endpoint does not support ILP."
)
} else if [401, 403].contains(&code) {
fmt!(
AuthError,
"Could not flush buffer: HTTP endpoint authentication error [code: {}]",
code
)
} else {
fmt!(SocketError, "Could not flush buffer: {}: {}", url, err)
}
}
e => {
fmt!(SocketError, "Could not flush buffer: {}: {}", url, e)
}
}
}
pub fn code(&self) -> ErrorCode {
self.0.code
}
pub fn msg(&self) -> &str {
&self.0.msg
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn qwp_ws_rejection(&self) -> Option<&crate::ingress::QwpWsSenderError> {
self.0.qwp_ws_rejection.as_deref()
}
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) fn qwp_ws_role_reject(&self) -> Option<&crate::ingress::QwpWsRoleReject> {
self.0.qwp_ws_role_reject.as_ref()
}
#[cfg(feature = "_egress")]
pub fn upgrade_reject(&self) -> Option<&crate::egress::server_event::UpgradeReject> {
self.0.upgrade_reject.as_ref()
}
#[cfg(feature = "_egress")]
pub fn server_info(&self) -> Option<&crate::egress::server_event::ServerInfo> {
self.0.server_info.as_ref()
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.msg)
}
}
impl std::error::Error for Error {}
impl From<Infallible> for Error {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) use fmt;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn errors_are_not_in_doubt_by_default() {
let err = Error::new(ErrorCode::SocketError, "boom");
assert!(!err.in_doubt());
}
#[test]
#[cfg(feature = "sync-sender-qwp-ws")]
fn with_in_doubt_sets_and_preserves_code_and_msg() {
let err =
Error::new(ErrorCode::FailoverRetry, "mid-frame write failed").with_in_doubt(true);
assert!(err.in_doubt());
assert_eq!(err.code(), ErrorCode::FailoverRetry);
assert_eq!(err.msg(), "mid-frame write failed");
assert!(!err.with_in_doubt(false).in_doubt());
}
#[test]
fn display_matches_msg() {
let err = Error::new(ErrorCode::ProtocolError, "boom");
assert_eq!(format!("{}", err), "boom");
}
#[test]
fn fmt_macro_builds_error() {
let err = fmt!(ProtocolError, "bad code 0x{:02X}", 0xAB);
assert_eq!(err.code(), ErrorCode::ProtocolError);
assert_eq!(err.msg(), "bad code 0xAB");
}
#[cfg(feature = "_egress")]
#[test]
fn server_info_and_upgrade_reject_round_trip() {
use crate::egress::server_event::{ServerInfo, ServerRole, UpgradeReject};
let err_plain = Error::new(ErrorCode::SocketError, "x");
assert!(err_plain.server_info().is_none());
assert!(err_plain.upgrade_reject().is_none());
let info = ServerInfo {
role: ServerRole::Replica,
epoch: 7,
capabilities: 0,
server_wall_ns: 1_700_000_000_000_000_000,
cluster_id: "c-1".into(),
node_id: "n-2".into(),
zone_id: Some("eu-west-1a".into()),
};
let reject = UpgradeReject::new(0x02, "REPLICA", Some("eu-west-1a".into()));
let err = Error::new(ErrorCode::RoleMismatch, "no match")
.with_server_info(info.clone())
.with_upgrade_reject(reject.clone());
assert_eq!(err.server_info(), Some(&info));
assert_eq!(err.upgrade_reject(), Some(&reject));
}
#[test]
fn error_code_is_exhaustively_known() {
fn _exhaustive(code: ErrorCode) {
match code {
ErrorCode::CouldNotResolveAddr => {}
ErrorCode::InvalidApiCall => {}
ErrorCode::SocketError => {}
ErrorCode::ConnectTimeout => {}
ErrorCode::InvalidUtf8 => {}
ErrorCode::InvalidName => {}
ErrorCode::InvalidTimestamp => {}
ErrorCode::AuthError => {}
ErrorCode::TlsError => {}
ErrorCode::HttpNotSupported => {}
ErrorCode::ServerFlushError => {}
ErrorCode::ConfigError => {}
ErrorCode::ArrayError => {}
ErrorCode::ProtocolVersionError => {}
ErrorCode::InvalidDecimal => {}
ErrorCode::ServerRejection => {}
ErrorCode::ArrowUnsupportedColumnKind => {}
ErrorCode::ArrowIngest => {}
ErrorCode::FailoverRetry => {}
ErrorCode::RoleMismatch => {}
ErrorCode::HandshakeError => {}
ErrorCode::UnsupportedServer => {}
ErrorCode::ProtocolError => {}
ErrorCode::InvalidBind => {}
ErrorCode::ServerSchemaMismatch => {}
ErrorCode::ServerParseError => {}
ErrorCode::ServerInternalError => {}
ErrorCode::ServerSecurityError => {}
ErrorCode::LimitExceeded => {}
ErrorCode::ServerLimitExceeded => {}
ErrorCode::Cancelled => {}
ErrorCode::FailoverWouldDuplicate => {}
ErrorCode::SchemaDrift => {}
ErrorCode::NoSchema => {}
ErrorCode::ArrowExport => {}
ErrorCode::BatchTooLarge => {}
ErrorCode::StoreResendRequired => {}
ErrorCode::SymbolDictFull => {}
}
}
let _ = _exhaustive;
}
}