use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RetryClass {
Retryable,
Permanent,
}
impl RetryClass {
pub fn is_retryable(self) -> bool {
matches!(self, RetryClass::Retryable)
}
pub fn is_permanent(self) -> bool {
matches!(self, RetryClass::Permanent)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LockError {
Poisoned(String),
AcquireFailed(String),
ReleaseFailed(String),
Expired(String),
Busy(String),
Other(String),
}
impl LockError {
pub fn kind(&self) -> RetryClass {
match self {
LockError::Poisoned(_) => RetryClass::Permanent,
LockError::AcquireFailed(_)
| LockError::ReleaseFailed(_)
| LockError::Expired(_)
| LockError::Busy(_)
| LockError::Other(_) => RetryClass::Retryable,
}
}
pub fn is_retryable(&self) -> bool {
self.kind().is_retryable()
}
}
impl fmt::Display for LockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LockError::Poisoned(msg) => write!(f, "lock poisoned: {}", msg),
LockError::AcquireFailed(msg) => write!(f, "lock acquire failed: {}", msg),
LockError::ReleaseFailed(msg) => write!(f, "lock release failed: {}", msg),
LockError::Expired(msg) => write!(f, "lock expired: {}", msg),
LockError::Busy(msg) => write!(f, "lock busy: {}", msg),
LockError::Other(msg) => write!(f, "lock error: {}", msg),
}
}
}
impl std::error::Error for LockError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contention_and_lease_loss_are_retryable() {
for err in [
LockError::AcquireFailed("contended".into()),
LockError::ReleaseFailed("transient".into()),
LockError::Expired("ttl elapsed".into()),
LockError::Busy("database is locked".into()),
LockError::Other("unknown".into()),
] {
assert_eq!(err.kind(), RetryClass::Retryable, "{err}");
assert!(err.is_retryable());
}
}
#[test]
fn poisoned_is_permanent() {
let err = LockError::Poisoned("map poisoned".into());
assert_eq!(err.kind(), RetryClass::Permanent);
assert!(!err.is_retryable());
}
}