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