execution_policy/
error.rs1use std::time::Duration;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum BreakerState {
8 Disabled,
9 Closed,
10 Open,
11 HalfOpen,
12}
13
14#[derive(Debug, Clone)]
16pub struct ErrorContext {
17 pub attempts: u32,
18 pub elapsed: Duration,
19 pub last_delay: Option<Duration>,
20 pub breaker_state: BreakerState,
21}
22
23#[non_exhaustive]
25#[derive(Debug)]
26pub enum ExecutionError<E> {
27 Operation {
28 source: E,
29 context: Box<ErrorContext>,
30 },
31 AttemptTimeout {
32 context: Box<ErrorContext>,
33 },
34 TotalTimeout {
35 context: Box<ErrorContext>,
36 },
37 CircuitOpen {
38 context: Box<ErrorContext>,
39 },
40 ConcurrencyRejected {
41 context: Box<ErrorContext>,
42 },
43 RetryBudgetExhausted {
44 context: Box<ErrorContext>,
45 },
46}
47
48impl<E> ExecutionError<E> {
49 pub fn context(&self) -> &ErrorContext {
51 match self {
52 Self::Operation { context, .. }
53 | Self::AttemptTimeout { context }
54 | Self::TotalTimeout { context }
55 | Self::CircuitOpen { context }
56 | Self::ConcurrencyRejected { context }
57 | Self::RetryBudgetExhausted { context } => context,
58 }
59 }
60
61 pub fn into_inner(self) -> Option<E> {
63 match self {
64 Self::Operation { source, .. } => Some(source),
65 _ => None,
66 }
67 }
68
69 pub fn is_timeout(&self) -> bool {
70 matches!(
71 self,
72 Self::AttemptTimeout { .. } | Self::TotalTimeout { .. }
73 )
74 }
75 pub fn is_circuit_open(&self) -> bool {
76 matches!(self, Self::CircuitOpen { .. })
77 }
78 pub fn is_rejected(&self) -> bool {
79 matches!(self, Self::ConcurrencyRejected { .. })
80 }
81 pub fn is_exhausted(&self) -> bool {
82 matches!(
83 self,
84 Self::Operation { .. } | Self::RetryBudgetExhausted { .. }
85 )
86 }
87}
88
89impl<E: std::fmt::Display> std::fmt::Display for ExecutionError<E> {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 let ctx = self.context();
92 match self {
93 Self::Operation { source, .. } => write!(
94 f,
95 "operation failed after {} attempt(s) in {:?}: {source}",
96 ctx.attempts, ctx.elapsed
97 ),
98 Self::AttemptTimeout { .. } => {
99 write!(f, "attempt timed out (attempt {})", ctx.attempts)
100 }
101 Self::TotalTimeout { .. } => {
102 write!(
103 f,
104 "total timeout after {:?} ({} attempts)",
105 ctx.elapsed, ctx.attempts
106 )
107 }
108 Self::CircuitOpen { .. } => write!(f, "circuit open"),
109 Self::ConcurrencyRejected { .. } => {
110 write!(f, "concurrency limit rejected the call")
111 }
112 Self::RetryBudgetExhausted { .. } => write!(f, "retry budget exhausted"),
113 }
114 }
115}
116
117impl<E: std::error::Error + 'static> std::error::Error for ExecutionError<E> {
118 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119 match self {
120 Self::Operation { source, .. } => Some(source),
121 _ => None,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn ctx() -> Box<ErrorContext> {
131 Box::new(ErrorContext {
132 attempts: 3,
133 elapsed: Duration::from_millis(120),
134 last_delay: Some(Duration::from_millis(50)),
135 breaker_state: BreakerState::Disabled,
136 })
137 }
138
139 #[test]
140 fn predicates_and_context() {
141 let e: ExecutionError<std::io::Error> = ExecutionError::TotalTimeout { context: ctx() };
142 assert!(e.is_timeout());
143 assert!(!e.is_circuit_open());
144 assert_eq!(e.context().attempts, 3);
145 }
146
147 #[test]
148 fn into_inner_recovers_operation_error() {
149 let src = std::io::Error::other("boom");
150 let e = ExecutionError::Operation {
151 source: src,
152 context: ctx(),
153 };
154 assert_eq!(e.into_inner().unwrap().to_string(), "boom");
155 }
156
157 #[test]
158 fn error_source_chains() {
159 use std::error::Error;
160 let src = std::io::Error::other("io fail");
161 let e = ExecutionError::Operation {
162 source: src,
163 context: ctx(),
164 };
165 assert!(e.source().is_some());
166 }
167}