1use redis::{ErrorKind, RedisError};
8use thiserror::Error;
9
10#[derive(Debug, Error, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum GlideError {
14 #[error("ClosingError: {0}")]
16 Closing(String),
17
18 #[error("ConfigurationError: {0}")]
20 Configuration(String),
21
22 #[error("ConnectionError: {0}")]
24 Connection(String),
25
26 #[error("ExecAbortError: {0}")]
28 ExecAbort(String),
29
30 #[error("RequestError: {0}")]
32 Request(String),
33
34 #[error("TimeoutError: {0}")]
36 Timeout(String),
37
38 #[error("CircuitBreakerError: {0}")]
40 CircuitBreaker(String),
41}
42
43impl GlideError {
44 pub fn class_name(&self) -> &'static str {
46 match self {
47 GlideError::Closing(_) => "ClosingError",
48 GlideError::Configuration(_) => "ConfigurationError",
49 GlideError::Connection(_) => "ConnectionError",
50 GlideError::ExecAbort(_) => "ExecAbortError",
51 GlideError::Request(_) => "RequestError",
52 GlideError::Timeout(_) => "TimeoutError",
53 GlideError::CircuitBreaker(_) => "CircuitBreakerError",
54 }
55 }
56
57 pub fn message(&self) -> &str {
59 match self {
60 GlideError::Closing(m)
61 | GlideError::Configuration(m)
62 | GlideError::Connection(m)
63 | GlideError::ExecAbort(m)
64 | GlideError::Request(m)
65 | GlideError::Timeout(m)
66 | GlideError::CircuitBreaker(m) => m,
67 }
68 }
69
70 pub(crate) fn from_redis_error(err: RedisError) -> Self {
71 let msg = err.to_string();
72 if err.is_timeout() {
74 GlideError::Timeout(msg)
75 } else if err.is_unrecoverable_error() {
76 GlideError::Connection(msg)
77 } else {
78 match err.kind() {
79 ErrorKind::ExecAbortError => GlideError::ExecAbort(msg),
80 ErrorKind::CircuitBreakerOpen => GlideError::CircuitBreaker(msg),
81 ErrorKind::InvalidClientConfig => GlideError::Configuration(msg),
82 _ => GlideError::Request(msg),
83 }
84 }
85 }
86
87 pub(crate) fn from_connection_error(err: glide_core::client::ConnectionError) -> Self {
88 use glide_core::client::ConnectionError as CE;
89 match err {
90 CE::Timeout => GlideError::Timeout("connection attempt timed out".to_string()),
91 CE::Configuration(msg) => GlideError::Configuration(msg),
92 other => GlideError::Connection(other.to_string()),
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
103 use glide_core::client::ConnectionError as CE;
104
105 #[test]
108 fn redis_exec_abort_maps_to_exec_abort() {
109 let err = RedisError::from((ErrorKind::ExecAbortError, "aborted"));
110 let g = GlideError::from_redis_error(err);
111 assert!(matches!(g, GlideError::ExecAbort(_)));
112 assert_eq!(g.class_name(), "ExecAbortError");
113 }
114
115 #[test]
116 fn redis_circuit_breaker_maps_to_circuit_breaker() {
117 let err = RedisError::from((ErrorKind::CircuitBreakerOpen, "open"));
118 let g = GlideError::from_redis_error(err);
119 assert!(matches!(g, GlideError::CircuitBreaker(_)));
120 assert_eq!(g.class_name(), "CircuitBreakerError");
121 }
122
123 #[test]
124 fn redis_invalid_client_config_maps_to_configuration() {
125 let err = RedisError::from((ErrorKind::InvalidClientConfig, "bad config"));
126 let g = GlideError::from_redis_error(err);
127 assert!(matches!(g, GlideError::Configuration(_)));
128 assert_eq!(g.class_name(), "ConfigurationError");
129 }
130
131 #[test]
132 fn redis_response_error_maps_to_request() {
133 let err = RedisError::from((ErrorKind::ResponseError, "wrong type"));
134 let g = GlideError::from_redis_error(err);
135 assert!(matches!(g, GlideError::Request(_)));
136 assert_eq!(g.class_name(), "RequestError");
137 }
138
139 #[test]
140 fn redis_unknown_kind_maps_to_request() {
141 let err = RedisError::from((ErrorKind::TypeError, "type mismatch"));
143 let g = GlideError::from_redis_error(err);
144 assert!(matches!(g, GlideError::Request(_)));
145 }
146
147 #[test]
148 fn redis_io_error_maps_to_connection() {
149 let io_err = std::io::Error::from(std::io::ErrorKind::ConnectionReset);
150 let err: RedisError = io_err.into();
151 assert!(err.is_unrecoverable_error());
152 let g = GlideError::from_redis_error(err);
153 assert!(matches!(g, GlideError::Connection(_)));
154 assert_eq!(g.class_name(), "ConnectionError");
155 }
156
157 #[test]
158 fn redis_client_error_maps_to_request() {
159 let err = RedisError::from((ErrorKind::ClientError, "inflight limit reached"));
160 let g = GlideError::from_redis_error(err);
161 assert!(matches!(g, GlideError::Request(_)));
162 assert_eq!(g.class_name(), "RequestError");
163 }
164
165 #[test]
166 fn redis_real_io_timeout_maps_to_timeout() {
167 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
168 let err: RedisError = io_err.into();
169 assert!(err.is_timeout());
170 assert!(matches!(
171 GlideError::from_redis_error(err),
172 GlideError::Timeout(_)
173 ));
174 }
175
176 #[test]
177 fn redis_error_preserves_message() {
178 let err = RedisError::from((ErrorKind::ResponseError, "custom detail"));
179 let g = GlideError::from_redis_error(err);
180 assert!(g.message().contains("custom detail"));
181 }
182
183 #[test]
186 fn connection_timeout_maps_to_timeout() {
187 let g = GlideError::from_connection_error(CE::Timeout);
188 assert!(matches!(g, GlideError::Timeout(_)));
189 assert_eq!(g.class_name(), "TimeoutError");
190 }
191
192 #[test]
193 fn connection_configuration_maps_to_configuration() {
194 let g = GlideError::from_connection_error(CE::Configuration("nope".to_string()));
195 assert!(matches!(g, GlideError::Configuration(_)));
196 assert_eq!(g.message(), "nope");
197 }
198
199 #[test]
200 fn connection_io_error_maps_to_connection() {
201 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
202 let g = GlideError::from_connection_error(CE::IoError(io_err));
203 assert!(matches!(g, GlideError::Connection(_)));
204 assert_eq!(g.class_name(), "ConnectionError");
205 }
206
207 #[test]
208 fn connection_cluster_error_maps_to_connection() {
209 let redis_err = RedisError::from((ErrorKind::ResponseError, "cluster boom"));
210 let g = GlideError::from_connection_error(CE::Cluster(redis_err));
211 assert!(matches!(g, GlideError::Connection(_)));
212 }
213
214 #[test]
217 fn class_names_for_all_variants() {
218 assert_eq!(GlideError::Closing("x".into()).class_name(), "ClosingError");
219 assert_eq!(
220 GlideError::Configuration("x".into()).class_name(),
221 "ConfigurationError"
222 );
223 assert_eq!(
224 GlideError::Connection("x".into()).class_name(),
225 "ConnectionError"
226 );
227 assert_eq!(
228 GlideError::ExecAbort("x".into()).class_name(),
229 "ExecAbortError"
230 );
231 assert_eq!(GlideError::Request("x".into()).class_name(), "RequestError");
232 assert_eq!(GlideError::Timeout("x".into()).class_name(), "TimeoutError");
233 assert_eq!(
234 GlideError::CircuitBreaker("x".into()).class_name(),
235 "CircuitBreakerError"
236 );
237 }
238
239 #[test]
240 fn message_for_all_variants() {
241 assert_eq!(GlideError::Closing("a".into()).message(), "a");
242 assert_eq!(GlideError::Configuration("b".into()).message(), "b");
243 assert_eq!(GlideError::Connection("c".into()).message(), "c");
244 assert_eq!(GlideError::ExecAbort("d".into()).message(), "d");
245 assert_eq!(GlideError::Request("e".into()).message(), "e");
246 assert_eq!(GlideError::Timeout("f".into()).message(), "f");
247 assert_eq!(GlideError::CircuitBreaker("g".into()).message(), "g");
248 }
249
250 #[test]
251 fn display_includes_class_and_message() {
252 let e = GlideError::Timeout("slow".into());
253 assert_eq!(e.to_string(), "TimeoutError: slow");
254 }
255
256 #[test]
257 fn variants_are_clonable_and_comparable() {
258 let a = GlideError::Request("same".into());
259 let b = a.clone();
260 assert_eq!(a, b);
261 assert_ne!(a, GlideError::Request("different".into()));
262 }
263}