1use core::{
2 fmt,
3 sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
4 time::Duration,
5};
6use std::sync::Arc;
7
8use moka::{policy::EvictionPolicy, sync::Cache};
9use rama_core::{
10 Layer, Service,
11 error::{BoxError, BoxErrorExt as _},
12 extensions::ExtensionsRef,
13 telemetry::tracing,
14};
15use rama_utils::{macros::define_inner_service_accessors, time::now_monotonic_nanos};
16
17use crate::client::{
18 ConnectionError, ConnectionErrorDomain, ConnectionErrorKind, ConnectorService,
19 EstablishedClientConnection,
20};
21use crate::{
22 AuthorityInputExt, Protocol, ProtocolInputExt, address::HostWithPort, user::ProxyCredential,
23};
24
25use super::ProxyRoute;
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub enum ProxyRouteFailureCacheScope {
31 #[default]
33 PerDestination,
34 PerProxy,
36}
37
38#[derive(Debug, Clone)]
40#[non_exhaustive]
41pub struct ProxyRouteFailureCacheConfig {
42 pub initial_backoff: Duration,
44 pub max_backoff: Duration,
46 pub probe_lease: Duration,
48 pub max_entries: u64,
50 pub scope: ProxyRouteFailureCacheScope,
53}
54
55impl Default for ProxyRouteFailureCacheConfig {
56 fn default() -> Self {
57 Self {
58 initial_backoff: Duration::from_secs(60),
59 max_backoff: Duration::from_mins(30),
60 probe_lease: Duration::from_secs(30),
61 max_entries: 1_024,
62 scope: ProxyRouteFailureCacheScope::PerDestination,
63 }
64 }
65}
66
67impl ProxyRouteFailureCacheConfig {
68 fn validate(&self) -> Result<(), BoxError> {
69 if self.initial_backoff.is_zero() {
70 return Err(BoxError::from_static_str(
71 "proxy route failure cache initial backoff must be non-zero",
72 ));
73 }
74 if self.max_backoff < self.initial_backoff {
75 return Err(BoxError::from_static_str(
76 "proxy route failure cache max backoff must not be smaller than its initial backoff",
77 ));
78 }
79 if self.probe_lease.is_zero() {
80 return Err(BoxError::from_static_str(
81 "proxy route failure cache probe lease must be non-zero",
82 ));
83 }
84 if self.max_entries == 0 {
85 return Err(BoxError::from_static_str(
86 "proxy route failure cache capacity must be non-zero",
87 ));
88 }
89 Ok(())
90 }
91
92 fn backoff(&self, previous_failures: u32) -> Duration {
93 let multiplier = 1u32
94 .checked_shl(previous_failures.min(31))
95 .unwrap_or(u32::MAX);
96 self.initial_backoff
97 .saturating_mul(multiplier)
98 .min(self.max_backoff)
99 }
100}
101
102#[derive(Clone, PartialEq, Eq, Hash)]
103struct FailureCacheKey {
104 protocol: Option<Protocol>,
105 proxy: HostWithPort,
106 basic_username: Option<String>,
107 bearer_credential: bool,
108 destination_protocol: Option<Protocol>,
109 destination: Option<HostWithPort>,
110}
111
112type SharedFailureCacheKey = Arc<FailureCacheKey>;
113
114#[derive(Default)]
115struct FailureEntry {
116 blocked_until: AtomicU64,
117 probe_until: AtomicU64,
118 failure_count: AtomicU32,
119 active_attempts: AtomicU32,
120 succeeded: AtomicBool,
121}
122
123impl FailureEntry {
124 fn mark_live(&self) {
125 self.succeeded.store(true, Ordering::Release);
129 self.blocked_until.store(0, Ordering::Release);
130 self.probe_until.store(0, Ordering::Release);
131 self.failure_count.store(0, Ordering::Release);
132 }
133}
134
135struct AttemptPermit {
136 entries: Arc<Cache<SharedFailureCacheKey, Arc<FailureEntry>>>,
137 key: SharedFailureCacheKey,
138 entry: Arc<FailureEntry>,
139 started_time: u64,
140 probe_lease: Option<u64>,
141 remove_on_drop: bool,
142}
143
144impl AttemptPermit {
145 fn release_probe(&mut self) {
146 if let Some(lease) = self.probe_lease.take() {
147 let _release_result = self.entry.probe_until.compare_exchange(
148 lease,
149 0,
150 Ordering::AcqRel,
151 Ordering::Acquire,
152 );
153 }
154 }
155
156 fn mark_live(&mut self) {
157 self.entry.mark_live();
158 self.release_probe();
159 self.entries.invalidate(&self.key);
160 self.remove_on_drop = false;
161 }
162}
163
164impl Drop for AttemptPermit {
165 fn drop(&mut self) {
166 self.release_probe();
167 let previous_attempts = self.entry.active_attempts.fetch_sub(1, Ordering::AcqRel);
168 debug_assert!(previous_attempts > 0);
169 if self.remove_on_drop
170 && previous_attempts == 1
171 && self.entry.failure_count.load(Ordering::Acquire) == 0
172 {
173 self.entries.invalidate(&self.key);
174 }
175 }
176}
177
178enum CacheDecision {
179 Attempt(AttemptPermit),
180 Blocked(Duration),
181}
182
183#[derive(Clone)]
196pub struct ProxyRouteFailureCache {
197 entries: Arc<Cache<SharedFailureCacheKey, Arc<FailureEntry>>>,
198 config: Arc<ProxyRouteFailureCacheConfig>,
199}
200
201impl fmt::Debug for ProxyRouteFailureCache {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.debug_struct("ProxyRouteFailureCache")
204 .field("config", &self.config)
205 .field("entry_count", &self.entries.entry_count())
206 .finish()
207 }
208}
209
210impl Default for ProxyRouteFailureCache {
211 fn default() -> Self {
212 Self::build(ProxyRouteFailureCacheConfig::default())
213 }
214}
215
216impl ProxyRouteFailureCache {
217 pub fn try_new(config: ProxyRouteFailureCacheConfig) -> Result<Self, BoxError> {
219 config.validate()?;
220 Ok(Self::build(config))
221 }
222
223 fn build(config: ProxyRouteFailureCacheConfig) -> Self {
224 let idle = config.max_backoff.saturating_mul(2);
225 let entries = Arc::new(
226 Cache::builder()
227 .max_capacity(config.max_entries)
228 .initial_capacity(config.max_entries.min(128) as usize)
229 .eviction_policy(EvictionPolicy::lru())
230 .time_to_idle(idle)
231 .build(),
232 );
233 Self {
234 entries,
235 config: Arc::new(config),
236 }
237 }
238
239 fn begin<Input>(&self, input: &Input) -> Option<CacheDecision>
240 where
241 Input: AuthorityInputExt + ExtensionsRef + ProtocolInputExt,
242 {
243 let route = input.extensions().get_arc::<ProxyRoute>()?;
244 let ProxyRoute::Proxy(proxy) = route.as_ref() else {
245 return None;
246 };
247 let (destination_protocol, destination) = match self.config.scope {
248 ProxyRouteFailureCacheScope::PerDestination => (
249 input.protocol(),
250 Some(
251 input
252 .authority()?
253 .into_host_with_port(input.protocol_default_port())?,
254 ),
255 ),
256 ProxyRouteFailureCacheScope::PerProxy => (None, None),
257 };
258 let (basic_username, bearer_credential) = match proxy.credential.as_ref() {
259 Some(ProxyCredential::Basic(basic)) => (Some(basic.username()), false),
260 Some(ProxyCredential::Bearer(_)) => (None, true),
261 None => (None, false),
262 };
263 let key = Arc::new(FailureCacheKey {
264 protocol: proxy.protocol.clone(),
265 proxy: proxy.address.clone(),
266 basic_username: basic_username.map(ToOwned::to_owned),
267 bearer_credential,
268 destination_protocol: destination_protocol.cloned(),
269 destination,
270 });
271 let entry = self
276 .entries
277 .get_with(key.clone(), || Arc::new(FailureEntry::default()));
278
279 let failure_count = entry.failure_count.load(Ordering::Acquire);
280 let blocked_until = entry.blocked_until.load(Ordering::Acquire);
281 if failure_count == 0 && blocked_until == 0 {
282 entry.active_attempts.fetch_add(1, Ordering::AcqRel);
283 return Some(CacheDecision::Attempt(AttemptPermit {
284 entries: self.entries.clone(),
285 key,
286 started_time: now_monotonic_nanos(),
287 entry,
288 probe_lease: None,
289 remove_on_drop: true,
290 }));
291 }
292
293 let mut now = now_monotonic_nanos();
294 if let Some(remaining) = remaining_duration(blocked_until, now) {
295 return Some(CacheDecision::Blocked(remaining));
296 }
297
298 let lease_duration = duration_nanos(self.config.probe_lease);
299 loop {
300 now = now_monotonic_nanos();
301 let probe_until = entry.probe_until.load(Ordering::Acquire);
302 if let Some(remaining) = remaining_duration(probe_until, now) {
303 return Some(CacheDecision::Blocked(remaining));
304 }
305 let new_probe_until = now.saturating_add(lease_duration);
306 if entry
307 .probe_until
308 .compare_exchange(
309 probe_until,
310 new_probe_until,
311 Ordering::AcqRel,
312 Ordering::Acquire,
313 )
314 .is_ok()
315 {
316 entry.active_attempts.fetch_add(1, Ordering::AcqRel);
317 return Some(CacheDecision::Attempt(AttemptPermit {
318 entries: self.entries.clone(),
319 key,
320 started_time: now_monotonic_nanos(),
321 entry,
322 probe_lease: Some(new_probe_until),
323 remove_on_drop: false,
324 }));
325 }
326 }
327 }
328
329 fn mark_failure(&self, permit: &mut AttemptPermit) {
330 let entry = &permit.entry;
331 let started_time = permit.started_time;
332 loop {
333 if entry.succeeded.load(Ordering::Acquire) {
334 break;
335 }
336
337 let current_deadline = entry.blocked_until.load(Ordering::Acquire);
338 if current_deadline > started_time {
339 break;
340 }
341
342 let previous_failures = entry.failure_count.load(Ordering::Relaxed);
343 let new_deadline = now_monotonic_nanos()
344 .saturating_add(duration_nanos(self.config.backoff(previous_failures)));
345 if entry
346 .blocked_until
347 .compare_exchange(
348 current_deadline,
349 new_deadline,
350 Ordering::AcqRel,
351 Ordering::Acquire,
352 )
353 .is_ok()
354 {
355 entry
356 .failure_count
357 .store(previous_failures.saturating_add(1), Ordering::Release);
358 if entry.succeeded.load(Ordering::Acquire) {
359 entry.mark_live();
360 }
361 break;
362 }
363 }
364 permit.remove_on_drop = false;
365 permit.release_probe();
366 }
367
368 #[must_use]
370 pub fn config(&self) -> &ProxyRouteFailureCacheConfig {
371 &self.config
372 }
373
374 #[must_use]
376 pub fn entry_count(&self) -> u64 {
377 self.entries.entry_count()
378 }
379
380 pub fn invalidate_all(&self) {
382 self.entries.invalidate_all();
383 }
384}
385
386fn duration_nanos(duration: Duration) -> u64 {
387 duration.as_nanos().try_into().unwrap_or(u64::MAX)
388}
389
390fn remaining_duration(deadline: u64, now: u64) -> Option<Duration> {
391 deadline
392 .checked_sub(now)
393 .filter(|remaining| *remaining != 0)
394 .map(Duration::from_nanos)
395}
396
397fn should_cache_failure(error: &ConnectionError) -> bool {
398 error.domain() == ConnectionErrorDomain::Transport
399 && matches!(
400 error.kind(),
401 ConnectionErrorKind::Unavailable
402 | ConnectionErrorKind::Timeout
403 | ConnectionErrorKind::Protocol
404 )
405}
406
407#[derive(Debug)]
409pub struct ProxyRouteFailureCachedError {
410 retry_after: Duration,
411}
412
413impl ProxyRouteFailureCachedError {
414 #[must_use]
416 pub const fn retry_after(&self) -> Duration {
417 self.retry_after
418 }
419}
420
421impl fmt::Display for ProxyRouteFailureCachedError {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 write!(
424 f,
425 "proxy route is temporarily blocked after a connection failure (retry in {:?})",
426 self.retry_after
427 )
428 }
429}
430
431impl core::error::Error for ProxyRouteFailureCachedError {}
432
433#[derive(Debug, Clone)]
443pub struct ProxyRouteFailureCacheConnector<S> {
444 inner: S,
445 cache: ProxyRouteFailureCache,
446}
447
448impl<S> ProxyRouteFailureCacheConnector<S> {
449 #[must_use]
451 pub const fn new(inner: S, cache: ProxyRouteFailureCache) -> Self {
452 Self { inner, cache }
453 }
454
455 #[must_use]
457 pub const fn cache(&self) -> &ProxyRouteFailureCache {
458 &self.cache
459 }
460
461 define_inner_service_accessors!();
462}
463
464impl<S, Input> Service<Input> for ProxyRouteFailureCacheConnector<S>
465where
466 S: ConnectorService<Input>,
467 Input: AuthorityInputExt + ExtensionsRef + ProtocolInputExt + Send + 'static,
468{
469 type Output = EstablishedClientConnection<S::Connection, Input>;
470 type Error = ConnectionError;
471
472 async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
473 let cache = &self.cache;
474 let mut permit = match cache.begin(&input) {
475 None => return self.inner.connect(input).await,
476 Some(CacheDecision::Attempt(permit)) => permit,
477 Some(CacheDecision::Blocked(retry_after)) => {
478 tracing::debug!(?retry_after, "skip temporarily failing proxy route",);
479 return Err(ConnectionError::transport(
480 ProxyRouteFailureCachedError { retry_after },
481 ConnectionErrorKind::Unavailable,
482 ));
483 }
484 };
485
486 match self.inner.connect(input).await {
487 Ok(established) => {
488 permit.mark_live();
489 Ok(established)
490 }
491 Err(error) if should_cache_failure(&error) => {
492 cache.mark_failure(&mut permit);
493 Err(error)
494 }
495 Err(error) => {
496 permit.mark_live();
497 Err(error)
498 }
499 }
500 }
501}
502
503#[derive(Debug, Clone)]
505pub struct ProxyRouteFailureCacheLayer {
506 cache: ProxyRouteFailureCache,
507}
508
509impl ProxyRouteFailureCacheLayer {
510 #[must_use]
512 pub const fn new(cache: ProxyRouteFailureCache) -> Self {
513 Self { cache }
514 }
515
516 #[must_use]
518 pub const fn cache(&self) -> &ProxyRouteFailureCache {
519 &self.cache
520 }
521}
522
523impl<S> Layer<S> for ProxyRouteFailureCacheLayer {
524 type Service = ProxyRouteFailureCacheConnector<S>;
525
526 fn layer(&self, inner: S) -> Self::Service {
527 ProxyRouteFailureCacheConnector::new(inner, self.cache.clone())
528 }
529
530 fn into_layer(self, inner: S) -> Self::Service {
531 ProxyRouteFailureCacheConnector::new(inner, self.cache)
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use core::future::Future;
538 use core::sync::atomic::{AtomicUsize, Ordering};
539 use std::{sync::Arc, time::Duration};
540
541 use rama_core::{ServiceInput, service::service_fn};
542 use tokio::sync::{Barrier, Notify};
543
544 use crate::client::{ConnectRequest, ProxyRoute, ProxyRoutes};
545
546 use super::*;
547
548 fn cache(scope: ProxyRouteFailureCacheScope) -> ProxyRouteFailureCache {
549 ProxyRouteFailureCache::try_new(ProxyRouteFailureCacheConfig {
550 initial_backoff: Duration::from_millis(20),
551 max_backoff: Duration::from_millis(80),
552 probe_lease: Duration::from_secs(1),
553 max_entries: 32,
554 scope,
555 })
556 .unwrap()
557 }
558
559 fn proxy(username: Option<&str>) -> ProxyRoute {
560 let address = match username {
561 Some(username) => format!("http://{username}:secret@proxy.example:8080"),
562 None => "http://proxy.example:8080".to_owned(),
563 };
564 ProxyRoute::Proxy(address.parse().unwrap())
565 }
566
567 fn request(destination: &str, route: ProxyRoute) -> ConnectRequest {
568 let request = ConnectRequest::new(destination.parse().unwrap());
569 request.extensions.insert(route);
570 request
571 }
572
573 fn unavailable() -> ConnectionError {
574 ConnectionError::transport(
575 BoxError::from_static_str("proxy unavailable"),
576 ConnectionErrorKind::Unavailable,
577 )
578 }
579
580 async fn within_test_timeout<F: Future>(future: F) -> F::Output {
581 tokio::time::timeout(Duration::from_secs(5), future)
582 .await
583 .expect("concurrent failure-cache test operation should complete")
584 }
585
586 fn begin_attempt(
587 failure_cache: &ProxyRouteFailureCache,
588 request: &ConnectRequest,
589 ) -> AttemptPermit {
590 match failure_cache.begin(request) {
591 Some(CacheDecision::Attempt(permit)) => permit,
592 Some(CacheDecision::Blocked(_)) => panic!("route was unexpectedly blocked"),
593 None => panic!("proxy route was unexpectedly ignored"),
594 }
595 }
596
597 #[tokio::test]
598 async fn repeated_failure_is_suppressed_for_same_destination() {
599 let attempts = Arc::new(AtomicUsize::new(0));
600 let inner = service_fn({
601 let attempts = attempts.clone();
602 move |_input: ConnectRequest| {
603 attempts.fetch_add(1, Ordering::SeqCst);
604 async {
605 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
606 unavailable(),
607 )
608 }
609 }
610 });
611 let connector = ProxyRouteFailureCacheConnector::new(
612 inner,
613 cache(ProxyRouteFailureCacheScope::PerDestination),
614 );
615
616 let _first_error = connector
617 .serve(request("one.example:443", proxy(None)))
618 .await
619 .unwrap_err();
620 let error = connector
621 .serve(request("one.example:443", proxy(None)))
622 .await
623 .unwrap_err();
624
625 assert_eq!(attempts.load(Ordering::SeqCst), 1);
626 assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
627 assert_eq!(error.kind(), ConnectionErrorKind::Unavailable);
628 assert!(
629 error
630 .get_ref()
631 .downcast_ref::<ProxyRouteFailureCachedError>()
632 .is_some()
633 );
634 }
635
636 #[tokio::test]
637 async fn cached_error_does_not_expose_route_or_credentials() {
638 let inner = service_fn(|_input: ConnectRequest| async {
639 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(unavailable())
640 });
641 let connector = ProxyRouteFailureCacheConnector::new(
642 inner,
643 cache(ProxyRouteFailureCacheScope::PerDestination),
644 );
645
646 let _first_error = connector
647 .serve(request("one.example:443", proxy(Some("alice"))))
648 .await
649 .unwrap_err();
650 let error = connector
651 .serve(request("one.example:443", proxy(Some("alice"))))
652 .await
653 .unwrap_err();
654 let rendered = format!("{error:?} {error}");
655
656 assert!(rendered.contains("temporarily blocked"));
657 assert!(!rendered.contains("alice"));
658 assert!(!rendered.contains("secret"));
659 assert!(!rendered.contains("proxy.example"));
660 }
661
662 #[tokio::test]
663 async fn timeout_and_protocol_failures_are_cached() {
664 for kind in [ConnectionErrorKind::Timeout, ConnectionErrorKind::Protocol] {
665 let attempts = Arc::new(AtomicUsize::new(0));
666 let inner = service_fn({
667 let attempts = attempts.clone();
668 move |_input: ConnectRequest| {
669 attempts.fetch_add(1, Ordering::SeqCst);
670 async move {
671 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
672 ConnectionError::transport(
673 BoxError::from_static_str("cacheable proxy failure"),
674 kind,
675 ),
676 )
677 }
678 }
679 });
680 let connector = ProxyRouteFailureCacheConnector::new(
681 inner,
682 cache(ProxyRouteFailureCacheScope::PerDestination),
683 );
684
685 let _first_error = connector
686 .serve(request("one.example:443", proxy(None)))
687 .await
688 .unwrap_err();
689 let cached = connector
690 .serve(request("one.example:443", proxy(None)))
691 .await
692 .unwrap_err();
693
694 assert_eq!(attempts.load(Ordering::SeqCst), 1, "{kind}");
695 assert!(
696 cached
697 .get_ref()
698 .downcast_ref::<ProxyRouteFailureCachedError>()
699 .is_some(),
700 "{kind}"
701 );
702 }
703 }
704
705 #[tokio::test(start_paused = true)]
706 async fn first_failure_is_cached_when_monotonic_time_is_zero() {
707 let attempts = Arc::new(AtomicUsize::new(0));
708 let inner = service_fn({
709 let attempts = attempts.clone();
710 move |_input: ConnectRequest| {
711 attempts.fetch_add(1, Ordering::SeqCst);
712 async {
713 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
714 unavailable(),
715 )
716 }
717 }
718 });
719 let connector = ProxyRouteFailureCacheConnector::new(
720 inner,
721 cache(ProxyRouteFailureCacheScope::PerDestination),
722 );
723
724 let _first_error = connector
725 .serve(request("one.example:443", proxy(None)))
726 .await
727 .unwrap_err();
728 let second_error = connector
729 .serve(request("one.example:443", proxy(None)))
730 .await
731 .unwrap_err();
732
733 assert_eq!(attempts.load(Ordering::SeqCst), 1);
734 assert!(
735 second_error
736 .get_ref()
737 .downcast_ref::<ProxyRouteFailureCachedError>()
738 .is_some()
739 );
740 }
741
742 #[tokio::test]
743 async fn per_destination_scope_does_not_poison_another_target() {
744 let attempts = Arc::new(AtomicUsize::new(0));
745 let inner = service_fn({
746 let attempts = attempts.clone();
747 move |_input: ConnectRequest| {
748 attempts.fetch_add(1, Ordering::SeqCst);
749 async {
750 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
751 unavailable(),
752 )
753 }
754 }
755 });
756 let connector = ProxyRouteFailureCacheConnector::new(
757 inner,
758 cache(ProxyRouteFailureCacheScope::PerDestination),
759 );
760
761 let _first_error = connector
762 .serve(request("one.example:443", proxy(None)))
763 .await
764 .unwrap_err();
765 let _second_error = connector
766 .serve(request("two.example:443", proxy(None)))
767 .await
768 .unwrap_err();
769
770 assert_eq!(attempts.load(Ordering::SeqCst), 2);
771 }
772
773 #[tokio::test]
774 async fn per_destination_scope_distinguishes_application_protocol() {
775 let attempts = Arc::new(AtomicUsize::new(0));
776 let inner = service_fn({
777 let attempts = attempts.clone();
778 move |_input: ConnectRequest| {
779 attempts.fetch_add(1, Ordering::SeqCst);
780 async {
781 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
782 unavailable(),
783 )
784 }
785 }
786 });
787 let connector = ProxyRouteFailureCacheConnector::new(
788 inner,
789 cache(ProxyRouteFailureCacheScope::PerDestination),
790 );
791
792 let _http_error = connector
793 .serve(
794 request("one.example:443", proxy(None)).with_application_protocol(Protocol::HTTP),
795 )
796 .await
797 .unwrap_err();
798 let _https_error = connector
799 .serve(
800 request("one.example:443", proxy(None)).with_application_protocol(Protocol::HTTPS),
801 )
802 .await
803 .unwrap_err();
804
805 assert_eq!(attempts.load(Ordering::SeqCst), 2);
806 }
807
808 #[tokio::test]
809 async fn per_proxy_scope_shares_failure_across_targets() {
810 let attempts = Arc::new(AtomicUsize::new(0));
811 let inner = service_fn({
812 let attempts = attempts.clone();
813 move |_input: ConnectRequest| {
814 attempts.fetch_add(1, Ordering::SeqCst);
815 async {
816 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
817 unavailable(),
818 )
819 }
820 }
821 });
822 let connector = ProxyRouteFailureCacheConnector::new(
823 inner,
824 cache(ProxyRouteFailureCacheScope::PerProxy),
825 );
826
827 let _first_error = connector
828 .serve(request("one.example:443", proxy(None)))
829 .await
830 .unwrap_err();
831 let _second_error = connector
832 .serve(request("two.example:443", proxy(None)))
833 .await
834 .unwrap_err();
835
836 assert_eq!(attempts.load(Ordering::SeqCst), 1);
837 }
838
839 #[tokio::test]
840 async fn routing_usernames_have_independent_failure_state() {
841 let attempts = Arc::new(AtomicUsize::new(0));
842 let inner = service_fn({
843 let attempts = attempts.clone();
844 move |_input: ConnectRequest| {
845 attempts.fetch_add(1, Ordering::SeqCst);
846 async {
847 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
848 unavailable(),
849 )
850 }
851 }
852 });
853 let connector = ProxyRouteFailureCacheConnector::new(
854 inner,
855 cache(ProxyRouteFailureCacheScope::PerDestination),
856 );
857
858 let _alice_error = connector
859 .serve(request("one.example:443", proxy(Some("alice"))))
860 .await
861 .unwrap_err();
862 let _bob_error = connector
863 .serve(request("one.example:443", proxy(Some("bob"))))
864 .await
865 .unwrap_err();
866
867 assert_eq!(attempts.load(Ordering::SeqCst), 2);
868 }
869
870 #[tokio::test]
871 async fn direct_routes_are_never_cached() {
872 let attempts = Arc::new(AtomicUsize::new(0));
873 let inner = service_fn({
874 let attempts = attempts.clone();
875 move |_input: ConnectRequest| {
876 attempts.fetch_add(1, Ordering::SeqCst);
877 async {
878 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
879 unavailable(),
880 )
881 }
882 }
883 });
884 let connector = ProxyRouteFailureCacheConnector::new(
885 inner,
886 cache(ProxyRouteFailureCacheScope::PerDestination),
887 );
888
889 for _ in 0..2 {
890 let _error = connector
891 .serve(request("one.example:443", ProxyRoute::Direct))
892 .await
893 .unwrap_err();
894 }
895
896 assert_eq!(attempts.load(Ordering::SeqCst), 2);
897 }
898
899 #[tokio::test]
900 async fn plural_routes_are_never_used_as_cache_keys() {
901 let attempts = Arc::new(AtomicUsize::new(0));
902 let inner = service_fn({
903 let attempts = attempts.clone();
904 move |_input: ConnectRequest| {
905 attempts.fetch_add(1, Ordering::SeqCst);
906 async {
907 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
908 unavailable(),
909 )
910 }
911 }
912 });
913 let connector = ProxyRouteFailureCacheConnector::new(
914 inner,
915 cache(ProxyRouteFailureCacheScope::PerDestination),
916 );
917
918 for _ in 0..2 {
919 let request = ConnectRequest::new("one.example:443".parse().unwrap());
920 request.extensions.insert(ProxyRoutes::new([proxy(None)]));
921 let _error = connector.serve(request).await.unwrap_err();
922 }
923
924 assert_eq!(attempts.load(Ordering::SeqCst), 2);
925 }
926
927 #[tokio::test]
928 async fn non_cacheable_failures_do_not_block_later_attempts() {
929 for (domain, kind) in [
930 (
931 ConnectionErrorDomain::Transport,
932 ConnectionErrorKind::Rejected,
933 ),
934 (ConnectionErrorDomain::Transport, ConnectionErrorKind::Other),
935 (
936 ConnectionErrorDomain::Transport,
937 ConnectionErrorKind::Authentication,
938 ),
939 (
940 ConnectionErrorDomain::Application,
941 ConnectionErrorKind::Protocol,
942 ),
943 (
944 ConnectionErrorDomain::Local,
945 ConnectionErrorKind::InvalidInput,
946 ),
947 (ConnectionErrorDomain::Unknown, ConnectionErrorKind::Other),
948 ] {
949 let attempts = Arc::new(AtomicUsize::new(0));
950 let inner = service_fn({
951 let attempts = attempts.clone();
952 move |_input: ConnectRequest| {
953 attempts.fetch_add(1, Ordering::SeqCst);
954 async move {
955 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
956 ConnectionError::new(
957 BoxError::from_static_str("not cacheable"),
958 domain,
959 kind,
960 ),
961 )
962 }
963 }
964 });
965 let connector = ProxyRouteFailureCacheConnector::new(
966 inner,
967 cache(ProxyRouteFailureCacheScope::PerDestination),
968 );
969
970 for _ in 0..2 {
971 let _error = connector
972 .serve(request("one.example:443", proxy(None)))
973 .await
974 .unwrap_err();
975 }
976
977 assert_eq!(attempts.load(Ordering::SeqCst), 2, "{domain}/{kind}");
978 }
979 }
980
981 #[tokio::test]
982 async fn success_clears_failure_backoff() {
983 let attempts = Arc::new(AtomicUsize::new(0));
984 let inner = service_fn({
985 let attempts = attempts.clone();
986 move |input: ConnectRequest| {
987 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
988 async move {
989 if attempt == 0 {
990 Err(unavailable())
991 } else {
992 Ok(EstablishedClientConnection {
993 input,
994 conn: ServiceInput::new(()),
995 })
996 }
997 }
998 }
999 });
1000 let connector = ProxyRouteFailureCacheConnector::new(
1001 inner,
1002 cache(ProxyRouteFailureCacheScope::PerDestination),
1003 );
1004
1005 let _first_error = connector
1006 .serve(request("one.example:443", proxy(None)))
1007 .await
1008 .unwrap_err();
1009 tokio::time::sleep(Duration::from_millis(40)).await;
1010 let _first_connection = connector
1011 .serve(request("one.example:443", proxy(None)))
1012 .await
1013 .unwrap();
1014 let _second_connection = connector
1015 .serve(request("one.example:443", proxy(None)))
1016 .await
1017 .unwrap();
1018
1019 assert_eq!(attempts.load(Ordering::SeqCst), 3);
1020 }
1021
1022 #[tokio::test]
1023 async fn successful_routes_are_not_retained() {
1024 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1025 let inner = service_fn(|input: ConnectRequest| async {
1026 Ok::<_, ConnectionError>(EstablishedClientConnection {
1027 input,
1028 conn: ServiceInput::new(()),
1029 })
1030 });
1031 let connector = ProxyRouteFailureCacheConnector::new(inner, failure_cache.clone());
1032
1033 let _connection = connector
1034 .serve(request("one.example:443", proxy(None)))
1035 .await
1036 .unwrap();
1037 failure_cache.entries.run_pending_tasks();
1038
1039 assert_eq!(failure_cache.entry_count(), 0);
1040 }
1041
1042 #[test]
1043 fn cancelled_attempt_is_not_retained() {
1044 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1045 let request = request("one.example:443", proxy(None));
1046
1047 let permit = begin_attempt(&failure_cache, &request);
1048 failure_cache.entries.run_pending_tasks();
1049 assert_eq!(failure_cache.entry_count(), 1);
1050
1051 drop(permit);
1052 failure_cache.entries.run_pending_tasks();
1053 assert_eq!(failure_cache.entry_count(), 0);
1054 }
1055
1056 #[test]
1057 fn published_failure_deadline_blocks_before_count_update() {
1058 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1059 let request = request("one.example:443", proxy(None));
1060 let permit = begin_attempt(&failure_cache, &request);
1061 permit.entry.blocked_until.store(
1062 now_monotonic_nanos().saturating_add(duration_nanos(Duration::from_secs(1))),
1063 Ordering::Release,
1064 );
1065
1066 assert!(matches!(
1067 failure_cache.begin(&request),
1068 Some(CacheDecision::Blocked(_))
1069 ));
1070 }
1071
1072 #[test]
1073 fn success_state_wins_over_an_in_flight_failure() {
1074 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1075 let request = request("one.example:443", proxy(None));
1076 let mut permit = begin_attempt(&failure_cache, &request);
1077 let entry = permit.entry.clone();
1078
1079 entry.mark_live();
1080 failure_cache.mark_failure(&mut permit);
1081
1082 assert!(entry.succeeded.load(Ordering::Acquire));
1083 assert_eq!(entry.blocked_until.load(Ordering::Acquire), 0);
1084 assert_eq!(entry.probe_until.load(Ordering::Acquire), 0);
1085 assert_eq!(entry.failure_count.load(Ordering::Acquire), 0);
1086 }
1087
1088 #[test]
1089 fn newer_failure_deadline_is_not_replaced() {
1090 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1091 let request = request("one.example:443", proxy(None));
1092 let mut permit = begin_attempt(&failure_cache, &request);
1093 let entry = permit.entry.clone();
1094 let newer_deadline = permit.started_time.saturating_add(1);
1095 entry.blocked_until.store(newer_deadline, Ordering::Release);
1096 entry.failure_count.store(1, Ordering::Release);
1097
1098 failure_cache.mark_failure(&mut permit);
1099
1100 assert_eq!(entry.blocked_until.load(Ordering::Acquire), newer_deadline);
1101 assert_eq!(entry.failure_count.load(Ordering::Acquire), 1);
1102 }
1103
1104 #[test]
1105 fn equal_failure_deadline_can_be_replaced() {
1106 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1107 let request = request("one.example:443", proxy(None));
1108 let mut permit = begin_attempt(&failure_cache, &request);
1109 let entry = permit.entry.clone();
1110 entry
1111 .blocked_until
1112 .store(permit.started_time, Ordering::Release);
1113 entry.failure_count.store(1, Ordering::Release);
1114
1115 failure_cache.mark_failure(&mut permit);
1116
1117 assert!(entry.blocked_until.load(Ordering::Acquire) > permit.started_time);
1118 assert_eq!(entry.failure_count.load(Ordering::Acquire), 2);
1119 }
1120
1121 #[tokio::test]
1122 async fn concurrent_new_success_prevents_older_failure_from_blocking() {
1123 let attempts = Arc::new(AtomicUsize::new(0));
1124 let failure_started = Arc::new(Notify::new());
1125 let release_failure = Arc::new(Notify::new());
1126 let inner = service_fn({
1127 let attempts = attempts.clone();
1128 let failure_started = failure_started.clone();
1129 let release_failure = release_failure.clone();
1130 move |input: ConnectRequest| {
1131 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
1132 let failure_started = failure_started.clone();
1133 let release_failure = release_failure.clone();
1134 async move {
1135 if attempt == 0 {
1136 failure_started.notify_one();
1137 release_failure.notified().await;
1138 Err(unavailable())
1139 } else {
1140 Ok(EstablishedClientConnection {
1141 input,
1142 conn: ServiceInput::new(()),
1143 })
1144 }
1145 }
1146 }
1147 });
1148 let connector = ProxyRouteFailureCacheConnector::new(
1149 inner,
1150 cache(ProxyRouteFailureCacheScope::PerDestination),
1151 );
1152
1153 let failure_notification = failure_started.notified();
1154 let failing_attempt = tokio::spawn({
1155 let connector = connector.clone();
1156 async move {
1157 connector
1158 .serve(request("one.example:443", proxy(None)))
1159 .await
1160 }
1161 });
1162 within_test_timeout(failure_notification).await;
1163
1164 let _concurrent_success =
1165 within_test_timeout(connector.serve(request("one.example:443", proxy(None))))
1166 .await
1167 .unwrap();
1168 release_failure.notify_one();
1169 let _older_error = within_test_timeout(failing_attempt)
1170 .await
1171 .unwrap()
1172 .unwrap_err();
1173
1174 let _later_success =
1175 within_test_timeout(connector.serve(request("one.example:443", proxy(None))))
1176 .await
1177 .expect("the older failure must not block a route that succeeded concurrently");
1178 assert_eq!(attempts.load(Ordering::SeqCst), 3);
1179 }
1180
1181 #[tokio::test]
1182 async fn cancelled_attempt_does_not_discard_concurrent_failure() {
1183 let attempts = Arc::new(AtomicUsize::new(0));
1184 let failure_started = Arc::new(Notify::new());
1185 let cancelled_started = Arc::new(Notify::new());
1186 let release_failure = Arc::new(Notify::new());
1187 let hold_cancelled = Arc::new(Notify::new());
1188 let inner = service_fn({
1189 let attempts = attempts.clone();
1190 let failure_started = failure_started.clone();
1191 let cancelled_started = cancelled_started.clone();
1192 let release_failure = release_failure.clone();
1193 let hold_cancelled = hold_cancelled.clone();
1194 move |_input: ConnectRequest| {
1195 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
1196 let failure_started = failure_started.clone();
1197 let cancelled_started = cancelled_started.clone();
1198 let release_failure = release_failure.clone();
1199 let hold_cancelled = hold_cancelled.clone();
1200 async move {
1201 if attempt == 0 {
1202 failure_started.notify_one();
1203 release_failure.notified().await;
1204 } else {
1205 cancelled_started.notify_one();
1206 hold_cancelled.notified().await;
1207 }
1208 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
1209 unavailable(),
1210 )
1211 }
1212 }
1213 });
1214 let connector = ProxyRouteFailureCacheConnector::new(
1215 inner,
1216 cache(ProxyRouteFailureCacheScope::PerDestination),
1217 );
1218
1219 let failure_notification = failure_started.notified();
1220 let failing_attempt = tokio::spawn({
1221 let connector = connector.clone();
1222 async move {
1223 connector
1224 .serve(request("one.example:443", proxy(None)))
1225 .await
1226 }
1227 });
1228 within_test_timeout(failure_notification).await;
1229
1230 let cancelled_notification = cancelled_started.notified();
1231 let cancelled_attempt = tokio::spawn({
1232 let connector = connector.clone();
1233 async move {
1234 connector
1235 .serve(request("one.example:443", proxy(None)))
1236 .await
1237 }
1238 });
1239 within_test_timeout(cancelled_notification).await;
1240
1241 release_failure.notify_one();
1242 let _failure = within_test_timeout(failing_attempt)
1243 .await
1244 .unwrap()
1245 .unwrap_err();
1246 cancelled_attempt.abort();
1247 let _cancelled = within_test_timeout(cancelled_attempt).await.unwrap_err();
1248
1249 let cached = within_test_timeout(connector.serve(request("one.example:443", proxy(None))))
1250 .await
1251 .unwrap_err();
1252 assert_eq!(attempts.load(Ordering::SeqCst), 2);
1253 assert!(
1254 cached
1255 .get_ref()
1256 .downcast_ref::<ProxyRouteFailureCachedError>()
1257 .is_some()
1258 );
1259 }
1260
1261 #[tokio::test]
1262 async fn expired_entry_allows_only_one_concurrent_probe() {
1263 const TASKS: usize = 32;
1264
1265 let attempts = Arc::new(AtomicUsize::new(0));
1266 let probe_started = Arc::new(Notify::new());
1267 let release_probe = Arc::new(Notify::new());
1268 let inner = service_fn({
1269 let attempts = attempts.clone();
1270 let probe_started = probe_started.clone();
1271 let release_probe = release_probe.clone();
1272 move |_input: ConnectRequest| {
1273 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
1274 let probe_started = probe_started.clone();
1275 let release_probe = release_probe.clone();
1276 async move {
1277 if attempt > 0 {
1278 probe_started.notify_one();
1279 release_probe.notified().await;
1280 }
1281 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
1282 unavailable(),
1283 )
1284 }
1285 }
1286 });
1287 let connector = ProxyRouteFailureCacheConnector::new(
1288 inner,
1289 cache(ProxyRouteFailureCacheScope::PerDestination),
1290 );
1291
1292 let _initial_error = connector
1293 .serve(request("one.example:443", proxy(None)))
1294 .await
1295 .unwrap_err();
1296 tokio::time::sleep(Duration::from_millis(40)).await;
1297
1298 let barrier = Arc::new(Barrier::new(TASKS + 1));
1299 let mut tasks = Vec::with_capacity(TASKS);
1300 for _ in 0..TASKS {
1301 let connector = connector.clone();
1302 let barrier = barrier.clone();
1303 tasks.push(tokio::spawn(async move {
1304 barrier.wait().await;
1305 connector
1306 .serve(request("one.example:443", proxy(None)))
1307 .await
1308 }));
1309 }
1310 let probe_notification = probe_started.notified();
1311 within_test_timeout(async {
1312 barrier.wait().await;
1313 probe_notification.await;
1314 })
1315 .await;
1316 tokio::task::yield_now().await;
1317 release_probe.notify_one();
1318
1319 for task in tasks {
1320 let _error = within_test_timeout(task).await.unwrap().unwrap_err();
1321 }
1322 assert_eq!(attempts.load(Ordering::SeqCst), 2);
1323 }
1324
1325 #[tokio::test]
1326 async fn cancelling_half_open_probe_releases_its_lease() {
1327 let attempts = Arc::new(AtomicUsize::new(0));
1328 let probe_started = Arc::new(Notify::new());
1329 let hold_probe = Arc::new(Notify::new());
1330 let inner = service_fn({
1331 let attempts = attempts.clone();
1332 let probe_started = probe_started.clone();
1333 let hold_probe = hold_probe.clone();
1334 move |_input: ConnectRequest| {
1335 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
1336 let probe_started = probe_started.clone();
1337 let hold_probe = hold_probe.clone();
1338 async move {
1339 if attempt == 1 {
1340 probe_started.notify_one();
1341 hold_probe.notified().await;
1342 }
1343 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
1344 unavailable(),
1345 )
1346 }
1347 }
1348 });
1349 let connector = ProxyRouteFailureCacheConnector::new(
1350 inner,
1351 cache(ProxyRouteFailureCacheScope::PerDestination),
1352 );
1353
1354 let _initial_error = connector
1355 .serve(request("one.example:443", proxy(None)))
1356 .await
1357 .unwrap_err();
1358 tokio::time::sleep(Duration::from_millis(40)).await;
1359
1360 let probe_notification = probe_started.notified();
1361 let task = tokio::spawn({
1362 let connector = connector.clone();
1363 async move {
1364 connector
1365 .serve(request("one.example:443", proxy(None)))
1366 .await
1367 }
1368 });
1369 within_test_timeout(probe_notification).await;
1370 task.abort();
1371 let _join_error = within_test_timeout(task).await.unwrap_err();
1372
1373 let _probe_error = tokio::time::timeout(
1374 Duration::from_millis(100),
1375 connector.serve(request("one.example:443", proxy(None))),
1376 )
1377 .await
1378 .expect("cancelled probe must release its lease")
1379 .unwrap_err();
1380 assert_eq!(attempts.load(Ordering::SeqCst), 3);
1381 }
1382
1383 #[tokio::test]
1384 async fn cache_never_retains_more_than_its_capacity() {
1385 const CAPACITY: u64 = 4;
1386
1387 let failure_cache = ProxyRouteFailureCache::try_new(ProxyRouteFailureCacheConfig {
1388 initial_backoff: Duration::from_secs(1),
1389 max_backoff: Duration::from_secs(1),
1390 probe_lease: Duration::from_secs(1),
1391 max_entries: CAPACITY,
1392 scope: ProxyRouteFailureCacheScope::PerDestination,
1393 })
1394 .unwrap();
1395 let inner = service_fn(|_input: ConnectRequest| async {
1396 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(unavailable())
1397 });
1398 let connector = ProxyRouteFailureCacheConnector::new(inner, failure_cache.clone());
1399
1400 for index in 0..16 {
1401 let _error = connector
1402 .serve(request(
1403 &format!("destination-{index}.example:443"),
1404 proxy(None),
1405 ))
1406 .await
1407 .unwrap_err();
1408 }
1409 failure_cache.entries.run_pending_tasks();
1410
1411 assert!(failure_cache.entry_count() <= CAPACITY);
1412 }
1413
1414 #[tokio::test]
1415 async fn invalidating_cache_clears_retained_failure() {
1416 let attempts = Arc::new(AtomicUsize::new(0));
1417 let inner = service_fn({
1418 let attempts = attempts.clone();
1419 move |_input: ConnectRequest| {
1420 attempts.fetch_add(1, Ordering::SeqCst);
1421 async {
1422 Err::<EstablishedClientConnection<ServiceInput<()>, ConnectRequest>, _>(
1423 unavailable(),
1424 )
1425 }
1426 }
1427 });
1428 let failure_cache = cache(ProxyRouteFailureCacheScope::PerDestination);
1429 let connector = ProxyRouteFailureCacheConnector::new(inner, failure_cache.clone());
1430
1431 let _first_error = connector
1432 .serve(request("one.example:443", proxy(None)))
1433 .await
1434 .unwrap_err();
1435 failure_cache.entries.run_pending_tasks();
1436 assert_eq!(failure_cache.entry_count(), 1);
1437
1438 failure_cache.invalidate_all();
1439 let _second_error = connector
1440 .serve(request("one.example:443", proxy(None)))
1441 .await
1442 .unwrap_err();
1443 assert_eq!(attempts.load(Ordering::SeqCst), 2);
1444 }
1445
1446 #[test]
1447 fn rejects_invalid_configuration() {
1448 for config in [
1449 ProxyRouteFailureCacheConfig {
1450 initial_backoff: Duration::ZERO,
1451 ..Default::default()
1452 },
1453 ProxyRouteFailureCacheConfig {
1454 initial_backoff: Duration::from_secs(2),
1455 max_backoff: Duration::from_secs(1),
1456 ..Default::default()
1457 },
1458 ProxyRouteFailureCacheConfig {
1459 probe_lease: Duration::ZERO,
1460 ..Default::default()
1461 },
1462 ProxyRouteFailureCacheConfig {
1463 max_entries: 0,
1464 ..Default::default()
1465 },
1466 ] {
1467 ProxyRouteFailureCache::try_new(config).unwrap_err();
1468 }
1469 }
1470
1471 #[test]
1472 fn default_configuration_matches_easy_client_policy() {
1473 let custom_cache = cache(ProxyRouteFailureCacheScope::PerProxy);
1474 assert_eq!(
1475 custom_cache.config().scope,
1476 ProxyRouteFailureCacheScope::PerProxy
1477 );
1478 assert_eq!(
1479 custom_cache.config().initial_backoff,
1480 Duration::from_millis(20)
1481 );
1482
1483 let failure_cache = ProxyRouteFailureCache::default();
1484 let config = failure_cache.config();
1485 assert_eq!(config.scope, ProxyRouteFailureCacheScope::PerDestination);
1486 assert_eq!(config.initial_backoff, Duration::from_secs(60));
1487 assert_eq!(config.max_backoff, Duration::from_mins(30));
1488 assert_eq!(config.probe_lease, Duration::from_secs(30));
1489 assert_eq!(config.max_entries, 1_024);
1490 assert!(format!("{failure_cache:?}").contains("ProxyRouteFailureCache"));
1491 }
1492
1493 #[test]
1494 fn remaining_duration_excludes_the_deadline_itself() {
1495 assert_eq!(remaining_duration(11, 10), Some(Duration::from_nanos(1)));
1496 assert_eq!(remaining_duration(10, 10), None);
1497 assert_eq!(remaining_duration(9, 10), None);
1498 }
1499
1500 #[test]
1501 fn backoff_doubles_until_configured_cap() {
1502 let config = ProxyRouteFailureCacheConfig {
1503 initial_backoff: Duration::from_secs(1),
1504 max_backoff: Duration::from_secs(5),
1505 ..Default::default()
1506 };
1507 assert_eq!(config.backoff(0), Duration::from_secs(1));
1508 assert_eq!(config.backoff(1), Duration::from_secs(2));
1509 assert_eq!(config.backoff(2), Duration::from_secs(4));
1510 assert_eq!(config.backoff(3), Duration::from_secs(5));
1511 assert_eq!(config.backoff(u32::MAX), Duration::from_secs(5));
1512 }
1513}