chie_shared/config/retry.rs
1//! Retry policy configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Retry policy configuration
6///
7/// # Examples
8///
9/// Using the default configuration:
10/// ```
11/// use chie_shared::RetryConfig;
12///
13/// let config = RetryConfig::default();
14/// assert_eq!(config.max_attempts, 3);
15/// assert_eq!(config.initial_backoff_ms, 100);
16/// assert_eq!(config.multiplier, 2.0);
17/// assert!(config.enable_jitter);
18///
19/// // Check if retries are exhausted
20/// assert!(!config.is_exhausted(0));
21/// assert!(!config.is_exhausted(2));
22/// assert!(config.is_exhausted(3));
23/// ```
24///
25/// Building a custom aggressive retry policy:
26/// ```
27/// use chie_shared::RetryConfigBuilder;
28///
29/// let config = RetryConfigBuilder::new()
30/// .max_attempts(5)
31/// .initial_backoff_ms(100)
32/// .max_backoff_ms(10_000)
33/// .multiplier(2.0)
34/// .enable_jitter(false)
35/// .build();
36///
37/// assert_eq!(config.max_attempts, 5);
38/// assert_eq!(config.initial_backoff_ms, 100);
39/// assert!(!config.enable_jitter);
40///
41/// // Calculate backoff delays (without jitter) - exponential backoff
42/// assert_eq!(config.next_backoff_ms(0), 100); // 100 * 2^0
43/// assert_eq!(config.next_backoff_ms(1), 200); // 100 * 2^1
44/// assert_eq!(config.next_backoff_ms(2), 400); // 100 * 2^2
45/// assert_eq!(config.next_backoff_ms(3), 800); // 100 * 2^3
46/// ```
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct RetryConfig {
49 /// Maximum number of retry attempts
50 pub max_attempts: u32,
51 /// Initial backoff delay in milliseconds
52 pub initial_backoff_ms: u64,
53 /// Maximum backoff delay in milliseconds
54 pub max_backoff_ms: u64,
55 /// Backoff multiplier (exponential)
56 pub multiplier: f64,
57 /// Enable jitter to avoid thundering herd
58 pub enable_jitter: bool,
59}
60
61impl RetryConfig {
62 /// Check if retries are exhausted
63 #[must_use]
64 pub const fn is_exhausted(&self, attempt: u32) -> bool {
65 attempt >= self.max_attempts
66 }
67
68 /// Calculate next backoff delay with jitter
69 #[must_use]
70 pub fn next_backoff_ms(&self, attempt: u32) -> u64 {
71 use crate::utils::random_jitter;
72
73 // Cap attempt at 10 to prevent overflow and convert to i32
74 #[allow(clippy::cast_possible_wrap)]
75 let exponent = attempt.min(10) as i32;
76
77 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
78 let base_delay = self
79 .initial_backoff_ms
80 .saturating_mul(self.multiplier.powi(exponent) as u64)
81 .min(self.max_backoff_ms);
82
83 if self.enable_jitter {
84 random_jitter(base_delay, 0.25) // ±25% jitter
85 } else {
86 base_delay
87 }
88 }
89
90 /// Validate the retry configuration
91 ///
92 /// # Errors
93 ///
94 /// Returns error if configuration is invalid
95 pub fn validate(&self) -> crate::ChieResult<()> {
96 use crate::ChieError;
97
98 if self.max_attempts == 0 {
99 return Err(ChieError::validation("max_attempts must be greater than 0"));
100 }
101
102 if self.initial_backoff_ms == 0 {
103 return Err(ChieError::validation(
104 "initial_backoff_ms must be greater than 0",
105 ));
106 }
107
108 if self.max_backoff_ms < self.initial_backoff_ms {
109 return Err(ChieError::validation(
110 "max_backoff_ms must be greater than or equal to initial_backoff_ms",
111 ));
112 }
113
114 if self.multiplier <= 0.0 {
115 return Err(ChieError::validation("multiplier must be greater than 0"));
116 }
117
118 Ok(())
119 }
120}
121
122impl Default for RetryConfig {
123 fn default() -> Self {
124 Self {
125 max_attempts: 3,
126 initial_backoff_ms: 100,
127 max_backoff_ms: 30_000,
128 multiplier: 2.0,
129 enable_jitter: true,
130 }
131 }
132}
133
134/// Builder for `RetryConfig`
135///
136/// # Examples
137///
138/// Building a conservative retry policy for critical operations:
139/// ```
140/// use chie_shared::RetryConfigBuilder;
141///
142/// let config = RetryConfigBuilder::new()
143/// .max_attempts(10)
144/// .initial_backoff_ms(1_000)
145/// .max_backoff_ms(60_000)
146/// .multiplier(2.0)
147/// .enable_jitter(true)
148/// .build();
149///
150/// assert_eq!(config.max_attempts, 10);
151/// assert!(config.validate().is_ok());
152/// ```
153#[derive(Debug, Default)]
154pub struct RetryConfigBuilder {
155 config: RetryConfig,
156}
157
158impl RetryConfigBuilder {
159 /// Create a new builder with default values
160 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 /// Set maximum retry attempts
166 #[must_use]
167 pub const fn max_attempts(mut self, max: u32) -> Self {
168 self.config.max_attempts = max;
169 self
170 }
171
172 /// Set initial backoff delay
173 #[must_use]
174 pub const fn initial_backoff_ms(mut self, ms: u64) -> Self {
175 self.config.initial_backoff_ms = ms;
176 self
177 }
178
179 /// Set maximum backoff delay
180 #[must_use]
181 pub const fn max_backoff_ms(mut self, ms: u64) -> Self {
182 self.config.max_backoff_ms = ms;
183 self
184 }
185
186 /// Set backoff multiplier
187 #[must_use]
188 pub const fn multiplier(mut self, multiplier: f64) -> Self {
189 self.config.multiplier = multiplier;
190 self
191 }
192
193 /// Enable or disable jitter
194 #[must_use]
195 pub const fn enable_jitter(mut self, enable: bool) -> Self {
196 self.config.enable_jitter = enable;
197 self
198 }
199
200 /// Build the configuration
201 #[must_use]
202 pub const fn build(self) -> RetryConfig {
203 self.config
204 }
205}