1use crate::errors::{Error, ErrorKind};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6pub enum BackoffPolicy {
7 None,
9 Linear { base_ms: u64, max_ms: u64 },
11 Exponential { base_ms: u64, max_ms: u64 },
13}
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
17pub enum DlqPolicy {
18 Never,
20 OnExhausted,
22 Always,
24}
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28pub enum AlertLevel {
29 Info,
31 Warn,
33 Error,
35 Critical,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct Policy {
42 pub retryable: bool,
44 pub backoff: BackoffPolicy,
46 pub dlq: DlqPolicy,
48 pub alert: AlertLevel,
50 pub max_retries: u32,
52 pub backoff_ms: u64,
54}
55
56impl Default for Policy {
57 fn default() -> Self {
58 Self {
59 retryable: true,
60 backoff: BackoffPolicy::Exponential {
61 base_ms: 500,
62 max_ms: 30_000,
63 },
64 dlq: DlqPolicy::OnExhausted,
65 alert: AlertLevel::Warn,
66 max_retries: 3,
67 backoff_ms: 500,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Default)]
73pub struct PolicyConfig {
74 pub overrides: Vec<PolicyOverride>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct PolicyOverride {
81 pub domain: Option<String>,
83 pub event_type: Option<String>,
85 pub phase: Option<String>,
87 pub kind: ErrorKind,
89 pub retryable: Option<bool>,
91 pub backoff: Option<BackoffPolicy>,
93 pub dlq: Option<DlqPolicy>,
95 pub alert: Option<AlertLevel>,
97 pub max_retries: Option<u32>,
99 pub backoff_ms: Option<u64>,
101}
102
103#[derive(Debug, Clone)]
105pub struct Decision {
106 pub policy: Policy,
108 pub reason: String,
110}
111
112#[derive(Debug, Clone)]
114pub struct PolicyResolver {
115 overrides: Vec<PolicyOverride>,
116}
117
118impl PolicyResolver {
119 pub fn new(config: Option<&PolicyConfig>) -> Self {
121 let overrides = config.map(|cfg| cfg.overrides.clone()).unwrap_or_default();
122 Self { overrides }
123 }
124
125 pub fn resolve_with_error(
127 &self,
128 domain: &str,
129 event_type: Option<&str>,
130 phase: Option<&str>,
131 err: &Error,
132 ) -> Decision {
133 self.resolve_with_kind(domain, event_type, phase, err.kind().clone())
134 }
135
136 pub fn resolve_with_kind(
156 &self,
157 domain: &str,
158 event_type: Option<&str>,
159 phase: Option<&str>,
160 kind: ErrorKind,
161 ) -> Decision {
162 let policy = default_policy(domain, event_type, phase, &kind);
163 let policy = apply_overrides(policy, &self.overrides, domain, event_type, phase, &kind);
164 Decision {
165 policy,
166 reason: format!(
167 "{}/{}/{}/{:?}",
168 normalize(domain),
169 normalize_opt(event_type).unwrap_or("-"),
170 normalize_opt(phase).unwrap_or("-"),
171 kind
172 ),
173 }
174 }
175}
176
177fn normalize(value: &str) -> &str {
178 value.trim()
179}
180
181fn normalize_opt(value: Option<&str>) -> Option<&str> {
182 value.map(|v| v.trim()).filter(|v| !v.is_empty())
183}
184
185fn default_policy(
186 domain: &str,
187 event_type: Option<&str>,
188 phase: Option<&str>,
189 kind: &ErrorKind,
190) -> Policy {
191 let domain = normalize(domain).to_ascii_lowercase();
192 let event_type = normalize_opt(event_type).unwrap_or("").to_ascii_lowercase();
193 let phase = normalize_opt(phase).unwrap_or("").to_ascii_lowercase();
194
195 let is_failed_or_retry = phase == "failed" || phase == "retry";
196
197 match (
198 domain.as_str(),
199 event_type.as_str(),
200 is_failed_or_retry,
201 kind,
202 ) {
203 ("engine", "task_model", true, ErrorKind::Task | ErrorKind::Module) => Policy {
204 backoff: BackoffPolicy::Exponential {
205 base_ms: 500,
206 max_ms: 30_000,
207 },
208 dlq: DlqPolicy::OnExhausted,
209 alert: AlertLevel::Warn,
210 max_retries: 3,
211 backoff_ms: 500,
212 retryable: true,
213 },
214 ("engine", "parser_task_model", true, ErrorKind::Parser | ErrorKind::ProcessorChain) => {
215 Policy {
216 backoff: BackoffPolicy::Exponential {
217 base_ms: 500,
218 max_ms: 30_000,
219 },
220 dlq: DlqPolicy::OnExhausted,
221 alert: AlertLevel::Warn,
222 max_retries: 3,
223 backoff_ms: 500,
224 retryable: true,
225 }
226 }
227 ("engine", "request_publish", true, ErrorKind::Queue) => Policy {
228 backoff: BackoffPolicy::Exponential {
229 base_ms: 500,
230 max_ms: 30_000,
231 },
232 dlq: DlqPolicy::OnExhausted,
233 alert: AlertLevel::Warn,
234 max_retries: 3,
235 backoff_ms: 500,
236 retryable: true,
237 },
238 ("engine", "request_middleware", _, ErrorKind::Request | ErrorKind::ProcessorChain) => {
239 Policy {
240 retryable: false,
241 backoff: BackoffPolicy::None,
242 dlq: DlqPolicy::Always,
243 alert: AlertLevel::Error,
244 max_retries: 0,
245 backoff_ms: 0,
246 }
247 }
248 (
249 "engine",
250 "download",
251 true,
252 ErrorKind::Download | ErrorKind::Proxy | ErrorKind::RateLimit,
253 ) => Policy {
254 backoff: BackoffPolicy::Exponential {
255 base_ms: 500,
256 max_ms: 60_000,
257 },
258 dlq: DlqPolicy::OnExhausted,
259 alert: AlertLevel::Warn,
260 max_retries: 5,
261 backoff_ms: 500,
262 retryable: true,
263 },
264 ("engine", "response_middleware", _, ErrorKind::Response | ErrorKind::ProcessorChain) => {
265 Policy {
266 retryable: false,
267 backoff: BackoffPolicy::None,
268 dlq: DlqPolicy::Always,
269 alert: AlertLevel::Error,
270 max_retries: 0,
271 backoff_ms: 0,
272 }
273 }
274 ("engine", "response_publish", true, ErrorKind::Queue) => Policy {
275 backoff: BackoffPolicy::Exponential {
276 base_ms: 500,
277 max_ms: 30_000,
278 },
279 dlq: DlqPolicy::OnExhausted,
280 alert: AlertLevel::Warn,
281 max_retries: 3,
282 backoff_ms: 500,
283 retryable: true,
284 },
285 ("engine", "module_generate", true, ErrorKind::Module | ErrorKind::Task) => Policy {
286 backoff: BackoffPolicy::Exponential {
287 base_ms: 500,
288 max_ms: 30_000,
289 },
290 dlq: DlqPolicy::OnExhausted,
291 alert: AlertLevel::Error,
292 max_retries: 3,
293 backoff_ms: 500,
294 retryable: true,
295 },
296 ("engine", "parser", true, ErrorKind::Parser) => Policy {
297 retryable: false,
298 backoff: BackoffPolicy::None,
299 dlq: DlqPolicy::Always,
300 alert: AlertLevel::Error,
301 max_retries: 0,
302 backoff_ms: 0,
303 },
304 ("engine", "middleware_before", _, ErrorKind::DataMiddleware) => Policy {
305 backoff: BackoffPolicy::Linear {
306 base_ms: 200,
307 max_ms: 10_000,
308 },
309 dlq: DlqPolicy::OnExhausted,
310 alert: AlertLevel::Warn,
311 max_retries: 3,
312 backoff_ms: 200,
313 retryable: true,
314 },
315 ("engine", "data_store", _, ErrorKind::DataStore | ErrorKind::Orm) => Policy {
316 backoff: BackoffPolicy::Linear {
317 base_ms: 200,
318 max_ms: 10_000,
319 },
320 dlq: DlqPolicy::OnExhausted,
321 alert: AlertLevel::Error,
322 max_retries: 3,
323 backoff_ms: 200,
324 retryable: true,
325 },
326 ("system", "system_error", true, _) => Policy {
327 retryable: false,
328 backoff: BackoffPolicy::None,
329 dlq: DlqPolicy::Never,
330 alert: AlertLevel::Error,
331 max_retries: 0,
332 backoff_ms: 0,
333 },
334 _ => Policy::default(),
335 }
336}
337
338fn apply_overrides(
339 mut policy: Policy,
340 overrides: &[PolicyOverride],
341 domain: &str,
342 event_type: Option<&str>,
343 phase: Option<&str>,
344 kind: &ErrorKind,
345) -> Policy {
346 let domain = normalize(domain).to_ascii_lowercase();
347 let event_type = normalize_opt(event_type).unwrap_or("").to_ascii_lowercase();
348 let phase = normalize_opt(phase).unwrap_or("").to_ascii_lowercase();
349
350 let mut best_score = -1_i32;
351 let mut best_override: Option<&PolicyOverride> = None;
352
353 for override_item in overrides {
354 if &override_item.kind != kind {
355 continue;
356 }
357
358 let domain_match = match &override_item.domain {
359 Some(value) => value.trim().eq_ignore_ascii_case(&domain),
360 None => true,
361 };
362 if !domain_match {
363 continue;
364 }
365
366 let event_match = match &override_item.event_type {
367 Some(value) => value.trim().eq_ignore_ascii_case(&event_type),
368 None => true,
369 };
370 if !event_match {
371 continue;
372 }
373
374 let phase_match = match &override_item.phase {
375 Some(value) => value.trim().eq_ignore_ascii_case(&phase),
376 None => true,
377 };
378 if !phase_match {
379 continue;
380 }
381
382 let score = (override_item.domain.is_some() as i32)
383 + (override_item.event_type.is_some() as i32)
384 + (override_item.phase.is_some() as i32)
385 + 1;
386
387 if score > best_score || (score == best_score && best_override.is_some()) {
388 best_score = score;
389 best_override = Some(override_item);
390 }
391 }
392
393 if let Some(override_item) = best_override {
394 if let Some(value) = override_item.retryable {
395 policy.retryable = value;
396 }
397 if let Some(value) = override_item.backoff {
398 policy.backoff = value;
399 }
400 if let Some(value) = override_item.dlq {
401 policy.dlq = value;
402 }
403 if let Some(value) = override_item.alert {
404 policy.alert = value;
405 }
406 if let Some(value) = override_item.max_retries {
407 policy.max_retries = value;
408 }
409 if let Some(value) = override_item.backoff_ms {
410 policy.backoff_ms = value;
411 }
412 }
413
414 policy
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn default_policy_parser_failed_is_not_retryable() {
423 let resolver = PolicyResolver::new(None);
424 let decision =
425 resolver.resolve_with_kind("engine", Some("parser"), Some("failed"), ErrorKind::Parser);
426 assert!(!decision.policy.retryable);
427 assert_eq!(decision.policy.dlq, DlqPolicy::Always);
428 }
429
430 #[test]
431 fn overrides_take_precedence() {
432 let cfg = PolicyConfig {
433 overrides: vec![PolicyOverride {
434 domain: Some("engine".to_string()),
435 event_type: Some("download".to_string()),
436 phase: Some("failed".to_string()),
437 kind: ErrorKind::Download,
438 retryable: Some(false),
439 backoff: Some(BackoffPolicy::None),
440 dlq: Some(DlqPolicy::Always),
441 alert: Some(AlertLevel::Error),
442 max_retries: Some(0),
443 backoff_ms: Some(0),
444 }],
445 };
446
447 let resolver = PolicyResolver::new(Some(&cfg));
448 let decision = resolver.resolve_with_kind(
449 "engine",
450 Some("download"),
451 Some("failed"),
452 ErrorKind::Download,
453 );
454
455 assert!(!decision.policy.retryable);
456 assert_eq!(decision.policy.dlq, DlqPolicy::Always);
457 assert_eq!(decision.policy.max_retries, 0);
458 }
459}