qiniu_http_client/client/backoff/
randomized.rs

1use 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/// 均匀分布随机化退避时长提供者
9///
10/// 基于一个退避时长提供者并为其增加随机化范围
11///
12/// 默认的随机化范围是 `[1/2, 3/2]`
13#[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    /// 创建均匀分布随机化退避时长提供者
22    ///
23    /// 需要提供随机化范围,其中随机化范围由最小随机比率和最大随机比率组成,返回的退避时长为 `random(base_backoff * minification, base_backoff * magnification)`
24    ///
25    /// 需要注意,提供的随机比率的分母必须大于 0。
26    #[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    /// 获取基础退避时长提供者
38    #[inline]
39    pub const fn base_backoff(&self) -> &P {
40        &self.base_backoff
41    }
42
43    /// 获取最小随机比率
44    #[inline]
45    pub const fn minification(&self) -> Ratio<u8> {
46        self.minification
47    }
48
49    /// 获取最大随机比率
50    #[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}