use std::{borrow::Cow, error::Error, fmt, sync::Arc};
use crate::fail::Level;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Lane {
Rejected,
Denied,
Transient,
Fatal,
}
impl Lane {
pub fn as_str(self) -> &'static str {
match self {
Lane::Rejected => "rejected",
Lane::Denied => "denied",
Lane::Transient => "transient",
Lane::Fatal => "fatal",
}
}
}
impl Lane {
pub fn level(self) -> Level {
match self {
Lane::Rejected => Level::Info,
Lane::Denied => Level::Warn,
Lane::Transient => Level::Info,
Lane::Fatal => Level::Error,
}
}
}
impl fmt::Display for Lane {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransientKind {
OptimisticConflict,
SerializationFailure,
Deadlock,
PoolTimeout,
ConnectionLost,
UpstreamUnavailable,
Congestion,
Other,
}
impl TransientKind {
pub fn as_str(self) -> &'static str {
match self {
TransientKind::OptimisticConflict => "optimistic_conflict",
TransientKind::SerializationFailure => "serialization_failure",
TransientKind::Deadlock => "deadlock",
TransientKind::PoolTimeout => "pool_timeout",
TransientKind::ConnectionLost => "connection_lost",
TransientKind::UpstreamUnavailable => "upstream_unavailable",
TransientKind::Congestion => "congestion",
TransientKind::Other => "other",
}
}
pub fn is_congestion(self) -> bool {
matches!(self, TransientKind::PoolTimeout | TransientKind::Congestion)
}
pub fn is_contention(self) -> bool {
matches!(
self,
TransientKind::Deadlock | TransientKind::SerializationFailure
)
}
pub fn from_sqlstate(code: &str) -> Option<Self> {
match code {
"40001" => Some(TransientKind::SerializationFailure),
"40P01" => Some(TransientKind::Deadlock),
"57P01" | "57P02" | "57P03" | "08000" | "08003" | "08006" | "08001" | "08004" => {
Some(TransientKind::ConnectionLost)
}
_ => None,
}
}
}
impl fmt::Display for TransientKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Transient {
pub kind: TransientKind,
pub context: Option<Cow<'static, str>>,
source: Option<Arc<dyn Error + Send + Sync + 'static>>,
}
impl Transient {
pub fn new(kind: TransientKind) -> Self {
Self {
kind,
context: None,
source: None,
}
}
pub fn from_error(kind: TransientKind, e: impl Error + Send + Sync + 'static) -> Self {
Self::new(kind).with_source(e)
}
pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(e));
self
}
pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
self.context = Some(c.into());
self
}
pub fn source_arc(&self) -> Option<&Arc<dyn Error + Send + Sync>> {
self.source.as_ref()
}
pub fn is_congestion(&self) -> bool {
self.kind.is_congestion()
}
pub fn is_contention(&self) -> bool {
self.kind.is_contention()
}
}
impl fmt::Display for Transient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "transient({})", self.kind)?;
if let Some(ctx) = &self.context {
write!(f, ": {ctx}")?;
}
Ok(())
}
}
impl Error for Transient {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FatalKind {
Invariant,
Config,
CorruptState,
Dependency,
Panic,
Exhausted,
Denied,
}
impl FatalKind {
pub fn as_str(self) -> &'static str {
match self {
FatalKind::Invariant => "invariant",
FatalKind::Config => "config",
FatalKind::CorruptState => "corrupt_state",
FatalKind::Dependency => "dependency",
FatalKind::Panic => "panic",
FatalKind::Exhausted => "exhausted",
FatalKind::Denied => "denied",
}
}
}
impl fmt::Display for FatalKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Fatal {
pub kind: FatalKind,
pub context: Option<Cow<'static, str>>,
source: Option<Arc<dyn Error + Send + Sync + 'static>>,
opaque_source: bool,
}
impl Fatal {
pub fn new(kind: FatalKind) -> Self {
Self {
kind,
context: None,
source: None,
opaque_source: false,
}
}
pub fn from_error(kind: FatalKind, e: impl Error + Send + Sync + 'static) -> Self {
Self::new(kind).with_source(e)
}
pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(e));
self
}
pub fn from_boxed(kind: FatalKind, e: Box<dyn Error + Send + Sync>) -> Self {
Self {
kind,
context: None,
source: Some(Arc::from(e)),
opaque_source: false,
}
}
pub fn from_dyn(kind: FatalKind, e: &(dyn Error + 'static)) -> Self {
Self::new(kind).with_context(crate::dynamic::message_chain(e))
}
pub fn invariant(msg: impl Into<Cow<'static, str>>) -> Self {
Self {
kind: FatalKind::Invariant,
context: Some(msg.into()),
source: None,
opaque_source: false,
}
}
pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
self.context = Some(c.into());
self
}
pub(crate) fn with_opaque_source(mut self) -> Self {
self.opaque_source = true;
self
}
pub(crate) fn has_opaque_source(&self) -> bool {
self.opaque_source
}
}
impl fmt::Display for Fatal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "fatal({})", self.kind)?;
if let Some(ctx) = &self.context {
write!(f, ": {ctx}")?;
}
Ok(())
}
}
impl Error for Fatal {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
#[derive(Debug, Clone, Default)]
pub struct Denied {
pub object: Option<Cow<'static, str>>,
pub action: Option<Cow<'static, str>>,
context: Option<Cow<'static, str>>,
source: Option<Arc<dyn Error + Send + Sync + 'static>>,
}
impl Denied {
pub fn new() -> Self {
Self::default()
}
pub fn from_error(e: impl Error + Send + Sync + 'static) -> Self {
Self::new().with_source(e)
}
pub fn with_object(mut self, object: impl Into<Cow<'static, str>>) -> Self {
self.object = Some(object.into());
self
}
pub fn with_action(mut self, action: impl Into<Cow<'static, str>>) -> Self {
self.action = Some(action.into());
self
}
pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(e));
self
}
pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
self.context = Some(c.into());
self
}
pub fn context(&self) -> Option<&str> {
self.context.as_deref()
}
pub fn source_arc(&self) -> Option<&Arc<dyn Error + Send + Sync>> {
self.source.as_ref()
}
pub(crate) fn into_fatal(mut self) -> Fatal {
let context = self.context.take();
let fatal = Fatal::from_error(FatalKind::Denied, self);
match context {
Some(c) => fatal.with_context(c),
None => fatal,
}
}
}
impl fmt::Display for Denied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("denied")?;
if self.action.is_some() || self.object.is_some() {
f.write_str(": ")?;
if let Some(action) = &self.action {
write!(f, "{action} ")?;
}
if let Some(object) = &self.object {
write!(f, "on {object}")?;
}
}
Ok(())
}
}
impl Error for Denied {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|s| s.as_ref() as &(dyn Error + 'static))
}
}
#[derive(Debug, Clone)]
pub struct Exhausted {
pub attempts: u32,
pub last: Transient,
}
impl fmt::Display for Exhausted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"exhausted after {} attempts: {}",
self.attempts, self.last
)
}
}
impl Error for Exhausted {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.last)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_sqlstate_is_reachable_without_the_sqlx_feature() {
assert_eq!(
TransientKind::from_sqlstate("40P01"),
Some(TransientKind::Deadlock)
);
assert_eq!(TransientKind::from_sqlstate("not-a-code"), None);
}
#[test]
fn contention_is_the_server_confirmed_abort_subset_of_transient() {
assert!(TransientKind::Deadlock.is_contention());
assert!(TransientKind::SerializationFailure.is_contention());
assert!(!TransientKind::OptimisticConflict.is_contention());
assert!(!TransientKind::ConnectionLost.is_contention());
assert!(!TransientKind::PoolTimeout.is_contention());
assert!(!TransientKind::Congestion.is_contention());
assert!(!TransientKind::Other.is_contention());
assert!(
TransientKind::from_sqlstate("40P01")
.unwrap()
.is_contention()
);
assert!(
TransientKind::from_sqlstate("40001")
.unwrap()
.is_contention()
);
assert!(
!TransientKind::from_sqlstate("08006")
.unwrap()
.is_contention()
);
assert!(
!TransientKind::from_sqlstate("57P01")
.unwrap()
.is_contention()
);
}
}