Skip to main content

exception_collector/
retry.rs

1//! 指数退避重试策略。
2//!
3//! 用于上报失败批次的自动重试:5min → 25min → 125min,
4//! 3 次尝试后放弃并标记 Failed。
5//!
6//! # 示例
7//!
8//! ```rust,ignore
9//! use exception_collector::retry::RetryPolicy;
10//!
11//! let policy = RetryPolicy::default();
12//! let result = with_retry(&policy, || async {
13//!     // 可能失败的操作
14//!     Ok::<_, String>("success")
15//! }).await;
16//! ```
17
18use std::{cell::RefCell, future::Future, time::Duration};
19
20/// 指数退避重试策略。
21///
22/// 控制重试次数和退避延迟,内部跟踪当前尝试次数。
23///
24/// # 默认值
25///
26/// - `max_retries`: 3
27/// - `base_delay`: 5 分钟
28///
29/// # 退避计算
30///
31/// 第 `n` 次重试的延迟为 `base_delay * 5^n`:
32/// - 第 1 次重试:5min
33/// - 第 2 次重试:25min
34/// - 第 3 次重试:125min
35#[derive(Debug)]
36pub struct RetryPolicy {
37    /// 最大重试次数。
38    max_retries: u32,
39    /// 基础延迟,用于指数退避计算。
40    base_delay: Duration,
41    /// 当前已尝试次数,用于跟踪重试状态。
42    attempts: RefCell<u32>,
43}
44
45impl RetryPolicy {
46    /// 创建一个新的重试策略。
47    ///
48    /// `max_retries` 为 0 时不重试,直接返回首次结果。
49    #[must_use]
50    pub fn new(max_retries: u32, base_delay: Duration) -> Self {
51        Self {
52            max_retries,
53            base_delay,
54            attempts: RefCell::new(0),
55        }
56    }
57
58    /// 返回当前已尝试次数。
59    #[must_use]
60    pub fn attempts(&self) -> u32 {
61        *self.attempts.borrow()
62    }
63
64    /// 计算第 `attempt` 次重试的退避延迟。
65    ///
66    /// 使用指数退避公式:`base_delay * 5^attempt`。
67    /// 对溢出进行保护,超过 `Duration::MAX` 时返回 `Duration::MAX`。
68    #[must_use]
69    pub fn backoff_delay(&self, attempt: u32) -> Duration {
70        let factor = 5_u64.saturating_pow(attempt);
71        let secs = self.base_delay.as_secs().saturating_mul(factor);
72        Duration::from_secs(secs)
73    }
74
75    /// 重置尝试计数器。
76    ///
77    /// 在成功上报后调用,为下一次可能的故障重置状态。
78    pub fn reset(&self) {
79        *self.attempts.borrow_mut() = 0;
80    }
81}
82
83impl Default for RetryPolicy {
84    fn default() -> Self {
85        Self::new(3, Duration::from_mins(5))
86    }
87}
88
89/// 对异步操作执行指数退避重试。
90///
91/// 根据 `RetryPolicy` 的配置,在操作失败时自动重试,
92/// 每次重试之间等待指数增长的退避延迟。
93///
94/// # 返回
95///
96/// - 操作成功时立即返回 `Ok`
97/// - 所有重试耗尽后返回最后一次错误
98///
99/// # 示例
100///
101/// ```rust,ignore
102/// use exception_collector::retry::{RetryPolicy, with_retry};
103///
104/// let policy = RetryPolicy::default();
105/// let result = with_retry(&policy, || async {
106///     upload_batch().await
107/// }).await;
108/// ```
109///
110/// # Errors
111///
112/// 返回闭包产生的错误类型,当所有重试耗尽时。
113pub async fn with_retry<F, Fut, T, E>(policy: &RetryPolicy, f: F) -> Result<T, E>
114where
115    F: Fn() -> Fut,
116    Fut: Future<Output = Result<T, E>>,
117{
118    policy.reset();
119
120    let mut attempt: u32 = 0;
121    loop {
122        attempt += 1;
123        *policy.attempts.borrow_mut() = attempt;
124
125        match f().await {
126            Ok(value) => {
127                policy.reset();
128                return Ok(value);
129            }
130            Err(err) => {
131                // 已用尽所有重试(首次 + max_retries 次重试)
132                if attempt > policy.max_retries {
133                    return Err(err);
134                }
135                let delay = policy.backoff_delay(attempt - 1);
136                tokio::time::sleep(delay).await;
137            }
138        }
139    }
140}
141
142// ── Tests ─────────────────────────────────────────────────────────────────
143
144#[cfg(test)]
145#[allow(
146    clippy::unwrap_used,
147    clippy::unwrap_in_result,
148    clippy::expect_used,
149    clippy::panic,
150    clippy::pedantic,
151    clippy::disallowed_methods,
152    clippy::indexing_slicing,
153    clippy::wildcard_enum_match_arm,
154    reason = "test module relaxes production lint strictness"
155)]
156mod tests {
157    use std::sync::atomic::{AtomicU32, Ordering};
158
159    use super::*;
160
161    #[test]
162    fn test_retry_policy_default_values() {
163        let policy = RetryPolicy::default();
164        assert_eq!(policy.max_retries, 3);
165        assert_eq!(policy.base_delay, Duration::from_mins(5));
166    }
167
168    #[test]
169    fn test_retry_policy_custom_values() {
170        let policy = RetryPolicy::new(5, Duration::from_secs(10));
171        assert_eq!(policy.max_retries, 5);
172        assert_eq!(policy.base_delay, Duration::from_secs(10));
173    }
174
175    #[test]
176    fn test_retry_policy_starts_at_zero_attempts() {
177        let policy = RetryPolicy::default();
178        assert_eq!(policy.attempts(), 0);
179    }
180
181    #[test]
182    fn test_retry_policy_backoff_delay_exponential() {
183        let policy = RetryPolicy::new(3, Duration::from_mins(5));
184
185        // 5min → 25min → 125min
186        assert_eq!(policy.backoff_delay(0), Duration::from_mins(5));
187        assert_eq!(policy.backoff_delay(1), Duration::from_mins(25));
188        assert_eq!(policy.backoff_delay(2), Duration::from_mins(125));
189    }
190
191    #[test]
192    fn test_retry_policy_backoff_delay_protects_overflow() {
193        let policy = RetryPolicy::new(100, Duration::from_secs(u64::MAX));
194        let delay = policy.backoff_delay(10);
195        // saturating_mul returns u64::MAX when overflow
196        assert_eq!(delay, Duration::from_secs(u64::MAX));
197    }
198
199    #[test]
200    fn test_retry_policy_reset_clears_attempts() {
201        let policy = RetryPolicy::default();
202        *policy.attempts.borrow_mut() = 5;
203        assert_eq!(policy.attempts(), 5);
204
205        policy.reset();
206        assert_eq!(policy.attempts(), 0);
207    }
208
209    #[tokio::test]
210    async fn test_should_succeed_on_first_attempt() {
211        let policy = RetryPolicy::default();
212        let counter = AtomicU32::new(0);
213
214        let result = with_retry(&policy, || async {
215            counter.fetch_add(1, Ordering::SeqCst);
216            Ok::<_, String>("ok")
217        })
218        .await;
219
220        assert_eq!(result, Ok("ok"));
221        assert_eq!(counter.load(Ordering::SeqCst), 1);
222    }
223
224    #[tokio::test]
225    async fn test_should_retry_up_to_max() {
226        let policy = RetryPolicy::new(2, Duration::from_millis(1));
227        let counter = AtomicU32::new(0);
228
229        let result = with_retry(&policy, || async {
230            let attempt = counter.fetch_add(1, Ordering::SeqCst);
231            if attempt < 2 {
232                Err::<(), _>("fail".to_string())
233            } else {
234                Ok::<_, String>(())
235            }
236        })
237        .await;
238
239        assert!(result.is_ok());
240        assert_eq!(counter.load(Ordering::SeqCst), 3);
241    }
242
243    #[tokio::test]
244    async fn test_should_mark_failed_after_exhaustion() {
245        let policy = RetryPolicy::new(2, Duration::from_millis(1));
246
247        let result = with_retry(&policy, || async {
248            Err::<String, _>("permanent_fail".to_string())
249        })
250        .await;
251
252        assert_eq!(result, Err("permanent_fail".to_string()));
253        assert_eq!(policy.attempts(), 3);
254    }
255
256    #[tokio::test]
257    async fn test_should_reset_after_success() {
258        let policy = RetryPolicy::new(3, Duration::from_millis(1));
259        let call_count = AtomicU32::new(0);
260
261        // 第一次:失败后成功
262        let result = with_retry(&policy, || async {
263            let n = call_count.fetch_add(1, Ordering::SeqCst);
264            if n == 0 {
265                Err::<(), _>("first_fail".to_string())
266            } else {
267                Ok::<_, String>(())
268            }
269        })
270        .await;
271
272        assert!(result.is_ok());
273        assert_eq!(policy.attempts(), 0);
274
275        // 第二次:直接成功,尝试计数器应从 0 开始
276        let result = with_retry(&policy, || async { Ok::<_, String>(()) }).await;
277
278        assert!(result.is_ok());
279        assert_eq!(policy.attempts(), 0);
280    }
281
282    #[tokio::test]
283    async fn test_should_not_retry_when_max_retries_zero() {
284        let policy = RetryPolicy::new(0, Duration::from_millis(1));
285        let counter = AtomicU32::new(0);
286
287        let result = with_retry(&policy, || async {
288            counter.fetch_add(1, Ordering::SeqCst);
289            Err::<String, _>("fail".to_string())
290        })
291        .await;
292
293        assert_eq!(result, Err("fail".to_string()));
294        assert_eq!(counter.load(Ordering::SeqCst), 1);
295    }
296}