Skip to main content

cfgd_core/
retry.rs

1//! Shared retry-with-exponential-backoff policy.
2//!
3//! Before this module, the same `MAX_RETRIES = 3` and `500ms * 2^n` backoff
4//! math was inlined at `server_client::post_with_retry` (sync/ureq) and
5//! `gateway::api::create_drift_alert_crd` (async/kube). Three timeouts had
6//! already drifted when the dedup audit flagged it.
7//!
8//! Call sites retain their own error classification — the sync site retries
9//! on `ureq::Error::Transport` and 5xx, the async site retries on any kube
10//! error and short-circuits on HTTP 409. Only the policy (attempts + delay)
11//! lives here.
12
13use std::time::Duration;
14
15/// Retry policy for transient network/API failures.
16#[derive(Debug, Clone, Copy)]
17pub struct BackoffConfig {
18    pub max_attempts: u32,
19    pub initial_backoff: Duration,
20}
21
22impl BackoffConfig {
23    /// Canonical transient-error policy: 3 attempts, 500ms initial delay
24    /// doubled each retry (→ 500ms, 1s before the third attempt).
25    pub const DEFAULT_TRANSIENT: Self = Self {
26        max_attempts: 3,
27        initial_backoff: Duration::from_millis(500),
28    };
29
30    /// Delay before `attempt` (1-indexed; attempt 0 has no preceding delay).
31    /// Returns `Duration::ZERO` for `attempt == 0` so a single tight branch
32    /// at the call site handles both the first-attempt and retry cases.
33    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
34        if attempt == 0 {
35            Duration::ZERO
36        } else {
37            self.initial_backoff * 2u32.pow(attempt - 1)
38        }
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn default_transient_has_three_attempts() {
48        assert_eq!(BackoffConfig::DEFAULT_TRANSIENT.max_attempts, 3);
49    }
50
51    #[test]
52    fn delay_for_attempt_zero_is_zero() {
53        assert_eq!(
54            BackoffConfig::DEFAULT_TRANSIENT.delay_for_attempt(0),
55            Duration::ZERO
56        );
57    }
58
59    #[test]
60    fn delay_for_attempt_doubles() {
61        let c = BackoffConfig::DEFAULT_TRANSIENT;
62        assert_eq!(c.delay_for_attempt(1), Duration::from_millis(500));
63        assert_eq!(c.delay_for_attempt(2), Duration::from_millis(1000));
64        assert_eq!(c.delay_for_attempt(3), Duration::from_millis(2000));
65    }
66}