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}