qiniu_http_client/client/backoff/
randomized.rs1use super::{Backoff, BackoffOptions, GotBackoffDuration};
2use qiniu_http::RequestParts as HttpRequestParts;
3use rand::{thread_rng, Rng};
4use std::{convert::TryInto, fmt::Debug, time::Duration, u64};
5
6pub use num_rational::Ratio;
7
8#[derive(Debug, Clone)]
14pub struct RandomizedBackoff<P: ?Sized> {
15 minification: Ratio<u8>,
16 magnification: Ratio<u8>,
17 base_backoff: P,
18}
19
20impl<P> RandomizedBackoff<P> {
21 #[inline]
27 pub const fn new(base_backoff: P, minification: Ratio<u8>, magnification: Ratio<u8>) -> Self {
28 assert!(*minification.denom() > 0);
29 assert!(*magnification.denom() > 0);
30 Self {
31 base_backoff,
32 minification,
33 magnification,
34 }
35 }
36
37 #[inline]
39 pub const fn base_backoff(&self) -> &P {
40 &self.base_backoff
41 }
42
43 #[inline]
45 pub const fn minification(&self) -> Ratio<u8> {
46 self.minification
47 }
48
49 #[inline]
51 pub const fn magnification(&self) -> Ratio<u8> {
52 self.magnification
53 }
54}
55
56impl<P: Backoff + Clone> Backoff for RandomizedBackoff<P> {
57 fn time(&self, request: &mut HttpRequestParts, opts: BackoffOptions) -> GotBackoffDuration {
58 let duration = self.base_backoff().time(request, opts).duration();
59 let minification: Ratio<u128> = Ratio::new_raw(
60 self.minification().numer().to_owned().into(),
61 self.minification().denom().to_owned().into(),
62 );
63 let magnification: Ratio<u128> = Ratio::new_raw(
64 self.magnification().numer().to_owned().into(),
65 self.magnification().denom().to_owned().into(),
66 );
67 let minified: u64 = (minification * duration.as_nanos())
68 .to_integer()
69 .try_into()
70 .unwrap_or(u64::MAX);
71 let magnified: u64 = (magnification * duration.as_nanos())
72 .to_integer()
73 .try_into()
74 .unwrap_or(u64::MAX);
75
76 let randomized = thread_rng().gen_range(minified, magnified);
77 Duration::from_nanos(randomized).into()
78 }
79}
80
81impl<P: Default> Default for RandomizedBackoff<P> {
82 #[inline]
83 fn default() -> Self {
84 RandomizedBackoff::new(P::default(), Ratio::new_raw(1, 2), Ratio::new_raw(3, 2))
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::{
91 super::{FixedBackoff, ResponseError, RetriedStatsInfo, RetryDecision},
92 *,
93 };
94 use qiniu_http::ResponseErrorKind as HttpResponseErrorKind;
95 use std::{error::Error, result::Result};
96
97 #[test]
98 fn test_randomized_backoff() -> Result<(), Box<dyn Error>> {
99 let fixed = FixedBackoff::new(Duration::from_secs(1));
100 let randomized = RandomizedBackoff::new(fixed, Ratio::new_raw(1, 2), Ratio::new_raw(3, 2));
101
102 for _ in 0..10000 {
103 let delay = randomized
104 .time(
105 &mut HttpRequestParts::default(),
106 BackoffOptions::builder(
107 &ResponseError::new_with_msg(HttpResponseErrorKind::TimeoutError.into(), "Test Error"),
108 &RetriedStatsInfo::default(),
109 )
110 .retry_decision(RetryDecision::RetryRequest)
111 .build(),
112 )
113 .duration();
114 assert!(delay >= Duration::from_millis(500));
115 assert!(delay != Duration::from_millis(1000));
116 assert!(delay < Duration::from_millis(1500));
117 }
118
119 Ok(())
120 }
121}