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}