1use std::future::Future;
2use std::pin::Pin;
3use std::sync::{Arc, Mutex};
4use std::task::{Context, Poll};
5use std::time::Instant;
6
7use tower::{Layer, Service};
8
9use camel_api::metrics::MetricsCollector;
10use camel_api::{BoxProcessor, CamelError, CircuitBreakerConfig, Exchange};
11
12enum CircuitState {
15 Closed {
16 consecutive_failures: u32,
17 },
18 Open {
19 opened_at: Instant,
20 },
21 HalfOpen {
26 probe_admitted: bool,
27 },
28}
29
30#[derive(Clone)]
34pub struct CircuitBreakerLayer {
35 config: CircuitBreakerConfig,
36 state: Arc<Mutex<CircuitState>>,
37 route_id: Arc<str>,
38 metrics: Option<Arc<dyn MetricsCollector>>,
39}
40
41impl CircuitBreakerLayer {
42 pub fn new(
47 config: CircuitBreakerConfig,
48 route_id: Arc<str>,
49 metrics: Option<Arc<dyn MetricsCollector>>,
50 ) -> Self {
51 Self {
52 config,
53 state: Arc::new(Mutex::new(CircuitState::Closed {
54 consecutive_failures: 0,
55 })),
56 route_id,
57 metrics,
58 }
59 }
60}
61
62impl<S> Layer<S> for CircuitBreakerLayer {
63 type Service = CircuitBreakerService<S>;
64
65 fn layer(&self, inner: S) -> Self::Service {
66 CircuitBreakerService {
67 inner,
68 config: self.config.clone(),
69 state: Arc::clone(&self.state),
70 route_id: Arc::clone(&self.route_id),
71 metrics: self.metrics.clone(),
72 }
73 }
74}
75
76pub struct CircuitBreakerService<S> {
80 inner: S,
81 config: CircuitBreakerConfig,
82 state: Arc<Mutex<CircuitState>>,
83 route_id: Arc<str>,
84 metrics: Option<Arc<dyn MetricsCollector>>,
85}
86
87impl<S: Clone> Clone for CircuitBreakerService<S> {
88 fn clone(&self) -> Self {
89 Self {
90 inner: self.inner.clone(),
91 config: self.config.clone(),
92 state: Arc::clone(&self.state),
93 route_id: Arc::clone(&self.route_id),
94 metrics: self.metrics.clone(),
95 }
96 }
97}
98
99impl<S> CircuitBreakerService<S> {
100 fn record_rejection(&self) {
102 if let Some(ref metrics) = self.metrics {
103 metrics.increment_circuit_breaker_rejection(&self.route_id);
104 }
105 }
106}
107
108impl<S> Service<Exchange> for CircuitBreakerService<S>
109where
110 S: Service<Exchange, Response = Exchange, Error = CamelError> + Clone + Send + 'static,
111 S::Future: Send,
112{
113 type Response = Exchange;
114 type Error = CamelError;
115 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
116
117 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
118 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
119 match *state {
120 CircuitState::Closed { .. } => {
121 drop(state);
122 self.inner.poll_ready(cx)
123 }
124 CircuitState::Open { opened_at } => {
125 if opened_at.elapsed() >= self.config.open_duration {
126 tracing::info!("Circuit breaker transitioning from Open to HalfOpen");
127 *state = CircuitState::HalfOpen {
128 probe_admitted: true,
129 };
130 drop(state);
131 match self.inner.poll_ready(cx) {
135 Poll::Ready(result) => Poll::Ready(result),
136 Poll::Pending => {
137 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
138 if matches!(*st, CircuitState::HalfOpen { .. }) {
139 *st = CircuitState::HalfOpen {
140 probe_admitted: false,
141 };
142 }
143 Poll::Pending
144 }
145 }
146 } else if self.config.fallback.is_some() {
147 Poll::Ready(Ok(()))
148 } else {
149 self.record_rejection();
150 Poll::Ready(Err(CamelError::CircuitOpen(
151 "circuit breaker is open".into(),
152 )))
153 }
154 }
155 CircuitState::HalfOpen { probe_admitted } => {
156 if probe_admitted {
157 drop(state);
163 self.record_rejection();
164 Poll::Ready(Err(CamelError::CircuitOpen(
165 "circuit breaker is half-open (probe in flight)".into(),
166 )))
167 } else {
168 *state = CircuitState::HalfOpen {
171 probe_admitted: true,
172 };
173 drop(state);
174 match self.inner.poll_ready(cx) {
175 Poll::Ready(result) => Poll::Ready(result),
176 Poll::Pending => {
177 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
178 if matches!(*st, CircuitState::HalfOpen { .. }) {
179 *st = CircuitState::HalfOpen {
180 probe_admitted: false,
181 };
182 }
183 Poll::Pending
184 }
185 }
186 }
187 }
188 }
189 }
190
191 fn call(&mut self, exchange: Exchange) -> Self::Future {
192 {
193 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
194 if let CircuitState::Open { opened_at } = *st {
195 if opened_at.elapsed() < self.config.open_duration {
196 if let Some(mut fallback) = self.config.fallback.clone() {
197 return Box::pin(async move { fallback.call(exchange).await });
198 }
199 self.record_rejection();
200 return Box::pin(async {
201 Err(CamelError::CircuitOpen("circuit breaker is open".into()))
202 });
203 }
204
205 tracing::info!("Circuit breaker transitioning from Open to HalfOpen");
206 *st = CircuitState::HalfOpen {
207 probe_admitted: true,
208 };
209 }
210 }
214
215 let mut inner = self.inner.clone();
217 let state = Arc::clone(&self.state);
218 let config = self.config.clone();
219
220 let current_is_half_open = matches!(
222 *state.lock().unwrap_or_else(|e| e.into_inner()),
223 CircuitState::HalfOpen { .. }
224 );
225
226 Box::pin(async move {
227 let result = inner.call(exchange).await;
228
229 let mut st = state.lock().unwrap_or_else(|e| e.into_inner());
231 match &result {
232 Ok(_) => {
233 if current_is_half_open {
235 tracing::info!("Circuit breaker transitioning from HalfOpen to Closed");
236 }
237 *st = CircuitState::Closed {
238 consecutive_failures: 0,
239 };
240 }
241 Err(_) => {
242 if current_is_half_open {
243 tracing::warn!(
245 "Circuit breaker transitioning from HalfOpen to Open (probe failed)"
246 );
247 *st = CircuitState::Open {
248 opened_at: Instant::now(),
249 };
250 } else if let CircuitState::Closed {
251 consecutive_failures,
252 } = &mut *st
253 {
254 *consecutive_failures += 1;
255 if *consecutive_failures >= config.failure_threshold {
256 tracing::warn!(
257 threshold = config.failure_threshold,
258 "Circuit breaker transitioning from Closed to Open (failure threshold reached)"
259 );
260 *st = CircuitState::Open {
261 opened_at: Instant::now(),
262 };
263 }
264 }
265 }
266 }
267
268 result
269 })
270 }
271}
272
273pub enum CircuitBreakerDecision {
277 Allow,
279 Fallback(BoxProcessor),
282 Reject(CamelError),
284}
285
286#[derive(Clone)]
288pub struct CircuitBreakerGate {
289 config: CircuitBreakerConfig,
290 state: Arc<Mutex<CircuitState>>,
291 route_id: Arc<str>,
292 metrics: Option<Arc<dyn MetricsCollector>>,
293}
294
295impl CircuitBreakerGate {
296 pub fn new(
300 config: CircuitBreakerConfig,
301 route_id: Arc<str>,
302 metrics: Option<Arc<dyn MetricsCollector>>,
303 ) -> Self {
304 Self {
305 config,
306 state: Arc::new(Mutex::new(CircuitState::Closed {
307 consecutive_failures: 0,
308 })),
309 route_id,
310 metrics,
311 }
312 }
313
314 fn record_rejection(&self) {
316 if let Some(ref metrics) = self.metrics {
317 metrics.increment_circuit_breaker_rejection(&self.route_id);
318 }
319 }
320
321 pub fn before_call(&self) -> CircuitBreakerDecision {
322 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
323 match *state {
324 CircuitState::Closed { .. } => CircuitBreakerDecision::Allow,
325 CircuitState::Open { opened_at } => {
326 if opened_at.elapsed() >= self.config.open_duration {
327 tracing::info!("Circuit breaker gate: Open → HalfOpen");
328 *state = CircuitState::HalfOpen {
329 probe_admitted: true,
330 };
331 CircuitBreakerDecision::Allow
332 } else if let Some(ref fallback) = self.config.fallback {
333 CircuitBreakerDecision::Fallback(fallback.clone())
334 } else {
335 self.record_rejection();
336 CircuitBreakerDecision::Reject(CamelError::CircuitOpen(
337 "circuit breaker is open".into(),
338 ))
339 }
340 }
341 CircuitState::HalfOpen { probe_admitted } => {
342 if probe_admitted {
343 if let Some(ref fallback) = self.config.fallback {
344 CircuitBreakerDecision::Fallback(fallback.clone())
345 } else {
346 self.record_rejection();
347 CircuitBreakerDecision::Reject(CamelError::CircuitOpen(
348 "circuit breaker is half-open (probe in flight)".into(),
349 ))
350 }
351 } else {
352 *state = CircuitState::HalfOpen {
353 probe_admitted: true,
354 };
355 CircuitBreakerDecision::Allow
356 }
357 }
358 }
359 }
360
361 pub fn after_result(&self, result: &Result<Exchange, CamelError>) {
362 let mut st = self.state.lock().unwrap_or_else(|e| e.into_inner());
363 let current_is_half_open = matches!(*st, CircuitState::HalfOpen { .. });
364 match result {
365 Ok(_) => {
366 if current_is_half_open {
367 tracing::info!("Circuit breaker gate: HalfOpen → Closed");
368 }
369 *st = CircuitState::Closed {
370 consecutive_failures: 0,
371 };
372 }
373 Err(_) => {
374 if current_is_half_open {
375 tracing::warn!("Circuit breaker gate: HalfOpen → Open (probe failed)");
376 *st = CircuitState::Open {
377 opened_at: Instant::now(),
378 };
379 } else if let CircuitState::Closed {
380 consecutive_failures,
381 } = &mut *st
382 {
383 *consecutive_failures += 1;
384 if *consecutive_failures >= self.config.failure_threshold {
385 tracing::warn!(
386 threshold = self.config.failure_threshold,
387 "Circuit breaker gate: Closed → Open (failure threshold reached)"
388 );
389 *st = CircuitState::Open {
390 opened_at: Instant::now(),
391 };
392 }
393 }
394 }
395 }
396 }
397}
398
399#[cfg(test)]
402mod tests {
403 use super::*;
404 use camel_api::{BoxProcessor, BoxProcessorExt, Message};
405 use std::sync::atomic::{AtomicU32, Ordering};
406 use std::time::Duration;
407 use tower::ServiceExt;
408
409 fn make_exchange() -> Exchange {
410 Exchange::new(Message::new("test"))
411 }
412
413 fn gate_with(config: CircuitBreakerConfig) -> CircuitBreakerGate {
414 CircuitBreakerGate::new(config, Arc::from("test"), None)
415 }
416
417 fn ok_processor() -> BoxProcessor {
418 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
419 }
420
421 fn failing_processor() -> BoxProcessor {
422 BoxProcessor::from_fn(|_ex| {
423 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
424 })
425 }
426
427 fn fail_n_times(n: u32) -> BoxProcessor {
428 let count = Arc::new(AtomicU32::new(0));
429 BoxProcessor::from_fn(move |ex| {
430 let count = Arc::clone(&count);
431 Box::pin(async move {
432 let c = count.fetch_add(1, Ordering::SeqCst);
433 if c < n {
434 Err(CamelError::ProcessorError(format!("attempt {c}")))
435 } else {
436 Ok(ex)
437 }
438 })
439 })
440 }
441
442 fn tag_processor(tag: &'static str) -> BoxProcessor {
443 BoxProcessor::from_fn(move |_ex| {
444 Box::pin(async move {
445 let mut out = make_exchange();
446 out.input.body = tag.to_string().into();
447 Ok(out)
448 })
449 })
450 }
451
452 #[tokio::test]
454 async fn test_stays_closed_on_success() {
455 let config = CircuitBreakerConfig::new().failure_threshold(3);
456 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
457 let mut svc = layer.layer(ok_processor());
458
459 for _ in 0..5 {
460 let result = svc.ready().await.unwrap().call(make_exchange()).await;
461 assert!(result.is_ok());
462 }
463
464 let state = svc.state.lock().unwrap();
466 match *state {
467 CircuitState::Closed {
468 consecutive_failures,
469 } => assert_eq!(consecutive_failures, 0),
470 _ => panic!("expected Closed state"),
471 }
472 }
473
474 #[tokio::test]
476 async fn test_opens_after_failure_threshold() {
477 let config = CircuitBreakerConfig::new().failure_threshold(3);
478 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
479 let mut svc = layer.layer(failing_processor());
480
481 for _ in 0..3 {
483 let result = svc.ready().await.unwrap().call(make_exchange()).await;
484 assert!(result.is_err());
485 }
486
487 let waker = futures::task::noop_waker();
489 let mut cx = Context::from_waker(&waker);
490 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
491 match poll {
492 Poll::Ready(Err(CamelError::CircuitOpen(_))) => {} other => panic!("expected CircuitOpen error, got {other:?}"),
494 }
495 }
496
497 #[tokio::test]
499 async fn test_transitions_to_half_open_after_duration() {
500 let config = CircuitBreakerConfig::new()
501 .failure_threshold(2)
502 .open_duration(Duration::from_millis(50));
503 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
504 let mut svc = layer.layer(fail_n_times(2));
507
508 for _ in 0..2 {
510 let _ = svc.ready().await.unwrap().call(make_exchange()).await;
511 }
512
513 tokio::time::sleep(Duration::from_millis(60)).await;
515
516 let result = svc.ready().await.unwrap().call(make_exchange()).await;
518 assert!(result.is_ok(), "half-open probe should succeed");
519
520 let state = svc.state.lock().unwrap();
522 match *state {
523 CircuitState::Closed {
524 consecutive_failures,
525 } => assert_eq!(consecutive_failures, 0),
526 _ => panic!("expected Closed state after successful half-open probe"),
527 }
528 }
529
530 #[tokio::test]
532 async fn test_half_open_failure_reopens() {
533 let config = CircuitBreakerConfig::new()
534 .failure_threshold(2)
535 .open_duration(Duration::from_millis(50));
536 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
537 let mut svc = layer.layer(failing_processor());
538
539 for _ in 0..2 {
541 let _ = svc.ready().await.unwrap().call(make_exchange()).await;
542 }
543
544 tokio::time::sleep(Duration::from_millis(60)).await;
546
547 let result = svc.ready().await.unwrap().call(make_exchange()).await;
549 assert!(result.is_err());
550
551 let state = svc.state.lock().unwrap();
553 match *state {
554 CircuitState::Open { .. } => {} _ => panic!("expected Open state after half-open failure"),
556 }
557 }
558
559 #[tokio::test]
561 async fn test_intermittent_failures_dont_open() {
562 let config = CircuitBreakerConfig::new().failure_threshold(3);
563 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
564
565 let call_count = Arc::new(AtomicU32::new(0));
568 let cc = Arc::clone(&call_count);
569 let inner = BoxProcessor::from_fn(move |ex| {
570 let cc = Arc::clone(&cc);
571 Box::pin(async move {
572 let c = cc.fetch_add(1, Ordering::SeqCst);
573 if c % 3 == 2 {
575 Ok(ex)
576 } else {
577 Err(CamelError::ProcessorError("intermittent".into()))
578 }
579 })
580 });
581
582 let mut svc = layer.layer(inner);
583
584 for _ in 0..6 {
585 let _ = svc.ready().await.unwrap().call(make_exchange()).await;
586 }
587
588 let state = svc.state.lock().unwrap();
590 match *state {
591 CircuitState::Closed { .. } => {} _ => panic!("expected circuit to remain Closed"),
593 }
594 }
595
596 #[tokio::test]
597 async fn test_open_uses_fallback_when_configured() {
598 let fallback = tag_processor("fallback");
599 let config = CircuitBreakerConfig::new()
600 .failure_threshold(1)
601 .open_duration(Duration::from_secs(60))
602 .fallback(fallback);
603 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
604 let mut svc = layer.layer(failing_processor());
605
606 let _ = svc.ready().await.unwrap().call(make_exchange()).await;
607 let result = svc
608 .ready()
609 .await
610 .unwrap()
611 .call(make_exchange())
612 .await
613 .unwrap();
614 assert_eq!(result.input.body.as_text(), Some("fallback"));
615 }
616
617 #[tokio::test]
618 async fn test_open_without_fallback_returns_err() {
619 let config = CircuitBreakerConfig::new()
620 .failure_threshold(1)
621 .open_duration(Duration::from_secs(60));
622 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
623 let mut svc = layer.layer(failing_processor());
624
625 let _ = svc.ready().await.unwrap().call(make_exchange()).await;
626 let result = svc.ready().await;
627 assert!(matches!(result, Err(CamelError::CircuitOpen(_))));
628 }
629
630 #[test]
633 fn test_cb_gate_before_call_closed_allows() {
634 let gate = gate_with(CircuitBreakerConfig {
635 failure_threshold: 3,
636 open_duration: Duration::from_secs(60),
637 success_threshold: 1,
638 fallback: None,
639 });
640 assert!(matches!(gate.before_call(), CircuitBreakerDecision::Allow));
641 }
642
643 #[test]
644 fn test_cb_gate_records_failures_and_opens() {
645 let gate = gate_with(CircuitBreakerConfig {
646 failure_threshold: 2,
647 open_duration: Duration::from_secs(60),
648 success_threshold: 1,
649 fallback: None,
650 });
651 gate.after_result(&Err(CamelError::ProcessorError("fail".into())));
652 assert!(
653 matches!(gate.before_call(), CircuitBreakerDecision::Allow),
654 "still closed after 1 failure"
655 );
656 gate.after_result(&Err(CamelError::ProcessorError("fail".into())));
657 assert!(
658 matches!(gate.before_call(), CircuitBreakerDecision::Reject(_)),
659 "should be open after 2 failures"
660 );
661 }
662
663 #[tokio::test]
664 async fn test_cb_gate_closes_on_success() {
665 let gate = gate_with(CircuitBreakerConfig {
666 failure_threshold: 1,
667 open_duration: Duration::from_millis(1),
668 success_threshold: 1,
669 fallback: None,
670 });
671 gate.after_result(&Err(CamelError::ProcessorError("fail".into())));
672 assert!(
673 matches!(gate.before_call(), CircuitBreakerDecision::Reject(_)),
674 "should be open"
675 );
676 tokio::time::sleep(Duration::from_millis(10)).await;
677 assert!(
678 matches!(gate.before_call(), CircuitBreakerDecision::Allow),
679 "should transition to half-open"
680 );
681 let ex = Exchange::new(Message::new("test"));
682 gate.after_result(&Ok(ex));
683 assert!(
684 matches!(gate.before_call(), CircuitBreakerDecision::Allow),
685 "should be closed again"
686 );
687 }
688
689 #[tokio::test]
690 async fn test_cb_gate_half_open_failure_reopens() {
691 let gate = gate_with(CircuitBreakerConfig {
692 failure_threshold: 1,
693 open_duration: Duration::from_millis(1),
694 success_threshold: 1,
695 fallback: None,
696 });
697 gate.after_result(&Err(CamelError::ProcessorError("fail".into())));
699 assert!(
700 matches!(gate.before_call(), CircuitBreakerDecision::Reject(_)),
701 "should be open"
702 );
703
704 tokio::time::sleep(Duration::from_millis(10)).await;
706 assert!(
707 matches!(gate.before_call(), CircuitBreakerDecision::Allow),
708 "should be half-open now"
709 );
710
711 gate.after_result(&Err(CamelError::ProcessorError("probe fail".into())));
713 assert!(
714 matches!(gate.before_call(), CircuitBreakerDecision::Reject(_)),
715 "should be open again after probe failure"
716 );
717 }
718
719 #[test]
720 fn test_cb_gate_open_with_fallback_returns_fallback() {
721 let fallback = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
722 let gate = gate_with(CircuitBreakerConfig {
723 failure_threshold: 1,
724 open_duration: Duration::from_secs(60),
725 success_threshold: 1,
726 fallback: Some(fallback),
727 });
728 gate.after_result(&Err(CamelError::ProcessorError("fail".into())));
729 assert!(
730 matches!(gate.before_call(), CircuitBreakerDecision::Fallback(_)),
731 "should return fallback when open"
732 );
733 }
734
735 #[test]
736 fn test_cb_gate_handled_error_counts_as_success() {
737 let gate = gate_with(CircuitBreakerConfig {
738 failure_threshold: 1,
739 open_duration: Duration::from_secs(60),
740 success_threshold: 1,
741 fallback: None,
742 });
743 let ex = Exchange::new(Message::new("test"));
744 gate.after_result(&Ok(ex));
745 assert!(
746 matches!(gate.before_call(), CircuitBreakerDecision::Allow),
747 "handled error should not trip CB"
748 );
749 }
750
751 #[test]
756 fn gate_half_open_admits_only_one_probe() {
757 let config = CircuitBreakerConfig {
758 failure_threshold: 1,
759 open_duration: Duration::from_millis(1),
760 success_threshold: 1,
761 fallback: None,
762 };
763 let gate = gate_with(config);
764 gate.after_result(&Err::<Exchange, CamelError>(CamelError::CircuitOpen(
766 "boom".into(),
767 )));
768 std::thread::sleep(Duration::from_millis(10)); let d1 = gate.before_call();
770 assert!(
771 matches!(d1, CircuitBreakerDecision::Allow),
772 "first probe must be admitted"
773 );
774 let d2 = gate.before_call();
775 assert!(
776 matches!(d2, CircuitBreakerDecision::Reject(_)),
777 "2nd concurrent caller must be rejected"
778 );
779 }
780
781 #[tokio::test]
785 async fn service_half_open_admits_only_one_probe() {
786 let config = CircuitBreakerConfig::new()
787 .failure_threshold(1)
788 .open_duration(Duration::from_millis(1));
789 let layer = CircuitBreakerLayer::new(config, Arc::from("test"), None);
790 let mut svc1 = layer.layer(failing_processor());
791 let mut svc2 = svc1.clone();
792
793 let _ = svc1.ready().await.unwrap().call(make_exchange()).await;
795
796 tokio::time::sleep(Duration::from_millis(10)).await;
798
799 let waker = futures::task::noop_waker();
801 let mut cx = Context::from_waker(&waker);
802 let p1 = std::pin::Pin::new(&mut svc1).poll_ready(&mut cx);
803 assert!(
804 matches!(p1, Poll::Ready(Ok(()))),
805 "first probe admitted, got {p1:?}"
806 );
807
808 let p2 = std::pin::Pin::new(&mut svc2).poll_ready(&mut cx);
810 match p2 {
811 Poll::Ready(Err(CamelError::CircuitOpen(_))) => {} other => panic!("expected CircuitOpen error on 2nd probe, got {other:?}"),
813 }
814 }
815}