1mod bulkhead;
41mod circuit_breaker;
42mod rate_limit;
43
44use std::collections::HashMap;
45use std::fmt;
46use std::sync::{Arc, Mutex, PoisonError};
47use std::time::{Duration, Instant};
48
49use async_trait::async_trait;
50
51use crate::client::ClientBuilder;
52use crate::error::{Error, ErrorCode};
53use crate::observability::{Hooks, OperationInfo, OperationState, RequestInfo, RequestResult};
54
55pub use bulkhead::{Bulkhead, BulkheadConfig, BulkheadPermit};
56pub use circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
57pub use rate_limit::{RateLimitConfig, RateLimiter};
58
59#[derive(Debug, Clone)]
62pub struct ResilienceConfig {
63 pub circuit_breaker: Option<CircuitBreakerConfig>,
65 pub bulkhead: Option<BulkheadConfig>,
67 pub rate_limit: Option<RateLimitConfig>,
69}
70
71impl Default for ResilienceConfig {
72 fn default() -> ResilienceConfig {
73 ResilienceConfig {
74 circuit_breaker: Some(CircuitBreakerConfig::default()),
75 bulkhead: Some(BulkheadConfig::default()),
76 rate_limit: Some(RateLimitConfig::default()),
77 }
78 }
79}
80
81impl ResilienceConfig {
82 pub fn none() -> ResilienceConfig {
85 ResilienceConfig {
86 circuit_breaker: None,
87 bulkhead: None,
88 rate_limit: None,
89 }
90 }
91}
92
93impl ClientBuilder {
94 #[must_use]
97 pub fn resilience(mut self, config: ResilienceConfig) -> ClientBuilder {
98 self.hooks = Arc::new(ResilienceHooks::new(self.hooks.clone(), config));
99 self
100 }
101
102 #[must_use]
104 pub fn circuit_breaker(self, config: CircuitBreakerConfig) -> ClientBuilder {
105 self.resilience(ResilienceConfig {
106 circuit_breaker: Some(config),
107 ..ResilienceConfig::none()
108 })
109 }
110
111 #[must_use]
113 pub fn bulkhead(self, config: BulkheadConfig) -> ClientBuilder {
114 self.resilience(ResilienceConfig {
115 bulkhead: Some(config),
116 ..ResilienceConfig::none()
117 })
118 }
119
120 #[must_use]
122 pub fn rate_limit(self, config: RateLimitConfig) -> ClientBuilder {
123 self.resilience(ResilienceConfig {
124 rate_limit: Some(config),
125 ..ResilienceConfig::none()
126 })
127 }
128}
129
130pub fn should_trip_circuit(error: &Error) -> bool {
139 match error.code() {
140 ErrorCode::CircuitOpen | ErrorCode::BulkheadFull | ErrorCode::RateLimit => false,
141 ErrorCode::Network => true,
142 _ => error.http_status().is_some_and(|status| status >= 500),
143 }
144}
145
146#[derive(Clone)]
148pub struct Clock(Arc<dyn Fn() -> Instant + Send + Sync>);
149
150impl Clock {
151 pub fn new(now: impl Fn() -> Instant + Send + Sync + 'static) -> Clock {
153 Clock(Arc::new(now))
154 }
155
156 pub fn now(&self) -> Instant {
158 (self.0)()
159 }
160}
161
162impl Default for Clock {
163 fn default() -> Clock {
164 Clock::new(Instant::now)
165 }
166}
167
168impl fmt::Debug for Clock {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.write_str("Clock")
171 }
172}
173
174pub struct ResilienceHooks {
179 inner: Arc<dyn Hooks>,
180 circuit_breakers: Option<Registry<CircuitBreaker>>,
181 bulkheads: Option<Registry<Bulkhead>>,
182 rate_limiter: Option<RateLimiter>,
183 pending: Mutex<HashMap<String, Vec<BulkheadPermit>>>,
190}
191
192impl ResilienceHooks {
193 pub fn new(inner: Arc<dyn Hooks>, config: ResilienceConfig) -> ResilienceHooks {
195 ResilienceHooks {
196 inner,
197 circuit_breakers: config
198 .circuit_breaker
199 .map(|config| Registry::new(move || CircuitBreaker::new(config.clone()))),
200 bulkheads: config
201 .bulkhead
202 .map(|config| Registry::new(move || Bulkhead::new(config.clone()))),
203 rate_limiter: config.rate_limit.map(RateLimiter::new),
204 pending: Mutex::default(),
205 }
206 }
207
208 async fn admit(&self, scope: &str) -> Result<Option<BulkheadPermit>, Error> {
215 if let Some(breakers) = &self.circuit_breakers
216 && !breakers.get(scope).allow()
217 {
218 return Err(Error::circuit_open());
219 }
220
221 let permit = match &self.bulkheads {
222 Some(bulkheads) => Some(bulkheads.get(scope).acquire().await?),
223 None => None,
224 };
225
226 if let Some(limiter) = &self.rate_limiter
227 && !limiter.allow()
228 {
229 return Err(Error::rate_limited());
230 }
231
232 Ok(permit)
233 }
234
235 fn record(&self, scope: &str, outcome: Result<(), &Error>) {
236 if let Some(breakers) = &self.circuit_breakers {
237 let breaker = breakers.get(scope);
238 match outcome {
239 Ok(()) => breaker.record_success(),
240 Err(error) if should_trip_circuit(error) => breaker.record_failure(),
241 Err(_) => {}
242 }
243 }
244 }
245}
246
247#[async_trait]
248impl Hooks for ResilienceHooks {
249 async fn on_operation_gate(&self, op: &OperationInfo) -> Result<(), Error> {
250 let scope = scope_of(op);
251 let permit = self.admit(&scope).await?;
252 self.inner.on_operation_gate(op).await?;
253 if let Some(permit) = permit {
254 self.pending
255 .lock()
256 .unwrap_or_else(PoisonError::into_inner)
257 .entry(scope)
258 .or_default()
259 .push(permit);
260 }
261 Ok(())
262 }
263
264 fn on_operation_start(&self, op: &OperationInfo) -> OperationState {
265 let permit = self
266 .pending
267 .lock()
268 .unwrap_or_else(PoisonError::into_inner)
269 .get_mut(&scope_of(op))
270 .and_then(Vec::pop);
271 Some(Box::new(Held {
272 permit,
273 inner: self.inner.on_operation_start(op),
274 }))
275 }
276
277 fn on_operation_end(
278 &self,
279 op: &OperationInfo,
280 state: OperationState,
281 outcome: Result<(), &Error>,
282 duration: Duration,
283 ) {
284 let held = match state.and_then(|state| state.downcast::<Held>().ok()) {
285 Some(held) => *held,
286 None => Held::default(),
287 };
288 drop(held.permit);
289 self.record(&scope_of(op), outcome);
290 self.inner
291 .on_operation_end(op, held.inner, outcome, duration);
292 }
293
294 fn on_request_start(&self, info: &RequestInfo) {
295 self.inner.on_request_start(info);
296 }
297
298 fn on_request_end(&self, info: &RequestInfo, result: &RequestResult<'_>) {
302 if let Some(limiter) = &self.rate_limiter {
303 let asked = result.retry_after.filter(|seconds| *seconds > 0);
304 match result.status.map(|status| status.as_u16()) {
305 Some(429) => limiter.set_retry_after_in(Duration::from_secs(asked.unwrap_or(60))),
306 Some(503) => {
307 if let Some(seconds) = asked {
308 limiter.set_retry_after_in(Duration::from_secs(seconds));
309 }
310 }
311 _ => {}
312 }
313 }
314 self.inner.on_request_end(info, result);
315 }
316
317 fn on_retry(&self, info: &RequestInfo, next_attempt: u32, cause: &Error) {
318 self.inner.on_retry(info, next_attempt, cause);
319 }
320}
321
322#[derive(Default)]
325struct Held {
326 permit: Option<BulkheadPermit>,
327 inner: OperationState,
328}
329
330fn scope_of(op: &OperationInfo) -> String {
331 format!("{}.{}", op.service, op.operation)
332}
333
334struct Registry<T> {
337 build: Box<dyn Fn() -> T + Send + Sync>,
338 entries: Mutex<HashMap<String, Arc<T>>>,
339}
340
341impl<T> Registry<T> {
342 fn new(build: impl Fn() -> T + Send + Sync + 'static) -> Registry<T> {
343 Registry {
344 build: Box::new(build),
345 entries: Mutex::default(),
346 }
347 }
348
349 fn get(&self, scope: &str) -> Arc<T> {
350 self.entries
351 .lock()
352 .unwrap_or_else(PoisonError::into_inner)
353 .entry(scope.to_string())
354 .or_insert_with(|| Arc::new((self.build)()))
355 .clone()
356 }
357}
358
359#[cfg(test)]
361pub(crate) fn test_clock() -> (Clock, Arc<Mutex<Instant>>) {
362 let now = Arc::new(Mutex::new(Instant::now()));
363 let reading = now.clone();
364 (Clock::new(move || *reading.lock().unwrap()), now)
365}
366
367#[cfg(test)]
368pub(crate) fn advance(clock: &Arc<Mutex<Instant>>, elapsed: Duration) {
369 let mut now = clock.lock().unwrap();
370 *now += elapsed;
371}
372
373#[cfg(test)]
374mod tests {
375 use std::borrow::Cow;
376
377 use super::*;
378
379 #[test]
380 fn the_default_config_installs_every_layer() {
381 let config = ResilienceConfig::default();
382
383 assert!(config.circuit_breaker.is_some());
384 assert!(config.bulkhead.is_some());
385 assert!(config.rate_limit.is_some());
386 }
387
388 #[test]
389 fn only_what_hey_failed_to_answer_trips_the_breaker() {
390 let cases = [
391 (Error::circuit_open(), false),
392 (Error::bulkhead_full(), false),
393 (Error::rate_limited(), false),
394 (Error::rate_limit(Some(3)), false),
395 (
396 Error::network(std::io::Error::other("connection refused")),
397 true,
398 ),
399 (Error::api(500, "boom"), true),
400 (Error::api(503, "unavailable"), true),
401 (Error::api(400, "bad request"), false),
402 (Error::auth("authentication required"), false),
403 (Error::usage("bad argument"), false),
404 (Error::not_found("box", 1), false),
405 ];
406
407 for (error, expected) in cases {
408 assert_eq!(expected, should_trip_circuit(&error), "{error}");
409 }
410 }
411
412 #[test]
413 fn a_registry_keeps_one_breaker_per_scope() {
414 let registry = Registry::new(|| CircuitBreaker::new(CircuitBreakerConfig::default()));
415
416 assert!(Arc::ptr_eq(
417 ®istry.get("scope1"),
418 ®istry.get("scope1")
419 ));
420 assert!(!Arc::ptr_eq(
421 ®istry.get("scope1"),
422 ®istry.get("scope2")
423 ));
424 }
425
426 #[test]
427 fn a_registry_keeps_one_bulkhead_per_scope() {
428 let registry = Registry::new(|| Bulkhead::new(BulkheadConfig::default()));
429
430 assert!(Arc::ptr_eq(
431 ®istry.get("scope1"),
432 ®istry.get("scope1")
433 ));
434 assert!(!Arc::ptr_eq(
435 ®istry.get("scope1"),
436 ®istry.get("scope2")
437 ));
438 }
439
440 #[tokio::test]
441 async fn a_breaker_only_config_refuses_once_the_scope_has_failed_enough() {
442 let hooks = hooks(ResilienceConfig {
443 circuit_breaker: Some(CircuitBreakerConfig {
444 failure_threshold: 2,
445 ..CircuitBreakerConfig::default()
446 }),
447 ..ResilienceConfig::none()
448 });
449
450 fail(&hooks, &operation("Boxes", "ListBoxes")).await;
451 assert!(
452 hooks
453 .on_operation_gate(&operation("Boxes", "ListBoxes"))
454 .await
455 .is_ok()
456 );
457 fail(&hooks, &operation("Boxes", "ListBoxes")).await;
458
459 let refused = hooks
460 .on_operation_gate(&operation("Boxes", "ListBoxes"))
461 .await
462 .unwrap_err();
463 assert_eq!(ErrorCode::CircuitOpen, refused.code());
464 assert_eq!("circuit breaker is open", refused.message());
465 assert!(
466 hooks
467 .on_operation_gate(&operation("Boxes", "GetBox"))
468 .await
469 .is_ok()
470 );
471 }
472
473 #[tokio::test]
474 async fn a_bulkhead_with_no_wait_refuses_a_scope_that_is_already_busy() {
475 let hooks = hooks(ResilienceConfig {
476 bulkhead: Some(BulkheadConfig {
477 max_concurrent: 1,
478 max_wait: Duration::ZERO,
479 }),
480 ..ResilienceConfig::none()
481 });
482 let op = operation("Boxes", "ListBoxes");
483
484 hooks.on_operation_gate(&op).await.unwrap();
485 let state = hooks.on_operation_start(&op);
486
487 let refused = hooks.on_operation_gate(&op).await.unwrap_err();
488 assert_eq!(ErrorCode::BulkheadFull, refused.code());
489 assert_eq!("bulkhead is full", refused.message());
490 assert!(
491 hooks
492 .on_operation_gate(&operation("Boxes", "GetBox"))
493 .await
494 .is_ok()
495 );
496
497 hooks.on_operation_end(&op, state, Ok(()), Duration::ZERO);
498 assert!(hooks.on_operation_gate(&op).await.is_ok());
499 }
500
501 #[tokio::test]
502 async fn a_limiter_only_config_refuses_once_the_budget_is_spent() {
503 let hooks = hooks(ResilienceConfig {
504 rate_limit: Some(RateLimitConfig {
505 requests_per_second: 0.0001,
506 burst_size: 1,
507 ..RateLimitConfig::default()
508 }),
509 ..ResilienceConfig::none()
510 });
511 let op = operation("Boxes", "ListBoxes");
512
513 hooks.on_operation_gate(&op).await.unwrap();
514
515 let refused = hooks.on_operation_gate(&op).await.unwrap_err();
516 assert_eq!(ErrorCode::RateLimit, refused.code());
517 assert_eq!("rate limit exceeded", refused.message());
518 assert_eq!(None, refused.http_status());
519 }
520
521 #[tokio::test]
524 async fn a_refused_call_gives_back_the_permit_it_took() {
525 let hooks = hooks(ResilienceConfig {
526 bulkhead: Some(BulkheadConfig {
527 max_concurrent: 1,
528 max_wait: Duration::ZERO,
529 }),
530 rate_limit: Some(RateLimitConfig {
531 requests_per_second: 0.0001,
532 burst_size: 1,
533 ..RateLimitConfig::default()
534 }),
535 ..ResilienceConfig::none()
536 });
537 let op = operation("Boxes", "ListBoxes");
538
539 hooks.on_operation_gate(&op).await.unwrap();
540 let state = hooks.on_operation_start(&op);
541 hooks.on_operation_end(&op, state, Ok(()), Duration::ZERO);
542
543 assert_eq!(
544 ErrorCode::RateLimit,
545 hooks.on_operation_gate(&op).await.unwrap_err().code()
546 );
547 assert_eq!(
548 1,
549 hooks
550 .bulkheads
551 .as_ref()
552 .unwrap()
553 .get("Boxes.ListBoxes")
554 .available()
555 );
556 }
557
558 #[tokio::test]
561 async fn a_call_refused_below_gives_back_the_permit_too() {
562 let hooks = ResilienceHooks::new(
563 Arc::new(Refusing),
564 ResilienceConfig {
565 bulkhead: Some(BulkheadConfig {
566 max_concurrent: 1,
567 max_wait: Duration::ZERO,
568 }),
569 ..ResilienceConfig::none()
570 },
571 );
572 let op = operation("Boxes", "ListBoxes");
573
574 let refused = hooks.on_operation_gate(&op).await.unwrap_err();
575
576 assert_eq!(ErrorCode::Usage, refused.code());
577 assert_eq!(
578 1,
579 hooks
580 .bulkheads
581 .as_ref()
582 .unwrap()
583 .get("Boxes.ListBoxes")
584 .available()
585 );
586 }
587
588 #[tokio::test]
589 async fn the_hooks_underneath_still_hear_everything() {
590 let recorder = Arc::new(Recorder::default());
591 let hooks = ResilienceHooks::new(recorder.clone(), ResilienceConfig::default());
592 let op = operation("Boxes", "ListBoxes");
593
594 hooks.on_operation_gate(&op).await.unwrap();
595 let state = hooks.on_operation_start(&op);
596 hooks.on_operation_end(&op, state, Ok(()), Duration::ZERO);
597
598 assert_eq!(
599 vec!["gate", "start", "end carrying its own"],
600 recorder.entries()
601 );
602 }
603
604 fn hooks(config: ResilienceConfig) -> ResilienceHooks {
605 ResilienceHooks::new(Arc::new(crate::observability::NoopHooks), config)
606 }
607
608 async fn fail(hooks: &ResilienceHooks, op: &OperationInfo) {
609 hooks.on_operation_gate(op).await.unwrap();
610 let state = hooks.on_operation_start(op);
611 hooks.on_operation_end(op, state, Err(&Error::api(500, "boom")), Duration::ZERO);
612 }
613
614 fn operation(service: &'static str, operation: &'static str) -> OperationInfo {
615 OperationInfo {
616 service: Cow::Borrowed(service),
617 operation: Cow::Borrowed(operation),
618 resource_type: Cow::Borrowed("box"),
619 is_mutation: false,
620 resource_id: None,
621 }
622 }
623
624 #[derive(Default)]
625 struct Recorder {
626 entries: Mutex<Vec<String>>,
627 }
628
629 impl Recorder {
630 fn entries(&self) -> Vec<String> {
631 self.entries.lock().unwrap().clone()
632 }
633
634 fn record(&self, entry: &str) {
635 self.entries.lock().unwrap().push(entry.to_string());
636 }
637 }
638
639 #[async_trait]
640 impl Hooks for Recorder {
641 async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
642 self.record("gate");
643 Ok(())
644 }
645
646 fn on_operation_start(&self, _op: &OperationInfo) -> OperationState {
647 self.record("start");
648 Some(Box::new("its own"))
649 }
650
651 fn on_operation_end(
652 &self,
653 _op: &OperationInfo,
654 state: OperationState,
655 _outcome: Result<(), &Error>,
656 _duration: Duration,
657 ) {
658 let carried = match state.and_then(|state| state.downcast::<&str>().ok()) {
659 Some(carried) => *carried,
660 None => "nothing",
661 };
662 self.record(&format!("end carrying {carried}"));
663 }
664 }
665
666 struct Refusing;
668
669 #[async_trait]
670 impl Hooks for Refusing {
671 async fn on_operation_gate(&self, _op: &OperationInfo) -> Result<(), Error> {
672 Err(Error::usage("blocked"))
673 }
674 }
675}