Skip to main content

connpool/core/
errors.rs

1//! The error taxonomy raised by the pool.
2//!
3//! The sibling implementations use an exception hierarchy (Python/C#/Java) or a marker
4//! interface over several structs (Go). Rust's idiom is a single enum implementing
5//! [`std::error::Error`]: callers match on the variant (the analog of `except PoolTimeoutError`
6//! / `errors.As(&PoolTimeoutError{})`). The taxonomy — config, closed, timeout, exhausted,
7//! connection-create, circuit-open, invalid-release — is preserved one-to-one.
8
9use std::error::Error;
10use std::fmt;
11
12/// The boxed error type a [`ConnectionFactory`](crate::ConnectionFactory) returns from
13/// `create`/`reset`/`close`. Any `std::error::Error` works.
14pub type FactoryError = Box<dyn Error + Send + Sync + 'static>;
15
16/// Every error the pool raises. Mirrors the `PoolError` base type of the other languages;
17/// each variant corresponds to a sibling error class.
18#[derive(Debug)]
19pub enum PoolError {
20    /// Invalid pool / breaker / retry configuration (`PoolConfigError`).
21    Config(String),
22    /// The pool has been closed; no more connections can be acquired (`PoolClosedError`).
23    Closed(String),
24    /// No connection became available before the acquire deadline (`PoolTimeoutError`).
25    Timeout(String),
26    /// The pool is full and the waiter queue is at `max_waiters` (`PoolExhaustedError`).
27    Exhausted(String),
28    /// The factory failed to create a connection after all retries
29    /// (`ConnectionCreateError`). The original error is available via
30    /// [`Error::source`]/[`PoolError::source_error`].
31    ConnectionCreate {
32        /// Human-readable message.
33        message: String,
34        /// The underlying factory error, if any.
35        source: Option<FactoryError>,
36    },
37    /// The circuit breaker is rejecting calls to protect a failing backend
38    /// (`CircuitOpenError`). `retry_after` is the approximate seconds until it probes again.
39    CircuitOpen {
40        /// Approximate seconds until the breaker probes again.
41        retry_after: f64,
42    },
43    /// An object that is not checked out from this pool was released — a double release?
44    /// (`InvalidReleaseError`).
45    InvalidRelease(String),
46}
47
48impl PoolError {
49    /// Returns the underlying factory error for a [`PoolError::ConnectionCreate`], if present.
50    pub fn source_error(&self) -> Option<&(dyn Error + 'static)> {
51        match self {
52            PoolError::ConnectionCreate {
53                source: Some(e), ..
54            } => Some(&**e),
55            _ => None,
56        }
57    }
58}
59
60impl fmt::Display for PoolError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            PoolError::Config(m)
64            | PoolError::Closed(m)
65            | PoolError::Timeout(m)
66            | PoolError::Exhausted(m)
67            | PoolError::InvalidRelease(m) => f.write_str(m),
68            PoolError::ConnectionCreate { message, .. } => f.write_str(message),
69            PoolError::CircuitOpen { retry_after } => {
70                write!(f, "circuit breaker is open; retry in ~{retry_after:.1}s")
71            }
72        }
73    }
74}
75
76impl Error for PoolError {
77    fn source(&self) -> Option<&(dyn Error + 'static)> {
78        self.source_error()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn display_messages_match_wire_format() {
88        assert_eq!(PoolError::Config("bad".into()).to_string(), "bad");
89        assert_eq!(PoolError::Closed("x".into()).to_string(), "x");
90        assert_eq!(PoolError::Timeout("t".into()).to_string(), "t");
91        assert_eq!(PoolError::Exhausted("e".into()).to_string(), "e");
92        assert_eq!(PoolError::InvalidRelease("i".into()).to_string(), "i");
93        assert_eq!(
94            PoolError::CircuitOpen { retry_after: 2.34 }.to_string(),
95            "circuit breaker is open; retry in ~2.3s"
96        );
97        let cc = PoolError::ConnectionCreate {
98            message: "nope".into(),
99            source: None,
100        };
101        assert_eq!(cc.to_string(), "nope");
102    }
103
104    #[test]
105    fn connection_create_exposes_source() {
106        let inner: FactoryError = "boom".into();
107        let err = PoolError::ConnectionCreate {
108            message: "could not create".into(),
109            source: Some(inner),
110        };
111        assert_eq!(err.source().unwrap().to_string(), "boom");
112        assert_eq!(err.source_error().unwrap().to_string(), "boom");
113        // Non-create errors have no source.
114        assert!(PoolError::Timeout("t".into()).source().is_none());
115        // Debug is derived; touch it.
116        assert!(format!("{err:?}").contains("ConnectionCreate"));
117    }
118}