ddns_a/webhook/
retry.rs

1//! Retry policy configuration for webhook operations.
2
3use std::time::Duration;
4
5/// Configuration for exponential backoff retry behavior.
6///
7/// Controls how many times to retry a failed operation and how long
8/// to wait between attempts. Uses exponential backoff with a configurable
9/// multiplier and maximum delay cap.
10///
11/// # Defaults
12///
13/// - `max_attempts`: 3
14/// - `initial_delay`: 5 seconds
15/// - `max_delay`: 60 seconds
16/// - `multiplier`: 2.0
17///
18/// # Example
19///
20/// ```
21/// use ddns_a::webhook::RetryPolicy;
22/// use std::time::Duration;
23///
24/// // Create with defaults
25/// let policy = RetryPolicy::default();
26///
27/// // Or customize via builder
28/// let custom = RetryPolicy::new()
29///     .with_max_attempts(5)
30///     .with_initial_delay(Duration::from_secs(1))
31///     .with_max_delay(Duration::from_secs(30))
32///     .with_multiplier(1.5);
33/// ```
34#[derive(Debug, Clone, PartialEq)]
35pub struct RetryPolicy {
36    /// Maximum number of attempts (including the initial attempt).
37    ///
38    /// A value of 1 means no retries; only the initial attempt is made.
39    pub max_attempts: u32,
40
41    /// Delay before the first retry.
42    ///
43    /// Subsequent delays are computed by multiplying by `multiplier`.
44    pub initial_delay: Duration,
45
46    /// Maximum delay between retries.
47    ///
48    /// The computed delay is capped at this value to prevent
49    /// excessively long waits.
50    pub max_delay: Duration,
51
52    /// Multiplier applied to the delay after each retry.
53    ///
54    /// A value of 2.0 doubles the delay each time.
55    pub multiplier: f64,
56}
57
58impl RetryPolicy {
59    /// Default maximum attempts.
60    pub const DEFAULT_MAX_ATTEMPTS: u32 = 3;
61
62    /// Default initial delay (5 seconds).
63    pub const DEFAULT_INITIAL_DELAY: Duration = Duration::from_secs(5);
64
65    /// Default maximum delay (60 seconds).
66    pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(60);
67
68    /// Default multiplier (2.0).
69    pub const DEFAULT_MULTIPLIER: f64 = 2.0;
70
71    /// Creates a new retry policy with default values.
72    #[must_use]
73    pub const fn new() -> Self {
74        Self {
75            max_attempts: Self::DEFAULT_MAX_ATTEMPTS,
76            initial_delay: Self::DEFAULT_INITIAL_DELAY,
77            max_delay: Self::DEFAULT_MAX_DELAY,
78            multiplier: Self::DEFAULT_MULTIPLIER,
79        }
80    }
81
82    /// Minimum value for `max_attempts`.
83    pub const MIN_MAX_ATTEMPTS: u32 = 1;
84
85    /// Sets the maximum number of attempts.
86    ///
87    /// # Panics
88    ///
89    /// Panics if `max_attempts` is less than 1.
90    #[must_use]
91    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
92        assert!(
93            max_attempts >= Self::MIN_MAX_ATTEMPTS,
94            "max_attempts must be at least 1"
95        );
96        self.max_attempts = max_attempts;
97        self
98    }
99
100    /// Sets the initial delay between retries.
101    ///
102    /// Zero delay is supported (useful for testing with [`InstantSleeper`])
103    /// but not recommended for production as it creates a tight retry loop.
104    ///
105    /// [`InstantSleeper`]: crate::time::InstantSleeper
106    #[must_use]
107    pub const fn with_initial_delay(mut self, delay: Duration) -> Self {
108        self.initial_delay = delay;
109        self
110    }
111
112    /// Sets the maximum delay between retries.
113    #[must_use]
114    pub const fn with_max_delay(mut self, delay: Duration) -> Self {
115        self.max_delay = delay;
116        self
117    }
118
119    /// Sets the delay multiplier.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `multiplier` is not positive (must be > 0.0).
124    #[must_use]
125    pub fn with_multiplier(mut self, multiplier: f64) -> Self {
126        assert!(multiplier > 0.0, "multiplier must be positive");
127        self.multiplier = multiplier;
128        self
129    }
130
131    /// Computes the delay for a given retry number (0-indexed).
132    ///
133    /// # Arguments
134    ///
135    /// * `retry` - The retry number (0 = delay before first retry, 1 = delay before second retry, etc.)
136    ///
137    /// # Returns
138    ///
139    /// The delay to wait before this retry, capped at `max_delay`.
140    #[must_use]
141    pub fn delay_for_retry(&self, retry: u32) -> Duration {
142        // Safe cast: retry values are small (typically < 20) and i32::MAX is ~2 billion
143        #[allow(clippy::cast_possible_wrap)]
144        let multiplier = self.multiplier.powi(retry as i32);
145        let delay_secs = self.initial_delay.as_secs_f64() * multiplier;
146        let capped = delay_secs.min(self.max_delay.as_secs_f64());
147        Duration::from_secs_f64(capped)
148    }
149
150    /// Returns true if the given attempt number should be retried.
151    ///
152    /// # Arguments
153    ///
154    /// * `attempt` - The attempt number (1 = first attempt, 2 = first retry, etc.)
155    ///
156    /// # Returns
157    ///
158    /// `true` if the attempt is within the allowed number of attempts.
159    #[must_use]
160    pub const fn should_retry(&self, attempt: u32) -> bool {
161        attempt < self.max_attempts
162    }
163}
164
165impl Default for RetryPolicy {
166    fn default() -> Self {
167        Self::new()
168    }
169}