1use std::collections::HashMap;
8use std::sync::Arc;
9
10use regex::Regex;
11
12use crate::detail::ErrorDetail;
13use crate::handler::ErrorHandler;
14use crate::handlers::builtin_handler;
15
16struct HandlerMapping {
18 patterns: Vec<Regex>,
19 handlers: Vec<Arc<dyn ErrorHandler>>,
20}
21
22pub struct ErrorRouter {
43 mappings: Vec<HandlerMapping>,
44 cache: std::sync::Mutex<HashMap<String, Vec<Arc<dyn ErrorHandler>>>>,
46 retry_budget: Option<u32>,
54}
55
56pub const DEFAULT_RETRY_VERB_BUDGET: u32 = 3;
60
61fn parse_retry_verb(token: &str) -> Result<(&str, Option<u32>), String> {
66 if token == "retry" {
67 return Ok(("retry", Some(DEFAULT_RETRY_VERB_BUDGET)));
68 }
69 if let Some(arg) = token
70 .strip_prefix("retry(")
71 .and_then(|r| r.strip_suffix(')'))
72 {
73 let n: u32 = arg
74 .trim()
75 .parse()
76 .map_err(|_| format!("invalid retry budget in '{token}': expected retry(N)"))?;
77 return Ok(("retry", Some(n)));
78 }
79 Ok((token, None))
80}
81
82impl ErrorRouter {
83 pub fn parse(spec: &str) -> Result<Self, String> {
85 let mut mappings = Vec::new();
86 let mut retry_budget: Option<u32> = None;
87
88 for rule in spec.split(';') {
89 let rule = rule.trim();
90 if rule.is_empty() {
91 continue;
92 }
93
94 let (pattern_str, handler_str) = if let Some(colon) = rule.find(':') {
95 (&rule[..colon], &rule[colon + 1..])
96 } else {
97 (".*", rule)
99 };
100
101 let patterns: Vec<Regex> = pattern_str
102 .split(',')
103 .map(|p| p.trim())
104 .filter(|p| !p.is_empty())
105 .map(|p| Regex::new(p).map_err(|e| format!("invalid error pattern '{p}': {e}")))
106 .collect::<Result<Vec<_>, _>>()?;
107
108 let handlers: Vec<Arc<dyn ErrorHandler>> = handler_str
109 .split(',')
110 .map(|h| h.trim())
111 .filter(|h| !h.is_empty())
112 .map(|h| {
113 let (name, budget) = parse_retry_verb(h)?;
117 if let Some(b) = budget {
118 retry_budget = Some(retry_budget.map_or(b, |cur| cur.max(b)));
119 }
120 builtin_handler(name)
121 .map(|bh| Arc::from(bh) as Arc<dyn ErrorHandler>)
122 .ok_or_else(|| format!("unknown error handler: '{h}'"))
123 })
124 .collect::<Result<Vec<_>, _>>()?;
125
126 if patterns.is_empty() || handlers.is_empty() {
127 continue;
128 }
129
130 mappings.push(HandlerMapping { patterns, handlers });
131 }
132
133 Ok(Self {
134 mappings,
135 cache: std::sync::Mutex::new(HashMap::new()),
136 retry_budget,
137 })
138 }
139
140 pub fn retry_verb_budget(&self) -> Option<u32> {
143 self.retry_budget
144 }
145
146 pub fn has_catch_all(&self) -> bool {
154 self.mappings
155 .iter()
156 .any(|m| m.patterns.iter().any(|p| p.as_str() == ".*"))
157 }
158
159 pub fn default_stop() -> Self {
161 Self::parse(".*:stop").unwrap()
162 }
163
164 pub fn default_warn_count() -> Self {
166 Self::parse(".*:warn,counter").unwrap()
167 }
168
169 pub fn handle_error(
172 &self,
173 error_name: &str,
174 error_msg: &str,
175 cycle: u64,
176 duration_nanos: u64,
177 ) -> ErrorDetail {
178 let handlers = self.lookup(error_name);
179 let mut detail = ErrorDetail::non_retryable(error_name);
180
181 for handler in &handlers {
182 detail = handler.handle(error_name, error_msg, cycle, duration_nanos, detail);
183 }
184
185 detail
186 }
187
188 fn lookup(&self, error_name: &str) -> Vec<Arc<dyn ErrorHandler>> {
189 {
191 let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
192 if let Some(handlers) = cache.get(error_name) {
193 return handlers.clone();
194 }
195 }
196
197 for mapping in &self.mappings {
199 for pattern in &mapping.patterns {
200 if pattern.is_match(error_name) {
201 let handlers = mapping.handlers.clone();
202 self.cache
203 .lock()
204 .unwrap_or_else(|e| e.into_inner())
205 .insert(error_name.to_string(), handlers.clone());
206 return handlers;
207 }
208 }
209 }
210
211 eprintln!(
214 "error: no handler matched error type '{error_name}' — stopping (add a handler pattern to configure)"
215 );
216 let stop_handler = crate::handlers::builtin_handler("stop").unwrap();
217 let handlers = vec![Arc::from(stop_handler) as Arc<dyn ErrorHandler>];
218 self.cache
219 .lock()
220 .unwrap_or_else(|e| e.into_inner())
221 .insert(error_name.to_string(), handlers.clone());
222 handlers
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn parse_simple() {
232 let router = ErrorRouter::parse(".*:warn").unwrap();
233 let detail = router.handle_error("AnyError", "msg", 0, 0);
234 assert!(!detail.is_retryable());
235 }
236
237 #[test]
238 fn parse_multiple_handlers() {
239 let router = ErrorRouter::parse(".*:retry,warn").unwrap();
240 let detail = router.handle_error("AnyError", "msg", 0, 0);
241 assert!(detail.is_retryable());
242 }
243
244 #[test]
245 fn parse_multiple_rules() {
246 let router = ErrorRouter::parse("Timeout.*:retry,warn;.*:ignore").unwrap();
247
248 let d1 = router.handle_error("TimeoutError", "timed out", 0, 0);
249 assert!(d1.is_retryable());
250
251 let d2 = router.handle_error("OtherError", "other", 0, 0);
252 assert!(!d2.is_retryable());
253 }
254
255 #[test]
256 fn first_matching_rule_wins() {
257 let router = ErrorRouter::parse("Timeout:retry;.*:ignore").unwrap();
258 let d = router.handle_error("Timeout", "msg", 0, 0);
259 assert!(d.is_retryable());
260 }
261
262 #[test]
263 fn cache_works() {
264 let router = ErrorRouter::parse(".*:warn").unwrap();
265 router.handle_error("Err1", "msg", 0, 0);
267 router.handle_error("Err1", "msg", 1, 0);
269 let cache = router.cache.lock().unwrap();
270 assert!(cache.contains_key("Err1"));
271 }
272
273 #[test]
274 fn no_pattern_defaults_to_catch_all() {
275 let router = ErrorRouter::parse("warn,counter").unwrap();
276 let detail = router.handle_error("AnyError", "msg", 0, 0);
277 assert!(!detail.is_retryable());
279 }
280
281 #[test]
282 fn stop_handler_in_chain() {
283 let router = ErrorRouter::parse(".*:warn,stop").unwrap();
284 let detail = router.handle_error("Fatal", "kaboom", 42, 0);
285 assert!(
286 detail.should_stop,
287 "stop handler in chain should set should_stop"
288 );
289 }
290
291 #[test]
292 fn unknown_handler_rejected() {
293 let result = ErrorRouter::parse(".*:bogus_handler");
294 assert!(result.is_err());
295 }
296
297 #[test]
298 fn empty_spec_stops_on_unmatched() {
299 let router = ErrorRouter::parse("").unwrap();
300 let detail = router.handle_error("Err", "msg", 0, 0);
302 assert!(detail.should_stop, "unmatched errors should stop execution");
303 }
304
305 #[test]
306 fn default_constructors() {
307 let _ = ErrorRouter::default_stop();
308 let r = ErrorRouter::default_warn_count();
309 let d = r.handle_error("test", "msg", 0, 0);
310 assert!(!d.is_retryable());
311 }
312
313 #[test]
316 fn bare_retry_verb_implies_default_budget() {
317 let r = ErrorRouter::parse("Timeout:retry,warn;.*:stop").unwrap();
318 assert_eq!(r.retry_verb_budget(), Some(DEFAULT_RETRY_VERB_BUDGET));
319 let r = ErrorRouter::parse(".*:warn,counter").unwrap();
320 assert_eq!(r.retry_verb_budget(), None);
321 }
322
323 #[test]
325 fn parenthesised_retry_budget_wins_max() {
326 let r = ErrorRouter::parse("Timeout:retry(5),warn;Overload:retry(9);.*:stop").unwrap();
327 assert_eq!(r.retry_verb_budget(), Some(9));
328 let d = r.handle_error("Timeout", "t", 0, 0);
330 assert!(d.is_retryable());
331 }
332
333 #[test]
336 fn malformed_retry_budget_rejected() {
337 match ErrorRouter::parse(".*:retry(lots)") {
338 Err(err) => assert!(err.contains("retry(N)"), "diagnostic: {err}"),
339 Ok(_) => panic!("retry(lots) must be a parse error"),
340 }
341 }
342}