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}