1use std::future::Future;
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::sync::{Arc, Mutex, PoisonError};
39use std::time::{Duration, Instant};
40
41use async_trait::async_trait;
42
43use crate::backend::{Backend, Freshness, HealthStatus, LockableBackend};
44use crate::client::SharedBackend;
45use crate::error::BackendError;
46use crate::random_unit;
47
48#[derive(Debug, Clone, PartialEq)]
63pub struct RetryConfig {
64 pub max_attempts: u32,
66 pub base_delay: Duration,
68 pub max_delay: Duration,
70 pub jitter: bool,
72}
73
74impl Default for RetryConfig {
75 fn default() -> Self {
76 Self {
77 max_attempts: 3,
78 base_delay: Duration::from_millis(100),
79 max_delay: Duration::from_secs(5),
80 jitter: true,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq)]
90pub struct CircuitBreakerConfig {
91 pub failure_threshold: u32,
94 pub success_threshold: u32,
96 pub open_timeout: Duration,
99 pub half_open_max_calls: u32,
101 pub rolling_window: Duration,
103}
104
105impl Default for CircuitBreakerConfig {
106 fn default() -> Self {
107 Self {
108 failure_threshold: 5,
109 success_threshold: 3,
110 open_timeout: Duration::from_secs(5),
111 half_open_max_calls: 3,
112 rolling_window: Duration::from_secs(60),
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq)]
135pub struct BackpressureConfig {
136 pub max_concurrent: usize,
141 pub max_queue: usize,
145 pub acquire_timeout: Duration,
148}
149
150impl Default for BackpressureConfig {
151 fn default() -> Self {
152 Self {
153 max_concurrent: 100,
154 max_queue: 1000,
155 acquire_timeout: Duration::from_millis(100),
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq)]
176pub struct ReliabilityConfig {
177 pub retry: Option<RetryConfig>,
179 pub circuit_breaker: Option<CircuitBreakerConfig>,
181 pub backpressure: Option<BackpressureConfig>,
183}
184
185impl Default for ReliabilityConfig {
186 fn default() -> Self {
187 Self {
188 retry: Some(RetryConfig::default()),
189 circuit_breaker: Some(CircuitBreakerConfig::default()),
190 backpressure: Some(BackpressureConfig::default()),
191 }
192 }
193}
194
195impl ReliabilityConfig {
196 #[must_use]
209 pub fn disabled() -> Self {
210 Self {
211 retry: None,
212 circuit_breaker: None,
213 backpressure: None,
214 }
215 }
216
217 #[must_use]
221 pub fn is_disabled(&self) -> bool {
222 self.retry.is_none() && self.circuit_breaker.is_none() && self.backpressure.is_none()
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub(crate) struct RetryDeadlines {
234 pub(crate) read: Duration,
236 pub(crate) write: Duration,
238}
239
240#[cfg(feature = "cachekitio")]
242pub(crate) const CACHEKITIO_DEADLINES: RetryDeadlines = RetryDeadlines {
243 read: Duration::from_secs(5),
244 write: Duration::from_secs(10),
245};
246
247#[derive(Debug)]
252pub(crate) struct RetryPolicy {
253 config: RetryConfig,
254}
255
256impl RetryPolicy {
257 pub(crate) fn new(config: RetryConfig) -> Self {
258 Self { config }
259 }
260
261 fn delay(&self, attempt: u32) -> Duration {
262 let exp = self
263 .config
264 .base_delay
265 .saturating_mul(2u32.saturating_pow(attempt));
266 let capped = exp.min(self.config.max_delay);
267 if self.config.jitter {
268 capped.mul_f64(0.5 + random_unit())
269 } else {
270 capped
271 }
272 }
273
274 pub(crate) async fn execute<T, F, Fut>(
278 &self,
279 budget: Option<Duration>,
280 f: F,
281 ) -> Result<T, BackendError>
282 where
283 F: Fn() -> Fut,
284 Fut: Future<Output = Result<T, BackendError>>,
285 {
286 let deadline = budget.map(|budget| tokio::time::Instant::now() + budget);
287 let mut attempt: u32 = 0;
288 loop {
289 let result = match deadline {
290 Some(deadline) => match tokio::time::timeout_at(deadline, f()).await {
291 Ok(result) => result,
292 Err(_elapsed) => {
293 return Err(BackendError::timeout(format!(
294 "operation did not complete within its {:?} deadline ({} attempt(s))",
295 budget.unwrap_or_default(),
296 attempt + 1
297 )))
298 }
299 },
300 None => f().await,
301 };
302 match result {
303 Ok(v) => return Ok(v),
304 Err(e)
305 if e.kind.is_retryable()
306 && !e.is_quota_denied()
307 && attempt + 1 < self.config.max_attempts =>
308 {
309 let delay = self.delay(attempt);
310 if deadline.is_some_and(|d| tokio::time::Instant::now() + delay >= d) {
313 return Err(e);
314 }
315 tokio::time::sleep(delay).await;
316 attempt += 1;
317 }
318 Err(e) => return Err(e),
319 }
320 }
321 }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum CircuitState {
331 Closed,
333 Open,
336 HalfOpen,
338}
339
340#[derive(Debug)]
341enum State {
342 Closed,
343 Open { since: Instant },
344 HalfOpen,
345}
346
347impl From<&State> for CircuitState {
348 fn from(state: &State) -> Self {
349 match state {
350 State::Closed => Self::Closed,
351 State::Open { .. } => Self::Open,
352 State::HalfOpen => Self::HalfOpen,
353 }
354 }
355}
356
357#[derive(Debug)]
358struct BreakerInner {
359 state: State,
360 failures: Vec<Instant>,
362 half_open_successes: u32,
363 half_open_calls: u32,
364 transitions: u64,
367}
368
369enum Outcome {
371 Success,
372 Failure,
374 Neutral,
378}
379
380#[derive(Debug)]
384pub(crate) struct CircuitBreaker {
385 id: u64,
389 config: CircuitBreakerConfig,
390 inner: Mutex<BreakerInner>,
391}
392
393static BREAKER_IDS: AtomicU64 = AtomicU64::new(0);
396
397impl CircuitBreaker {
398 pub(crate) fn new(config: CircuitBreakerConfig) -> Self {
399 Self {
400 id: BREAKER_IDS.fetch_add(1, Ordering::Relaxed) + 1,
401 config,
402 inner: Mutex::new(BreakerInner {
403 state: State::Closed,
404 failures: Vec::new(),
405 half_open_successes: 0,
406 half_open_calls: 0,
407 transitions: 0,
408 }),
409 }
410 }
411
412 fn lock(&self) -> std::sync::MutexGuard<'_, BreakerInner> {
413 self.inner.lock().unwrap_or_else(PoisonError::into_inner)
416 }
417
418 pub(crate) fn state(&self) -> CircuitState {
420 let mut inner = self.lock();
421 let change = self.maybe_half_open(&mut inner);
422 let state = CircuitState::from(&inner.state);
423 drop(inner);
424 emit(change);
425 state
426 }
427
428 fn maybe_half_open(&self, inner: &mut BreakerInner) -> Option<Transition> {
429 let State::Open { since } = inner.state else {
430 return None;
431 };
432 if since.elapsed() < self.config.open_timeout {
433 return None;
434 }
435 let change = transition(self.id, inner, State::HalfOpen);
436 inner.half_open_successes = 0;
437 inner.half_open_calls = 0;
438 Some(change)
439 }
440
441 fn try_acquire(&self) -> Result<ProbePermit<'_>, BackendError> {
450 let mut inner = self.lock();
451 let change = self.maybe_half_open(&mut inner);
452 let admitted = match inner.state {
453 State::Closed => Ok(ProbePermit {
454 breaker: self,
455 took_slot: false,
456 }),
457 State::Open { .. } => Err(BackendError::circuit_open(
458 "circuit breaker is open: backend calls are failing fast",
459 )),
460 State::HalfOpen => {
461 if inner.half_open_calls >= self.config.half_open_max_calls {
462 Err(BackendError::circuit_open(
463 "circuit breaker is half-open and the probe limit is reached",
464 ))
465 } else {
466 inner.half_open_calls += 1;
467 Ok(ProbePermit {
468 breaker: self,
469 took_slot: true,
470 })
471 }
472 }
473 };
474 drop(inner);
475 emit(change);
476 admitted
477 }
478
479 fn record(&self, outcome: &Outcome) {
480 let mut inner = self.lock();
481 let mut change = None;
482 match outcome {
483 Outcome::Success => {
484 if matches!(inner.state, State::HalfOpen) {
485 inner.half_open_successes += 1;
486 if inner.half_open_successes >= self.config.success_threshold {
487 change = Some(transition(self.id, &mut inner, State::Closed));
488 inner.failures.clear();
489 inner.half_open_successes = 0;
490 inner.half_open_calls = 0;
491 } else {
492 inner.half_open_calls = inner.half_open_calls.saturating_sub(1);
502 }
503 }
504 }
505 Outcome::Failure => match inner.state {
506 State::HalfOpen => {
507 let now = Instant::now();
508 change = Some(transition(self.id, &mut inner, State::Open { since: now }));
509 inner.half_open_successes = 0;
510 inner.half_open_calls = 0;
511 }
512 State::Closed => {
513 let now = Instant::now();
514 inner.failures.push(now);
515 let window = self.config.rolling_window;
516 inner.failures.retain(|t| now.duration_since(*t) <= window);
517 if inner.failures.len() >= self.config.failure_threshold as usize {
518 change = Some(transition(self.id, &mut inner, State::Open { since: now }));
519 inner.failures.clear();
520 }
521 }
522 State::Open { .. } => {}
525 },
526 Outcome::Neutral => {
527 if matches!(inner.state, State::HalfOpen) {
528 inner.half_open_calls = inner.half_open_calls.saturating_sub(1);
529 }
530 }
531 }
532 drop(inner);
533 emit(change);
534 }
535}
536
537#[cfg_attr(not(feature = "tracing"), allow(dead_code))]
549#[derive(Debug, Clone, Copy)]
550struct Transition {
551 breaker: u64,
552 seq: u64,
553 from: CircuitState,
554 to: CircuitState,
555}
556
557fn transition(breaker: u64, inner: &mut BreakerInner, to: State) -> Transition {
559 inner.transitions += 1;
560 let change = Transition {
561 breaker,
562 seq: inner.transitions,
563 from: CircuitState::from(&inner.state),
564 to: CircuitState::from(&to),
565 };
566 inner.state = to;
567 change
568}
569
570fn emit(change: Option<Transition>) {
582 #[cfg(feature = "tracing")]
583 if let Some(Transition {
584 breaker,
585 seq,
586 from,
587 to,
588 }) = change
589 {
590 if to == CircuitState::Open {
591 tracing::warn!(breaker, seq, from = ?from, to = ?to, "circuit breaker opened");
592 } else {
593 tracing::info!(breaker, seq, from = ?from, to = ?to, "circuit breaker transitioned");
594 }
595 }
596 #[cfg(not(feature = "tracing"))]
597 let _ = change;
598}
599
600#[derive(Debug)]
610struct ProbePermit<'a> {
611 breaker: &'a CircuitBreaker,
612 took_slot: bool,
614}
615
616impl ProbePermit<'_> {
617 fn complete(mut self, outcome: &Outcome) {
619 self.took_slot = false;
620 self.breaker.record(outcome);
621 }
622}
623
624impl Drop for ProbePermit<'_> {
625 fn drop(&mut self) {
626 if !self.took_slot {
627 return;
628 }
629 let mut inner = self.breaker.lock();
634 if matches!(inner.state, State::HalfOpen) {
635 inner.half_open_calls = inner.half_open_calls.saturating_sub(1);
636 }
637 }
638}
639
640#[derive(Debug)]
648pub(crate) struct ConcurrencyLimiter {
649 semaphore: tokio::sync::Semaphore,
650 waiting: std::sync::atomic::AtomicUsize,
652 config: BackpressureConfig,
653}
654
655struct QueueSlot<'a> {
659 waiting: &'a std::sync::atomic::AtomicUsize,
660}
661
662impl Drop for QueueSlot<'_> {
663 fn drop(&mut self) {
664 self.waiting
665 .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
666 }
667}
668
669impl ConcurrencyLimiter {
670 pub(crate) fn new(config: BackpressureConfig) -> Self {
671 Self {
672 semaphore: tokio::sync::Semaphore::new(
679 config
680 .max_concurrent
681 .clamp(1, tokio::sync::Semaphore::MAX_PERMITS),
682 ),
683 waiting: std::sync::atomic::AtomicUsize::new(0),
684 config,
685 }
686 }
687
688 async fn acquire(&self) -> Result<tokio::sync::SemaphorePermit<'_>, BackendError> {
696 use std::sync::atomic::Ordering;
697
698 if let Ok(permit) = self.semaphore.try_acquire() {
699 return Ok(permit);
700 }
701 if self.waiting.fetch_add(1, Ordering::AcqRel) >= self.config.max_queue {
702 self.waiting.fetch_sub(1, Ordering::AcqRel);
703 return Err(BackendError::backpressure(format!(
704 "backpressure: waiting queue is full (max_queue={}), call shed without reaching the backend",
705 self.config.max_queue
706 )));
707 }
708 let _slot = QueueSlot {
709 waiting: &self.waiting,
710 };
711 match tokio::time::timeout(self.config.acquire_timeout, self.semaphore.acquire()).await {
712 Ok(Ok(permit)) => Ok(permit),
713 Ok(Err(_closed)) => Err(BackendError::backpressure(
715 "backpressure: limiter unavailable, call shed without reaching the backend",
716 )),
717 Err(_elapsed) => Err(BackendError::backpressure(format!(
718 "backpressure: timed out waiting for a permit after {:?}, call shed without reaching the backend",
719 self.config.acquire_timeout
720 ))),
721 }
722 }
723}
724
725pub(crate) struct ReliableBackend {
738 inner: SharedBackend,
739 retry: Option<RetryPolicy>,
740 breaker: Option<Arc<CircuitBreaker>>,
744 limiter: Option<ConcurrencyLimiter>,
745 deadlines: Option<RetryDeadlines>,
746}
747
748impl ReliableBackend {
749 pub(crate) fn new(
750 inner: SharedBackend,
751 config: ReliabilityConfig,
752 deadlines: Option<RetryDeadlines>,
753 ) -> Self {
754 Self {
755 inner,
756 deadlines,
757 retry: config.retry.map(RetryPolicy::new),
758 breaker: config
759 .circuit_breaker
760 .map(|config| Arc::new(CircuitBreaker::new(config))),
761 limiter: config.backpressure.map(ConcurrencyLimiter::new),
762 }
763 }
764
765 async fn guarded<T, F, Fut>(&self, budget: Option<Duration>, f: F) -> Result<T, BackendError>
767 where
768 F: Fn() -> Fut,
769 Fut: Future<Output = Result<T, BackendError>>,
770 {
771 let _permit = match &self.limiter {
775 Some(limiter) => Some(limiter.acquire().await?),
776 None => None,
777 };
778 let permit = match &self.breaker {
779 Some(cb) => Some(cb.try_acquire()?),
780 None => None,
781 };
782 let result = match &self.retry {
783 Some(retry) => retry.execute(budget, f).await,
784 None => f().await,
785 };
786 if let Some(permit) = permit {
787 let outcome = match &result {
788 Ok(_) => Outcome::Success,
789 Err(e) if e.kind.is_retryable() => Outcome::Failure,
790 Err(_) => Outcome::Neutral,
791 };
792 permit.complete(&outcome);
793 }
794 result
795 }
796}
797
798#[cfg_attr(not(feature = "unsync"), async_trait)]
799#[cfg_attr(feature = "unsync", async_trait(?Send))]
800impl Backend for ReliableBackend {
801 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError> {
802 self.guarded(self.deadlines.map(|d| d.read), || self.inner.get(key))
803 .await
804 }
805
806 async fn get_with_freshness(
809 &self,
810 key: &str,
811 ) -> Result<Option<(Vec<u8>, Freshness)>, BackendError> {
812 self.guarded(self.deadlines.map(|d| d.read), || {
813 self.inner.get_with_freshness(key)
814 })
815 .await
816 }
817
818 async fn set(
819 &self,
820 key: &str,
821 value: Vec<u8>,
822 ttl: Option<Duration>,
823 ) -> Result<(), BackendError> {
824 self.guarded(self.deadlines.map(|d| d.write), || {
826 self.inner.set(key, value.clone(), ttl)
827 })
828 .await
829 }
830
831 async fn delete(&self, key: &str) -> Result<bool, BackendError> {
832 self.guarded(self.deadlines.map(|d| d.write), || self.inner.delete(key))
833 .await
834 }
835
836 async fn exists(&self, key: &str) -> Result<bool, BackendError> {
837 self.guarded(self.deadlines.map(|d| d.read), || self.inner.exists(key))
838 .await
839 }
840
841 async fn health(&self) -> Result<HealthStatus, BackendError> {
842 self.inner.health().await
843 }
844
845 fn as_lockable(&self) -> Option<&dyn LockableBackend> {
846 self.inner.as_lockable()
847 }
848}
849
850pub(crate) fn wrap_reliable(
853 inner: SharedBackend,
854 config: ReliabilityConfig,
855 deadlines: Option<RetryDeadlines>,
856) -> (SharedBackend, Option<Arc<CircuitBreaker>>) {
857 let reliable = ReliableBackend::new(inner, config, deadlines);
858 let breaker = reliable.breaker.clone();
859 #[cfg(not(feature = "unsync"))]
860 let shared: SharedBackend = std::sync::Arc::new(reliable);
861 #[cfg(feature = "unsync")]
862 let shared: SharedBackend = std::rc::Rc::new(reliable);
863 (shared, breaker)
864}
865
866#[cfg(test)]
869#[allow(clippy::expect_used)] mod tests {
871 use std::pin::Pin;
872 use std::task::{Context, Poll, Waker};
873
874 use super::*;
875 use crate::error::BackendErrorKind;
876
877 fn breaker(failure_threshold: u32, open_timeout: Duration) -> CircuitBreaker {
878 CircuitBreaker::new(CircuitBreakerConfig {
879 failure_threshold,
880 success_threshold: 2,
881 open_timeout,
882 half_open_max_calls: 2,
883 rolling_window: Duration::from_secs(60),
884 })
885 }
886
887 fn admit_and(cb: &CircuitBreaker, outcome: &Outcome) {
889 let permit = cb.try_acquire().expect("breaker admits the call");
890 permit.complete(outcome);
891 }
892
893 #[test]
894 fn breaker_opens_after_threshold_and_fails_fast() {
895 let cb = breaker(3, Duration::from_secs(60));
896 for _ in 0..3 {
897 admit_and(&cb, &Outcome::Failure);
898 }
899 assert_eq!(cb.state(), CircuitState::Open);
900 let err = cb.try_acquire().expect_err("open breaker fails fast");
901 assert_eq!(err.kind, BackendErrorKind::CircuitOpen);
902 assert!(!err.kind.is_retryable());
903 }
904
905 #[test]
906 fn breaker_ignores_permanent_errors() {
907 let cb = breaker(2, Duration::from_secs(60));
908 for _ in 0..10 {
909 admit_and(&cb, &Outcome::Neutral);
910 }
911 assert_eq!(cb.state(), CircuitState::Closed);
912 }
913
914 #[test]
915 fn breaker_half_open_recovers_on_successes() {
916 let cb = breaker(1, Duration::from_millis(0));
917 admit_and(&cb, &Outcome::Failure);
918 assert_eq!(cb.state(), CircuitState::HalfOpen);
920 for _ in 0..2 {
921 admit_and(&cb, &Outcome::Success);
922 }
923 assert_eq!(cb.state(), CircuitState::Closed);
924 }
925
926 #[test]
927 fn breaker_half_open_reopens_on_failure() {
928 let cb = breaker(1, Duration::from_millis(0));
929 admit_and(&cb, &Outcome::Failure);
930 assert_eq!(cb.state(), CircuitState::HalfOpen);
931 admit_and(&cb, &Outcome::Failure);
932 assert!(matches!(cb.lock().state, State::Open { .. }));
935 }
936
937 #[test]
938 fn breaker_half_open_slot_released_by_neutral_outcome() {
939 let cb = breaker(1, Duration::from_millis(0));
940 admit_and(&cb, &Outcome::Failure);
941 assert_eq!(cb.state(), CircuitState::HalfOpen);
942 admit_and(&cb, &Outcome::Neutral);
944 admit_and(&cb, &Outcome::Neutral);
945 let permit = cb
947 .try_acquire()
948 .expect("neutral outcomes release their probe slots");
949 permit.complete(&Outcome::Neutral);
950 }
951
952 #[test]
953 fn breaker_half_open_closes_when_success_threshold_exceeds_probe_cap() {
954 let cb = CircuitBreaker::new(CircuitBreakerConfig {
961 failure_threshold: 1,
962 success_threshold: 3,
963 open_timeout: Duration::from_millis(0),
964 half_open_max_calls: 1,
965 rolling_window: Duration::from_secs(60),
966 });
967 admit_and(&cb, &Outcome::Failure);
968 assert_eq!(cb.state(), CircuitState::HalfOpen);
969 for _ in 0..3 {
970 let permit = cb
971 .try_acquire()
972 .expect("a non-closing success must release its probe slot");
973 permit.complete(&Outcome::Success);
974 }
975 assert_eq!(cb.state(), CircuitState::Closed);
976 }
977
978 #[test]
979 fn breaker_dropped_permit_releases_probe_slot() {
980 let cb = breaker(1, Duration::from_millis(0));
985 admit_and(&cb, &Outcome::Failure);
986 assert_eq!(cb.state(), CircuitState::HalfOpen);
987 for _ in 0..2 {
988 let permit = cb.try_acquire().expect("half-open admits a probe");
989 drop(permit); }
991 let permit = cb
992 .try_acquire()
993 .expect("dropped permits release their probe slots");
994 permit.complete(&Outcome::Success);
995 }
996
997 #[test]
998 fn breaker_closed_permit_drop_does_not_touch_half_open_accounting() {
999 let cb = breaker(1, Duration::from_millis(0));
1003 let closed_permit = cb.try_acquire().expect("closed breaker admits calls");
1004 admit_and(&cb, &Outcome::Failure);
1007 assert_eq!(cb.state(), CircuitState::HalfOpen);
1008 let p1 = cb.try_acquire().expect("probe slot 1");
1009 let p2 = cb.try_acquire().expect("probe slot 2");
1010 drop(closed_permit); assert!(
1012 cb.try_acquire().is_err(),
1013 "probe cap must still be enforced after a closed-state permit drops"
1014 );
1015 p1.complete(&Outcome::Success);
1016 p2.complete(&Outcome::Success);
1017 assert_eq!(cb.state(), CircuitState::Closed);
1018 }
1019
1020 #[test]
1021 fn reliability_default_enables_backpressure_with_python_parity_defaults() {
1022 let config = ReliabilityConfig::default();
1023 let bp = config.backpressure.expect("backpressure is on by default");
1024 assert_eq!(bp.max_concurrent, 100);
1025 assert_eq!(bp.max_queue, 1000);
1026 assert_eq!(bp.acquire_timeout, Duration::from_millis(100));
1027 }
1028
1029 #[tokio::test]
1030 async fn limiter_clamps_zero_max_concurrent_to_one() {
1031 let limiter = ConcurrencyLimiter::new(BackpressureConfig {
1032 max_concurrent: 0,
1033 max_queue: 0,
1034 acquire_timeout: Duration::from_millis(10),
1035 });
1036 let permit = limiter
1037 .acquire()
1038 .await
1039 .expect("0 behaves as 1 — one permit exists");
1040 drop(permit);
1041 }
1042
1043 #[tokio::test]
1044 async fn limiter_clamps_huge_max_concurrent_instead_of_panicking() {
1045 let limiter = ConcurrencyLimiter::new(BackpressureConfig {
1048 max_concurrent: usize::MAX,
1049 max_queue: 0,
1050 acquire_timeout: Duration::from_millis(10),
1051 });
1052 let permit = limiter.acquire().await.expect("clamped limiter admits");
1053 drop(permit);
1054 }
1055
1056 #[tokio::test]
1057 async fn limiter_sheds_immediately_when_queue_disabled() {
1058 let limiter = ConcurrencyLimiter::new(BackpressureConfig {
1059 max_concurrent: 1,
1060 max_queue: 0,
1061 acquire_timeout: Duration::from_secs(5),
1062 });
1063 let _held = limiter.acquire().await.expect("first permit");
1064 let start = Instant::now();
1065 let err = limiter
1066 .acquire()
1067 .await
1068 .expect_err("saturated with no waiting queue");
1069 assert_eq!(err.kind, BackendErrorKind::Backpressure);
1070 assert!(!err.kind.is_retryable());
1071 assert!(
1072 start.elapsed() < Duration::from_millis(500),
1073 "queue-full sheds immediately, not after acquire_timeout"
1074 );
1075 }
1076
1077 fn poll_once<F: Future>(fut: Pin<&mut F>) -> Poll<F::Output> {
1082 fut.poll(&mut Context::from_waker(Waker::noop()))
1083 }
1084
1085 #[tokio::test]
1086 async fn limiter_waiting_slot_released_on_cancelled_wait() {
1087 let limiter = ConcurrencyLimiter::new(BackpressureConfig {
1092 max_concurrent: 1,
1093 max_queue: 1,
1094 acquire_timeout: Duration::from_millis(100),
1095 });
1096 let _held = limiter.acquire().await.expect("first permit");
1097 let mut cancelled = Box::pin(limiter.acquire());
1098 assert!(
1099 poll_once(cancelled.as_mut()).is_pending(),
1100 "waiter parks in the queue"
1101 );
1102 drop(cancelled); let mut next = Box::pin(limiter.acquire());
1105 assert!(
1106 poll_once(next.as_mut()).is_pending(),
1107 "next waiter must park in the queue — a queue-full shed means \
1108 the cancelled waiter leaked its slot"
1109 );
1110 let err = next
1111 .await
1112 .expect_err("permit never frees, waiter times out");
1113 assert_eq!(err.kind, BackendErrorKind::Backpressure);
1114 }
1115
1116 #[tokio::test]
1117 async fn limiter_sheds_queue_full_at_nonzero_boundary() {
1118 let limiter = ConcurrencyLimiter::new(BackpressureConfig {
1122 max_concurrent: 1,
1123 max_queue: 1,
1124 acquire_timeout: Duration::from_millis(200),
1125 });
1126 let _held = limiter.acquire().await.expect("first permit");
1127 let mut waiter = Box::pin(limiter.acquire());
1128 assert!(
1129 poll_once(waiter.as_mut()).is_pending(),
1130 "waiter parks in the queue"
1131 );
1132 let third = poll_once(Box::pin(limiter.acquire()).as_mut());
1135 assert!(
1136 matches!(&third, Poll::Ready(Err(err)) if err.kind == BackendErrorKind::Backpressure),
1137 "queue of 1 is full, so the third caller sheds on its first poll: {third:?}"
1138 );
1139 waiter
1140 .await
1141 .expect_err("the parked waiter itself times out");
1142 }
1143
1144 #[test]
1145 fn retry_delay_is_capped_and_jittered() {
1146 let policy = RetryPolicy::new(RetryConfig {
1147 max_attempts: 5,
1148 base_delay: Duration::from_millis(100),
1149 max_delay: Duration::from_millis(300),
1150 jitter: true,
1151 });
1152 for attempt in 0..10 {
1153 let d = policy.delay(attempt);
1154 assert!(d < Duration::from_millis(450), "attempt {attempt}: {d:?}");
1156 }
1157 let no_jitter = RetryPolicy::new(RetryConfig {
1158 jitter: false,
1159 ..RetryConfig::default()
1160 });
1161 assert_eq!(no_jitter.delay(0), Duration::from_millis(100));
1162 assert_eq!(no_jitter.delay(1), Duration::from_millis(200));
1163 assert_eq!(no_jitter.delay(20), Duration::from_secs(5));
1164 }
1165
1166 #[tokio::test]
1167 async fn a_quota_deny_is_not_retried() {
1168 let calls = AtomicU64::new(0);
1169 let policy = RetryPolicy::new(RetryConfig::default());
1170
1171 let err = policy
1172 .execute(None, || async {
1173 calls.fetch_add(1, Ordering::SeqCst);
1174 Err::<(), _>(BackendError::from_http_status(429, b"").with_quota_denied())
1175 })
1176 .await
1177 .expect_err("denied");
1178
1179 assert!(err.is_quota_denied());
1180 assert_eq!(calls.load(Ordering::SeqCst), 1);
1181 }
1182}
1183
1184#[cfg(all(test, feature = "cachekitio"))]
1189#[allow(clippy::expect_used)] mod deadline_tests {
1191 use std::collections::HashMap;
1192 use std::sync::atomic::{AtomicU32, Ordering};
1193 use std::sync::Arc;
1194 use std::time::Duration;
1195
1196 use async_trait::async_trait;
1197
1198 use super::{CircuitState, ReliabilityConfig, RetryConfig, CACHEKITIO_DEADLINES};
1199 use crate::backend::{Backend, HealthStatus};
1200 use crate::client::SharedBackend;
1201 use crate::error::{BackendError, BackendErrorKind};
1202 use crate::{CacheKit, CachekitError};
1203
1204 #[derive(Debug, Clone)]
1207 struct SlowBackend {
1208 latency: Option<Duration>,
1209 kind: BackendErrorKind,
1210 calls: Arc<AtomicU32>,
1211 }
1212
1213 impl SlowBackend {
1214 fn new_with_handle(
1215 latency: Option<Duration>,
1216 kind: BackendErrorKind,
1217 ) -> (SharedBackend, Self) {
1218 let backend = Self {
1219 latency,
1220 kind,
1221 calls: Arc::new(AtomicU32::new(0)),
1222 };
1223 let handle = backend.clone();
1224 #[cfg(not(feature = "unsync"))]
1225 let shared: SharedBackend = Arc::new(backend);
1226 #[cfg(feature = "unsync")]
1227 let shared: SharedBackend = std::rc::Rc::new(backend);
1228 (shared, handle)
1229 }
1230
1231 fn calls(&self) -> u32 {
1232 self.calls.load(Ordering::SeqCst)
1233 }
1234
1235 async fn op<T>(&self) -> Result<T, BackendError> {
1236 self.calls.fetch_add(1, Ordering::SeqCst);
1237 match self.latency {
1238 Some(latency) => tokio::time::sleep(latency).await,
1239 None => std::future::pending().await,
1240 }
1241 Err(BackendError {
1242 kind: self.kind.clone(),
1243 message: "scripted failure".to_owned(),
1244 source: None,
1245 })
1246 }
1247 }
1248
1249 #[cfg_attr(not(feature = "unsync"), async_trait)]
1250 #[cfg_attr(feature = "unsync", async_trait(?Send))]
1251 impl Backend for SlowBackend {
1252 async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>, BackendError> {
1253 self.op().await
1254 }
1255
1256 async fn set(
1257 &self,
1258 _key: &str,
1259 _value: Vec<u8>,
1260 _ttl: Option<Duration>,
1261 ) -> Result<(), BackendError> {
1262 self.op().await
1263 }
1264
1265 async fn delete(&self, _key: &str) -> Result<bool, BackendError> {
1266 self.op().await
1267 }
1268
1269 async fn exists(&self, _key: &str) -> Result<bool, BackendError> {
1270 self.op().await
1271 }
1272
1273 async fn health(&self) -> Result<HealthStatus, BackendError> {
1274 Ok(HealthStatus {
1275 is_healthy: true,
1276 latency_ms: 0.0,
1277 backend_type: "slow".to_owned(),
1278 details: HashMap::new(),
1279 })
1280 }
1281 }
1282
1283 fn default_stack(backend: SharedBackend) -> CacheKit {
1286 let mut builder = CacheKit::builder()
1287 .backend(backend)
1288 .reliability(ReliabilityConfig {
1289 retry: Some(RetryConfig {
1290 jitter: false,
1291 ..RetryConfig::default()
1292 }),
1293 ..ReliabilityConfig::default()
1294 })
1295 .no_l1();
1296 builder.retry_deadlines = Some(CACHEKITIO_DEADLINES);
1297 builder.build().expect("client builds")
1298 }
1299
1300 fn backend_kind(err: &CachekitError) -> Option<&BackendErrorKind> {
1301 match err {
1302 CachekitError::Backend(b) => Some(&b.kind),
1303 _ => None,
1304 }
1305 }
1306
1307 async fn timed<T>(op: impl std::future::Future<Output = T>) -> (T, Duration) {
1309 let start = tokio::time::Instant::now();
1310 let out = op.await;
1311 (out, start.elapsed())
1312 }
1313
1314 #[tokio::test(start_paused = true)]
1315 async fn a_stalled_get_fails_at_the_5_s_read_deadline() {
1316 let (backend, handle) = SlowBackend::new_with_handle(None, BackendErrorKind::Timeout);
1317 let client = default_stack(backend);
1318
1319 let (result, elapsed) = timed(client.get::<u32>("k")).await;
1320
1321 let err = result.expect_err("stalled");
1322 assert_eq!(backend_kind(&err), Some(&BackendErrorKind::Timeout));
1323 assert_eq!(elapsed, Duration::from_secs(5));
1324 assert_eq!(handle.calls(), 1);
1325 }
1326
1327 #[tokio::test(start_paused = true)]
1328 async fn a_stalled_set_fails_at_the_10_s_write_deadline() {
1329 let (backend, handle) = SlowBackend::new_with_handle(None, BackendErrorKind::Timeout);
1330 let client = default_stack(backend);
1331
1332 let (result, elapsed) = timed(client.set("k", &7u32)).await;
1333
1334 let err = result.expect_err("stalled");
1335 assert_eq!(backend_kind(&err), Some(&BackendErrorKind::Timeout));
1336 assert_eq!(elapsed, Duration::from_secs(10));
1337 assert_eq!(handle.calls(), 1);
1338 }
1339
1340 #[tokio::test(start_paused = true)]
1343 async fn a_30_s_attempt_timeout_is_cut_to_the_deadline() {
1344 let (backend, handle) =
1345 SlowBackend::new_with_handle(Some(Duration::from_secs(30)), BackendErrorKind::Timeout);
1346 let client = default_stack(backend);
1347
1348 let (result, elapsed) = timed(client.get::<u32>("k")).await;
1349
1350 result.expect_err("timed out");
1351 assert_eq!(elapsed, Duration::from_secs(5));
1352 assert_eq!(handle.calls(), 1);
1353 }
1354
1355 #[tokio::test(start_paused = true)]
1356 async fn fast_failures_still_get_every_attempt() {
1357 let (backend, handle) =
1358 SlowBackend::new_with_handle(Some(Duration::ZERO), BackendErrorKind::Transient);
1359 let client = default_stack(backend);
1360
1361 let (result, elapsed) = timed(client.get::<u32>("k")).await;
1362
1363 let err = result.expect_err("always fails");
1364 assert_eq!(backend_kind(&err), Some(&BackendErrorKind::Transient));
1365 assert_eq!(handle.calls(), 3);
1366 assert_eq!(elapsed, Duration::from_millis(300), "100 + 200 ms backoff");
1367 }
1368
1369 #[tokio::test(start_paused = true)]
1370 async fn no_backoff_starts_past_the_deadline() {
1371 let (backend, handle) = SlowBackend::new_with_handle(
1374 Some(Duration::from_millis(4950)),
1375 BackendErrorKind::Transient,
1376 );
1377 let client = default_stack(backend);
1378
1379 let (result, elapsed) = timed(client.get::<u32>("k")).await;
1380
1381 let err = result.expect_err("fails");
1382 assert_eq!(backend_kind(&err), Some(&BackendErrorKind::Transient));
1383 assert_eq!(handle.calls(), 1);
1384 assert_eq!(elapsed, Duration::from_millis(4950));
1385 }
1386
1387 #[tokio::test(start_paused = true)]
1388 async fn serial_stalled_gets_open_the_breaker_inside_its_window() {
1389 let (backend, handle) = SlowBackend::new_with_handle(None, BackendErrorKind::Timeout);
1390 let client = default_stack(backend);
1391
1392 for _ in 0..5 {
1394 client.get::<u32>("k").await.expect_err("stalled");
1395 }
1396 assert_eq!(client.circuit_state(), Some(CircuitState::Open));
1397
1398 let calls = handle.calls();
1399 let err = client.get::<u32>("k").await.expect_err("open");
1400 assert_eq!(backend_kind(&err), Some(&BackendErrorKind::CircuitOpen));
1401 assert_eq!(handle.calls(), calls, "an open breaker sends nothing");
1402 }
1403}