1use std::{borrow::Cow, error::Error, fmt, sync::Arc};
2
3use crate::fail::Level;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum Lane {
8 Rejected,
9 Denied,
10 Transient,
11 Fatal,
12}
13
14impl Lane {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Lane::Rejected => "rejected",
18 Lane::Denied => "denied",
19 Lane::Transient => "transient",
20 Lane::Fatal => "fatal",
21 }
22 }
23}
24
25impl Lane {
26 pub fn level(self) -> Level {
29 match self {
30 Lane::Rejected => Level::Warn,
31 Lane::Denied => Level::Warn,
32 Lane::Transient => Level::Info,
33 Lane::Fatal => Level::Error,
34 }
35 }
36}
37
38impl fmt::Display for Lane {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.write_str(self.as_str())
41 }
42}
43
44#[non_exhaustive]
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum TransientKind {
48 OptimisticConflict,
49 SerializationFailure,
50 Deadlock,
51 PoolTimeout,
52 ConnectionLost,
53 UpstreamUnavailable,
54 Congestion,
55 Other,
56}
57
58impl TransientKind {
59 pub fn as_str(self) -> &'static str {
60 match self {
61 TransientKind::OptimisticConflict => "optimistic_conflict",
62 TransientKind::SerializationFailure => "serialization_failure",
63 TransientKind::Deadlock => "deadlock",
64 TransientKind::PoolTimeout => "pool_timeout",
65 TransientKind::ConnectionLost => "connection_lost",
66 TransientKind::UpstreamUnavailable => "upstream_unavailable",
67 TransientKind::Congestion => "congestion",
68 TransientKind::Other => "other",
69 }
70 }
71
72 pub fn is_congestion(self) -> bool {
78 matches!(self, TransientKind::PoolTimeout | TransientKind::Congestion)
79 }
80
81 pub fn is_contention(self) -> bool {
95 matches!(
96 self,
97 TransientKind::Deadlock | TransientKind::SerializationFailure
98 )
99 }
100
101 pub fn from_sqlstate(code: &str) -> Option<Self> {
107 match code {
108 "40001" => Some(TransientKind::SerializationFailure),
109 "40P01" => Some(TransientKind::Deadlock),
110 "57P01" | "57P02" | "57P03" | "08000" | "08003" | "08006" | "08001" | "08004" => {
111 Some(TransientKind::ConnectionLost)
112 }
113 _ => None,
114 }
115 }
116}
117
118impl fmt::Display for TransientKind {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.write_str(self.as_str())
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct Transient {
127 pub kind: TransientKind,
128 pub context: Option<Cow<'static, str>>,
130 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
131}
132
133impl Transient {
134 pub fn new(kind: TransientKind) -> Self {
135 Self {
136 kind,
137 context: None,
138 source: None,
139 }
140 }
141
142 pub fn from_error(kind: TransientKind, e: impl Error + Send + Sync + 'static) -> Self {
145 Self::new(kind).with_source(e)
146 }
147
148 pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
149 self.source = Some(Arc::new(e));
150 self
151 }
152
153 pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
154 self.context = Some(c.into());
155 self
156 }
157
158 pub fn source_arc(&self) -> Option<&Arc<dyn Error + Send + Sync>> {
159 self.source.as_ref()
160 }
161
162 pub fn is_congestion(&self) -> bool {
163 self.kind.is_congestion()
164 }
165
166 pub fn is_contention(&self) -> bool {
167 self.kind.is_contention()
168 }
169}
170
171impl fmt::Display for Transient {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(f, "transient({})", self.kind)?;
174 if let Some(ctx) = &self.context {
175 write!(f, ": {ctx}")?;
176 }
177 Ok(())
178 }
179}
180
181impl Error for Transient {
182 fn source(&self) -> Option<&(dyn Error + 'static)> {
183 self.source
184 .as_ref()
185 .map(|s| s.as_ref() as &(dyn Error + 'static))
186 }
187}
188
189#[non_exhaustive]
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum FatalKind {
193 Invariant,
194 Config,
195 CorruptState,
196 Dependency,
197 Panic,
198 Exhausted,
199 Denied,
205}
206
207impl FatalKind {
208 pub fn as_str(self) -> &'static str {
209 match self {
210 FatalKind::Invariant => "invariant",
211 FatalKind::Config => "config",
212 FatalKind::CorruptState => "corrupt_state",
213 FatalKind::Dependency => "dependency",
214 FatalKind::Panic => "panic",
215 FatalKind::Exhausted => "exhausted",
216 FatalKind::Denied => "denied",
217 }
218 }
219}
220
221impl fmt::Display for FatalKind {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 f.write_str(self.as_str())
224 }
225}
226
227#[derive(Debug, Clone)]
237pub struct Fatal {
238 pub kind: FatalKind,
239 pub context: Option<Cow<'static, str>>,
240 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
241 opaque_source: bool,
250}
251
252impl Fatal {
253 pub fn new(kind: FatalKind) -> Self {
254 Self {
255 kind,
256 context: None,
257 source: None,
258 opaque_source: false,
259 }
260 }
261
262 pub fn from_error(kind: FatalKind, e: impl Error + Send + Sync + 'static) -> Self {
265 Self::new(kind).with_source(e)
266 }
267
268 pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
271 self.source = Some(Arc::new(e));
272 self
273 }
274
275 pub fn from_boxed(kind: FatalKind, e: Box<dyn Error + Send + Sync>) -> Self {
279 Self {
280 kind,
281 context: None,
282 source: Some(Arc::from(e)),
283 opaque_source: false,
284 }
285 }
286
287 pub fn from_dyn(kind: FatalKind, e: &(dyn Error + 'static)) -> Self {
292 Self::new(kind).with_context(crate::dynamic::message_chain(e))
293 }
294
295 pub fn invariant(msg: impl Into<Cow<'static, str>>) -> Self {
296 Self {
297 kind: FatalKind::Invariant,
298 context: Some(msg.into()),
299 source: None,
300 opaque_source: false,
301 }
302 }
303
304 pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
305 self.context = Some(c.into());
306 self
307 }
308
309 pub(crate) fn with_opaque_source(mut self) -> Self {
315 self.opaque_source = true;
316 self
317 }
318
319 pub(crate) fn has_opaque_source(&self) -> bool {
320 self.opaque_source
321 }
322}
323
324impl fmt::Display for Fatal {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "fatal({})", self.kind)?;
327 if let Some(ctx) = &self.context {
328 write!(f, ": {ctx}")?;
329 }
330 Ok(())
331 }
332}
333
334impl Error for Fatal {
335 fn source(&self) -> Option<&(dyn Error + 'static)> {
336 self.source
337 .as_ref()
338 .map(|s| s.as_ref() as &(dyn Error + 'static))
339 }
340}
341
342#[derive(Debug, Clone, Default)]
344pub struct Denied {
345 pub object: Option<Cow<'static, str>>,
346 pub action: Option<Cow<'static, str>>,
347 context: Option<Cow<'static, str>>,
348 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
349}
350
351impl Denied {
352 pub fn new() -> Self {
353 Self::default()
354 }
355
356 pub fn from_error(e: impl Error + Send + Sync + 'static) -> Self {
357 Self::new().with_source(e)
358 }
359
360 pub fn with_object(mut self, object: impl Into<Cow<'static, str>>) -> Self {
361 self.object = Some(object.into());
362 self
363 }
364
365 pub fn with_action(mut self, action: impl Into<Cow<'static, str>>) -> Self {
366 self.action = Some(action.into());
367 self
368 }
369
370 pub fn with_source(mut self, e: impl Error + Send + Sync + 'static) -> Self {
371 self.source = Some(Arc::new(e));
372 self
373 }
374
375 pub fn with_context(mut self, c: impl Into<Cow<'static, str>>) -> Self {
376 self.context = Some(c.into());
377 self
378 }
379
380 pub fn context(&self) -> Option<&str> {
381 self.context.as_deref()
382 }
383
384 pub fn source_arc(&self) -> Option<&Arc<dyn Error + Send + Sync>> {
385 self.source.as_ref()
386 }
387
388 pub(crate) fn into_fatal(mut self) -> Fatal {
389 let context = self.context.take();
390 let fatal = Fatal::from_error(FatalKind::Denied, self);
391 match context {
392 Some(c) => fatal.with_context(c),
393 None => fatal,
394 }
395 }
396}
397
398impl fmt::Display for Denied {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 f.write_str("denied")?;
401 if self.action.is_some() || self.object.is_some() {
402 f.write_str(": ")?;
403 if let Some(action) = &self.action {
404 write!(f, "{action} ")?;
405 }
406 if let Some(object) = &self.object {
407 write!(f, "on {object}")?;
408 }
409 }
410 Ok(())
411 }
412}
413
414impl Error for Denied {
415 fn source(&self) -> Option<&(dyn Error + 'static)> {
416 self.source
417 .as_ref()
418 .map(|s| s.as_ref() as &(dyn Error + 'static))
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct Exhausted {
425 pub attempts: u32,
426 pub last: Transient,
427}
428
429impl fmt::Display for Exhausted {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 write!(
432 f,
433 "exhausted after {} attempts: {}",
434 self.attempts, self.last
435 )
436 }
437}
438
439impl Error for Exhausted {
440 fn source(&self) -> Option<&(dyn Error + 'static)> {
441 Some(&self.last)
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
456 fn from_sqlstate_is_reachable_without_the_sqlx_feature() {
457 assert_eq!(
458 TransientKind::from_sqlstate("40P01"),
459 Some(TransientKind::Deadlock)
460 );
461 assert_eq!(TransientKind::from_sqlstate("not-a-code"), None);
462 }
463
464 #[test]
465 fn contention_is_the_server_confirmed_abort_subset_of_transient() {
466 assert!(TransientKind::Deadlock.is_contention());
467 assert!(TransientKind::SerializationFailure.is_contention());
468 assert!(!TransientKind::OptimisticConflict.is_contention());
469 assert!(!TransientKind::ConnectionLost.is_contention());
470 assert!(!TransientKind::PoolTimeout.is_contention());
471 assert!(!TransientKind::Congestion.is_contention());
472 assert!(!TransientKind::Other.is_contention());
473 assert!(
475 TransientKind::from_sqlstate("40P01")
476 .unwrap()
477 .is_contention()
478 );
479 assert!(
480 TransientKind::from_sqlstate("40001")
481 .unwrap()
482 .is_contention()
483 );
484 assert!(
485 !TransientKind::from_sqlstate("08006")
486 .unwrap()
487 .is_contention()
488 );
489 assert!(
490 !TransientKind::from_sqlstate("57P01")
491 .unwrap()
492 .is_contention()
493 );
494 }
495}