1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::{BoxProcessor, CamelError, Exchange, PipelineOutcome, SyncBoxProcessor};
5
6pub const HEADER_REDELIVERED: &str = "CamelRedelivered";
8pub const HEADER_REDELIVERY_COUNTER: &str = "CamelRedeliveryCounter";
9pub const HEADER_REDELIVERY_MAX_COUNTER: &str = "CamelRedeliveryMaxCounter";
10
11#[derive(Debug, Clone)]
13pub struct RedeliveryPolicy {
14 pub max_attempts: u32,
15 pub initial_delay: Duration,
16 pub multiplier: f64,
17 pub max_delay: Duration,
18 pub jitter_factor: f64,
19}
20
21impl RedeliveryPolicy {
22 pub fn new(max_attempts: u32) -> Self {
27 Self {
28 max_attempts,
29 initial_delay: Duration::from_millis(100),
30 multiplier: 2.0,
31 max_delay: Duration::from_secs(10),
32 jitter_factor: 0.0,
33 }
34 }
35
36 pub fn with_initial_delay(mut self, d: Duration) -> Self {
38 self.initial_delay = d;
39 self
40 }
41
42 pub fn with_multiplier(mut self, m: f64) -> Self {
44 self.multiplier = m;
45 self
46 }
47
48 pub fn with_max_delay(mut self, d: Duration) -> Self {
50 self.max_delay = d;
51 self
52 }
53
54 pub fn with_jitter(mut self, j: f64) -> Self {
60 self.jitter_factor = j.clamp(0.0, 1.0);
61 self
62 }
63
64 pub fn delay_for(&self, attempt: u32) -> Duration {
66 let base_ms = self.initial_delay.as_millis() as f64 * self.multiplier.powi(attempt as i32);
67 let capped_ms = base_ms.min(self.max_delay.as_millis() as f64);
68
69 if self.jitter_factor > 0.0 {
70 let jitter = capped_ms * self.jitter_factor * (rand::random::<f64>() * 2.0 - 1.0);
71 Duration::from_millis((capped_ms + jitter).max(0.0) as u64)
72 } else {
73 Duration::from_millis(capped_ms as u64)
74 }
75 }
76}
77
78#[cfg_attr(feature = "schema", derive(schemars::JsonSchema, ts_rs::TS))]
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
92#[serde(rename_all = "lowercase")]
93#[non_exhaustive]
94pub enum ExceptionDisposition {
95 #[default]
97 Propagate,
98 Handled,
100 Continued,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub struct PolicyId(pub usize);
110
111#[derive(Debug)]
113#[non_exhaustive]
114pub enum RetryOutcome {
115 Recovered(Exchange),
117 Stopped(Exchange),
121 Exhausted {
123 exchange: Exchange,
124 error: CamelError,
125 policy: Option<PolicyId>,
126 },
127}
128
129pub trait RetryableStep: Send {
135 fn invoke<'a>(
136 &'a mut self,
137 exchange: Exchange,
138 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>;
139}
140
141impl RetryableStep for crate::BoxProcessor {
146 fn invoke<'a>(
147 &'a mut self,
148 exchange: Exchange,
149 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>> {
150 use tower::ServiceExt;
151 Box::pin(async move {
152 match self.ready().await {
153 Ok(ready_svc) => match tower::Service::call(ready_svc, exchange).await {
154 Ok(ex) => PipelineOutcome::Completed(ex),
155 Err(err) => PipelineOutcome::Failed(err),
156 },
157 Err(err) => PipelineOutcome::Failed(err),
158 }
159 })
160 }
161}
162
163#[non_exhaustive]
166pub enum StepDisposition {
167 Propagate(CamelError),
168 Handled(Exchange),
169 Continued(Exchange),
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174#[non_exhaustive]
175pub enum BoundaryKind {
176 Security,
177 CircuitBreaker,
178 Readiness,
179}
180
181pub struct ExceptionPolicy {
183 pub matches: Arc<dyn Fn(&CamelError) -> bool + Send + Sync>,
185 pub retry: Option<RedeliveryPolicy>,
187 pub handled_by: Option<String>,
189 pub on_steps: Option<SyncBoxProcessor>,
191 pub disposition: ExceptionDisposition,
193}
194
195impl ExceptionPolicy {
196 pub fn new(matches: impl Fn(&CamelError) -> bool + Send + Sync + 'static) -> Self {
198 Self {
199 matches: Arc::new(matches),
200 retry: None,
201 handled_by: None,
202 on_steps: None,
203 disposition: ExceptionDisposition::Propagate,
204 }
205 }
206}
207
208impl Clone for ExceptionPolicy {
209 fn clone(&self) -> Self {
210 Self {
211 matches: Arc::clone(&self.matches),
212 retry: self.retry.clone(),
213 handled_by: self.handled_by.clone(),
214 on_steps: self.on_steps.clone(),
215 disposition: self.disposition,
216 }
217 }
218}
219
220#[derive(Clone)]
222pub struct ErrorHandlerConfig {
223 pub dlc_uri: Option<String>,
225 pub policies: Vec<ExceptionPolicy>,
227 pub use_original_message: bool,
230}
231
232impl ErrorHandlerConfig {
233 pub fn log_only() -> Self {
235 Self {
236 dlc_uri: None,
237 policies: Vec::new(),
238 use_original_message: false,
239 }
240 }
241
242 pub fn dead_letter_channel(uri: impl Into<String>) -> Self {
244 Self {
245 dlc_uri: Some(uri.into()),
246 policies: Vec::new(),
247 use_original_message: false,
248 }
249 }
250
251 pub fn on_exception(
253 self,
254 matches: impl Fn(&CamelError) -> bool + Send + Sync + 'static,
255 ) -> ExceptionPolicyBuilder {
256 ExceptionPolicyBuilder {
257 config: self,
258 policy: ExceptionPolicy::new(matches),
259 }
260 }
261}
262
263pub struct ExceptionPolicyBuilder {
265 config: ErrorHandlerConfig,
266 policy: ExceptionPolicy,
267}
268
269impl ExceptionPolicyBuilder {
270 pub fn retry(mut self, max_attempts: u32) -> Self {
272 self.policy.retry = Some(RedeliveryPolicy::new(max_attempts));
273 self
274 }
275
276 pub fn with_backoff(mut self, initial: Duration, multiplier: f64, max: Duration) -> Self {
278 if let Some(ref mut p) = self.policy.retry {
279 p.initial_delay = initial;
280 p.multiplier = multiplier;
281 p.max_delay = max;
282 }
283 self
284 }
285
286 pub fn with_jitter(mut self, jitter_factor: f64) -> Self {
289 if let Some(ref mut p) = self.policy.retry {
290 p.jitter_factor = jitter_factor.clamp(0.0, 1.0);
291 }
292 self
293 }
294
295 pub fn handled_by(mut self, uri: impl Into<String>) -> Self {
297 self.policy.handled_by = Some(uri.into());
298 self
299 }
300
301 pub fn on_steps(mut self, pipeline: BoxProcessor) -> Self {
303 self.policy.on_steps = Some(SyncBoxProcessor::new(pipeline));
304 self
305 }
306
307 pub fn handled(mut self, handled: bool) -> Self {
309 self.policy.disposition = if handled {
310 ExceptionDisposition::Handled
311 } else {
312 ExceptionDisposition::Propagate
313 };
314 self
315 }
316
317 pub fn continued(mut self, continued: bool) -> Self {
319 self.policy.disposition = if continued {
320 ExceptionDisposition::Continued
321 } else {
322 ExceptionDisposition::Propagate
323 };
324 self
325 }
326
327 pub fn propagate(mut self) -> Self {
329 self.policy.disposition = ExceptionDisposition::Propagate;
330 self
331 }
332
333 pub fn build(mut self) -> ErrorHandlerConfig {
335 self.config.policies.push(self.policy);
336 self.config
337 }
338}
339
340#[cfg(test)]
341mod retryable_step_tests {
342 use super::*;
343 use crate::{BoxProcessor, CamelError, Exchange, Message, PipelineOutcome};
344 use std::future::Future;
345 use std::pin::Pin;
346 use std::sync::Arc;
347 use std::sync::atomic::{AtomicUsize, Ordering};
348
349 struct CountingProcessor {
350 call_count: Arc<AtomicUsize>,
351 succeed: bool,
352 }
353
354 impl tower::Service<Exchange> for CountingProcessor {
355 type Response = Exchange;
356 type Error = CamelError;
357 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
358
359 fn poll_ready(
360 &mut self,
361 _cx: &mut std::task::Context<'_>,
362 ) -> std::task::Poll<Result<(), Self::Error>> {
363 std::task::Poll::Ready(Ok(()))
364 }
365
366 fn call(&mut self, exchange: Exchange) -> Self::Future {
367 let count = self.call_count.clone();
368 let succeed = self.succeed;
369 Box::pin(async move {
370 count.fetch_add(1, Ordering::SeqCst);
371 if succeed {
372 Ok(exchange)
373 } else {
374 Err(CamelError::ProcessorError("fail".into()))
375 }
376 })
377 }
378 }
379
380 impl Clone for CountingProcessor {
381 fn clone(&self) -> Self {
382 Self {
383 call_count: self.call_count.clone(),
384 succeed: self.succeed,
385 }
386 }
387 }
388
389 #[tokio::test]
390 async fn boxprocessor_adapter_maps_ok_to_completed() {
391 let count = Arc::new(AtomicUsize::new(0));
392 let processor = CountingProcessor {
393 call_count: count.clone(),
394 succeed: true,
395 };
396 let bp: BoxProcessor = BoxProcessor::new(processor);
397 let mut retryable: Box<dyn RetryableStep> = Box::new(bp);
398 let ex = Exchange::new(Message::new("hello"));
399 let outcome = retryable.invoke(ex).await;
400 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
401 assert_eq!(count.load(Ordering::SeqCst), 1);
402 }
403
404 #[tokio::test]
405 async fn boxprocessor_adapter_maps_err_to_failed() {
406 let processor = CountingProcessor {
407 call_count: Arc::new(AtomicUsize::new(0)),
408 succeed: false,
409 };
410 let bp: BoxProcessor = BoxProcessor::new(processor);
411 let mut retryable: Box<dyn RetryableStep> = Box::new(bp);
412 let ex = Exchange::new(Message::new("hello"));
413 let outcome = retryable.invoke(ex).await;
414 assert!(matches!(outcome, PipelineOutcome::Failed(_)));
415 }
416
417 #[tokio::test]
418 async fn boxprocessor_readiness_error_propagates_to_failed() {
419 struct AlwaysNotReady;
420
421 impl tower::Service<Exchange> for AlwaysNotReady {
422 type Response = Exchange;
423 type Error = CamelError;
424 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
425
426 fn poll_ready(
427 &mut self,
428 _cx: &mut std::task::Context<'_>,
429 ) -> std::task::Poll<Result<(), Self::Error>> {
430 std::task::Poll::Ready(Err(CamelError::ProcessorError(
431 "readiness failed: consumer closed".into(),
432 )))
433 }
434
435 fn call(&mut self, _ex: Exchange) -> Self::Future {
436 Box::pin(async {
437 unreachable!("call() must not be reached when poll_ready errors")
438 })
439 }
440 }
441
442 impl Clone for AlwaysNotReady {
443 fn clone(&self) -> Self {
444 AlwaysNotReady
445 }
446 }
447
448 let bp: BoxProcessor = BoxProcessor::new(AlwaysNotReady);
449 let mut retryable: Box<dyn RetryableStep> = Box::new(bp);
450 let ex = Exchange::new(Message::new("hello"));
451 let outcome = retryable.invoke(ex).await;
452 match outcome {
453 PipelineOutcome::Failed(err) => {
454 let msg = err.to_string();
455 assert!(
456 msg.contains("readiness") || msg.contains("consumer"),
457 "readiness error message should be preserved, got: {msg}"
458 );
459 }
460 other => panic!(
461 "readiness failure must map to PipelineOutcome::Failed, got {:?}",
462 other
463 ),
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::CamelError;
472 use std::time::Duration;
473
474 #[test]
475 fn test_redelivery_policy_defaults() {
476 let p = RedeliveryPolicy::new(3);
477 assert_eq!(p.max_attempts, 3);
478 assert_eq!(p.initial_delay, Duration::from_millis(100));
479 assert_eq!(p.multiplier, 2.0);
480 assert_eq!(p.max_delay, Duration::from_secs(10));
481 assert_eq!(p.jitter_factor, 0.0);
482 }
483
484 #[test]
485 fn test_exception_policy_matches() {
486 let policy = ExceptionPolicy::new(|e| matches!(e, CamelError::ProcessorError(_)));
487 assert!((policy.matches)(&CamelError::ProcessorError("oops".into())));
488 assert!(!(policy.matches)(&CamelError::Io("io".into())));
489 }
490
491 #[test]
492 fn test_error_handler_config_log_only() {
493 let config = ErrorHandlerConfig::log_only();
494 assert!(config.dlc_uri.is_none());
495 assert!(config.policies.is_empty());
496 }
497
498 #[test]
499 fn test_error_handler_config_dlc() {
500 let config = ErrorHandlerConfig::dead_letter_channel("log:dlc");
501 assert_eq!(config.dlc_uri.as_deref(), Some("log:dlc"));
502 }
503
504 #[test]
505 fn test_error_handler_config_with_policy() {
506 let config = ErrorHandlerConfig::dead_letter_channel("log:dlc")
507 .on_exception(|e| matches!(e, CamelError::Io(_)))
508 .retry(2)
509 .handled_by("log:io-errors")
510 .build();
511 assert_eq!(config.policies.len(), 1);
512 let p = &config.policies[0];
513 assert!(p.retry.is_some());
514 assert_eq!(p.retry.as_ref().unwrap().max_attempts, 2);
515 assert_eq!(p.handled_by.as_deref(), Some("log:io-errors"));
516 }
517
518 #[test]
519 fn test_jitter_applies_randomness() {
520 let policy = RedeliveryPolicy::new(3)
521 .with_initial_delay(Duration::from_millis(100))
522 .with_jitter(0.5);
523
524 let mut delays = std::collections::HashSet::new();
525 for _ in 0..10 {
526 delays.insert(policy.delay_for(0));
527 }
528
529 assert!(delays.len() > 1, "jitter should produce varying delays");
530 }
531
532 #[test]
533 fn test_jitter_stays_within_bounds() {
534 let policy = RedeliveryPolicy::new(3)
535 .with_initial_delay(Duration::from_millis(100))
536 .with_jitter(0.5);
537
538 for _ in 0..100 {
539 let delay = policy.delay_for(0);
540 assert!(
541 delay >= Duration::from_millis(50),
542 "delay too low: {:?}",
543 delay
544 );
545 assert!(
546 delay <= Duration::from_millis(150),
547 "delay too high: {:?}",
548 delay
549 );
550 }
551 }
552
553 #[test]
554 fn test_max_attempts_zero_means_no_retries() {
555 let policy = RedeliveryPolicy::new(0);
556 assert_eq!(policy.max_attempts, 0);
557 }
558
559 #[test]
560 fn test_jitter_zero_produces_exact_delay() {
561 let policy = RedeliveryPolicy::new(3)
562 .with_initial_delay(Duration::from_millis(100))
563 .with_jitter(0.0);
564
565 for _ in 0..10 {
566 let delay = policy.delay_for(0);
567 assert_eq!(delay, Duration::from_millis(100));
568 }
569 }
570
571 #[test]
572 fn test_jitter_one_produces_wide_range() {
573 let policy = RedeliveryPolicy::new(3)
574 .with_initial_delay(Duration::from_millis(100))
575 .with_jitter(1.0);
576
577 for _ in 0..100 {
578 let delay = policy.delay_for(0);
579 assert!(
580 delay >= Duration::from_millis(0),
581 "delay should be >= 0, got {:?}",
582 delay
583 );
584 assert!(
585 delay <= Duration::from_millis(200),
586 "delay should be <= 200ms, got {:?}",
587 delay
588 );
589 }
590 }
591
592 #[test]
593 fn test_redelivery_policy_builder_methods_apply_values() {
594 let p = RedeliveryPolicy::new(5)
595 .with_initial_delay(Duration::from_millis(250))
596 .with_multiplier(3.0)
597 .with_max_delay(Duration::from_secs(2))
598 .with_jitter(2.0);
599
600 assert_eq!(p.initial_delay, Duration::from_millis(250));
601 assert_eq!(p.multiplier, 3.0);
602 assert_eq!(p.max_delay, Duration::from_secs(2));
603 assert_eq!(p.jitter_factor, 1.0);
604 }
605
606 #[test]
607 fn test_with_jitter_clamps_low_bound() {
608 let p = RedeliveryPolicy::new(1).with_jitter(-0.2);
609 assert_eq!(p.jitter_factor, 0.0);
610 }
611
612 #[test]
613 fn test_delay_for_exponential_growth_and_cap() {
614 let p = RedeliveryPolicy::new(3)
615 .with_initial_delay(Duration::from_millis(100))
616 .with_multiplier(2.0)
617 .with_max_delay(Duration::from_millis(250));
618
619 assert_eq!(p.delay_for(0), Duration::from_millis(100));
620 assert_eq!(p.delay_for(1), Duration::from_millis(200));
621 assert_eq!(p.delay_for(2), Duration::from_millis(250));
622 assert_eq!(p.delay_for(20), Duration::from_millis(250));
623 }
624
625 #[test]
626 fn test_exception_policy_builder_backoff_and_jitter() {
627 let config = ErrorHandlerConfig::log_only()
628 .on_exception(|e| matches!(e, CamelError::Io(_)))
629 .retry(4)
630 .with_backoff(Duration::from_millis(10), 1.5, Duration::from_millis(40))
631 .with_jitter(1.5)
632 .build();
633
634 let retry = config.policies[0].retry.as_ref().unwrap();
635 assert_eq!(retry.max_attempts, 4);
636 assert_eq!(retry.initial_delay, Duration::from_millis(10));
637 assert_eq!(retry.multiplier, 1.5);
638 assert_eq!(retry.max_delay, Duration::from_millis(40));
639 assert_eq!(retry.jitter_factor, 1.0);
640 }
641
642 #[test]
643 fn test_exception_policy_builder_no_retry_ignores_backoff_and_jitter() {
644 let config = ErrorHandlerConfig::log_only()
645 .on_exception(|_| true)
646 .with_backoff(Duration::from_secs(1), 9.0, Duration::from_secs(2))
647 .with_jitter(0.8)
648 .build();
649
650 assert!(config.policies[0].retry.is_none());
651 }
652
653 #[test]
654 fn test_exception_policy_clone_preserves_behavior_and_fields() {
655 let policy = ExceptionPolicy::new(|e| matches!(e, CamelError::RouteError(_)));
656 let mut configured = policy;
657 configured.retry = Some(RedeliveryPolicy::new(2));
658 configured.handled_by = Some("log:route-errors".to_string());
659
660 let cloned = configured.clone();
661 assert!((cloned.matches)(&CamelError::RouteError("x".into())));
662 assert_eq!(cloned.retry.as_ref().unwrap().max_attempts, 2);
663 assert_eq!(cloned.handled_by.as_deref(), Some("log:route-errors"));
664 }
665
666 #[test]
667 fn test_delay_for_respects_max_delay_with_jitter() {
668 let policy = RedeliveryPolicy::new(5)
669 .with_initial_delay(Duration::from_millis(200))
670 .with_multiplier(10.0)
671 .with_max_delay(Duration::from_millis(500))
672 .with_jitter(0.2);
673
674 for _ in 0..30 {
675 let delay = policy.delay_for(4);
676 assert!(delay <= Duration::from_millis(600));
677 assert!(delay >= Duration::from_millis(400));
678 }
679 }
680
681 #[test]
682 fn test_exception_policy_builder_keeps_dlc_and_policy_order() {
683 let config = ErrorHandlerConfig::dead_letter_channel("log:dlc")
684 .on_exception(|e| matches!(e, CamelError::Io(_)))
685 .retry(1)
686 .build()
687 .on_exception(|e| matches!(e, CamelError::RouteError(_)))
688 .handled_by("log:routes")
689 .build();
690
691 assert_eq!(config.dlc_uri.as_deref(), Some("log:dlc"));
692 assert_eq!(config.policies.len(), 2);
693 assert!((config.policies[0].matches)(&CamelError::Io("x".into())));
694 assert!((config.policies[1].matches)(&CamelError::RouteError(
695 "x".into()
696 )));
697 }
698
699 #[test]
700 fn test_backoff_without_retry_does_not_create_retry_config() {
701 let config = ErrorHandlerConfig::log_only()
702 .on_exception(|_| true)
703 .with_backoff(Duration::from_millis(1), 3.0, Duration::from_millis(9))
704 .build();
705
706 assert!(config.policies[0].retry.is_none());
707 }
708
709 #[test]
710 fn test_exception_disposition_default_is_propagate() {
711 assert_eq!(
712 ExceptionDisposition::default(),
713 ExceptionDisposition::Propagate
714 );
715 }
716
717 #[test]
718 fn test_exception_policy_new_has_propagate_disposition() {
719 let p = ExceptionPolicy::new(|_| true);
720 assert_eq!(p.disposition, ExceptionDisposition::Propagate);
721 }
722
723 #[test]
724 fn test_policy_id_equality() {
725 assert_eq!(PolicyId(0), PolicyId(0));
726 assert_ne!(PolicyId(0), PolicyId(1));
727 }
728
729 #[test]
730 fn test_builder_continued_sets_disposition() {
731 let cfg = ErrorHandlerConfig::log_only()
732 .on_exception(|_| true)
733 .continued(true)
734 .build();
735 assert_eq!(cfg.policies[0].disposition, ExceptionDisposition::Continued);
736 }
737
738 #[test]
739 fn test_builder_propagate_sets_disposition() {
740 let cfg = ErrorHandlerConfig::log_only()
741 .on_exception(|_| true)
742 .propagate()
743 .build();
744 assert_eq!(cfg.policies[0].disposition, ExceptionDisposition::Propagate);
745 }
746
747 #[test]
748 fn test_builder_handled_true_still_works() {
749 let cfg = ErrorHandlerConfig::log_only()
750 .on_exception(|_| true)
751 .handled(true)
752 .build();
753 assert_eq!(cfg.policies[0].disposition, ExceptionDisposition::Handled);
754 }
755}