pool_mod/error.rs
1//! The pool error type.
2
3use std::fmt;
4
5/// An error returned by a [`Pool`](crate::Pool) operation.
6///
7/// The type parameter `E` is the manager's own error type
8/// ([`Manager::Error`](crate::Manager::Error)). A failure that originates inside
9/// the manager — a connection that could not be opened, a transaction that could
10/// not be rolled back — is carried unchanged in [`Error::Backend`], so the caller
11/// can inspect or match on the underlying cause rather than a stringified copy of
12/// it.
13///
14/// The enum is `#[non_exhaustive]`: future versions may add variants, so a match
15/// on it must include a wildcard arm.
16///
17/// # Examples
18///
19/// Distinguish a saturated pool from a backend failure:
20///
21/// ```
22/// use pool_mod::Error;
23/// use std::convert::Infallible;
24///
25/// fn describe(err: &Error<Infallible>) -> &'static str {
26/// match err {
27/// Error::Timeout => "pool is busy, try again",
28/// Error::Closed => "pool is shut down",
29/// _ => "other",
30/// }
31/// }
32///
33/// assert_eq!(describe(&Error::Timeout), "pool is busy, try again");
34/// ```
35#[non_exhaustive]
36#[derive(Debug)]
37pub enum Error<E> {
38 /// The manager failed to create or recycle a resource. Carries the
39 /// manager's own error.
40 ///
41 /// Inspect the inner error to decide whether the failure is transient (worth
42 /// retrying) or permanent.
43 Backend(E),
44
45 /// The configured wait elapsed before a resource became available.
46 ///
47 /// The pool is healthy but saturated. Retry later, raise `max_size`, or hold
48 /// borrowed resources for less time.
49 Timeout,
50
51 /// The pool has been shut down with [`Pool::close`](crate::Pool::close).
52 ///
53 /// No further resources will be handed out; this is terminal for the pool.
54 Closed,
55
56 /// The pool could not be built because its configuration is invalid.
57 ///
58 /// The message names the constraint that was violated (for example, a
59 /// `max_size` of zero).
60 InvalidConfig(&'static str),
61}
62
63impl<E: fmt::Display> fmt::Display for Error<E> {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Error::Backend(source) => write!(f, "resource manager error: {source}"),
67 Error::Timeout => f.write_str("timed out waiting for a resource from the pool"),
68 Error::Closed => f.write_str("the pool has been closed"),
69 Error::InvalidConfig(reason) => write!(f, "invalid pool configuration: {reason}"),
70 }
71 }
72}
73
74impl<E> std::error::Error for Error<E>
75where
76 E: std::error::Error + 'static,
77{
78 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79 match self {
80 Error::Backend(source) => Some(source),
81 Error::Timeout | Error::Closed | Error::InvalidConfig(_) => None,
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[derive(Debug)]
91 struct Backend;
92
93 impl fmt::Display for Backend {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 f.write_str("connection refused")
96 }
97 }
98
99 impl std::error::Error for Backend {}
100
101 #[test]
102 fn test_display_backend_includes_source_message() {
103 let err = Error::Backend(Backend);
104 assert_eq!(
105 err.to_string(),
106 "resource manager error: connection refused"
107 );
108 }
109
110 #[test]
111 fn test_display_timeout_is_actionable() {
112 let err: Error<Backend> = Error::Timeout;
113 assert_eq!(
114 err.to_string(),
115 "timed out waiting for a resource from the pool"
116 );
117 }
118
119 #[test]
120 fn test_display_invalid_config_names_constraint() {
121 let err: Error<Backend> = Error::InvalidConfig("max_size must be at least 1");
122 assert_eq!(
123 err.to_string(),
124 "invalid pool configuration: max_size must be at least 1"
125 );
126 }
127
128 #[test]
129 fn test_source_present_only_for_backend() {
130 use std::error::Error as _;
131
132 let backend = Error::Backend(Backend);
133 assert!(backend.source().is_some());
134
135 let timeout: Error<Backend> = Error::Timeout;
136 assert!(timeout.source().is_none());
137 }
138}