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}