confium_coordinator/
retry.rs1use std::sync::Mutex;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
12pub struct RetryConfig {
13 pub initial_delay: Duration,
15 pub max_delay: Duration,
17 pub max_attempts: u32,
19 pub backoff_multiplier: f64,
21}
22
23impl Default for RetryConfig {
24 fn default() -> Self {
25 Self {
26 initial_delay: Duration::from_millis(100),
27 max_delay: Duration::from_secs(30),
28 max_attempts: 5,
29 backoff_multiplier: 2.0,
30 }
31 }
32}
33
34pub fn delay_for_attempt(config: &RetryConfig, attempt: u32) -> Duration {
36 let multiplier = config.backoff_multiplier.powi(attempt as i32);
37 let millis = config.initial_delay.as_millis() as f64 * multiplier;
38 let capped = millis.min(config.max_delay.as_millis() as f64);
39 Duration::from_millis(capped as u64)
40}
41
42#[derive(Debug, Clone)]
44pub struct RetryEntry<T> {
45 pub item: T,
47 pub attempts: u32,
49 pub next_delay: Duration,
51}
52
53pub struct RetryQueue<T> {
55 config: RetryConfig,
56 entries: Mutex<Vec<RetryEntry<T>>>,
57}
58
59impl<T: Clone> RetryQueue<T> {
60 pub fn new(config: RetryConfig) -> Self {
62 Self {
63 config,
64 entries: Mutex::new(Vec::new()),
65 }
66 }
67
68 pub fn enqueue(&self, item: T, attempts: u32) -> bool {
71 if attempts >= self.config.max_attempts {
72 return false;
73 }
74 let delay = delay_for_attempt(&self.config, attempts);
75 self.entries.lock().unwrap().push(RetryEntry {
76 item,
77 attempts,
78 next_delay: delay,
79 });
80 true
81 }
82
83 pub fn drain_ready(&self) -> Vec<RetryEntry<T>> {
86 let mut entries = self.entries.lock().unwrap();
87 std::mem::take(&mut *entries)
88 }
89
90 pub fn len(&self) -> usize {
92 self.entries.lock().unwrap().len()
93 }
94
95 pub fn is_empty(&self) -> bool {
97 self.len() == 0
98 }
99
100 pub fn config(&self) -> &RetryConfig {
102 &self.config
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn default_config_has_5_attempts() {
112 let config = RetryConfig::default();
113 assert_eq!(config.max_attempts, 5);
114 assert_eq!(config.initial_delay, Duration::from_millis(100));
115 }
116
117 #[test]
118 fn delay_doubles_each_attempt() {
119 let config = RetryConfig::default();
120 let d0 = delay_for_attempt(&config, 0);
121 let d1 = delay_for_attempt(&config, 1);
122 let d2 = delay_for_attempt(&config, 2);
123 assert_eq!(d0, Duration::from_millis(100));
124 assert_eq!(d1, Duration::from_millis(200));
125 assert_eq!(d2, Duration::from_millis(400));
126 }
127
128 #[test]
129 fn delay_capped_at_max() {
130 let config = RetryConfig::default();
131 let big = delay_for_attempt(&config, 20);
132 assert!(big <= config.max_delay);
133 }
134
135 #[test]
136 fn enqueue_adds_item() {
137 let queue = RetryQueue::<String>::new(RetryConfig::default());
138 assert!(queue.enqueue("task-1".into(), 0));
139 assert_eq!(queue.len(), 1);
140 }
141
142 #[test]
143 fn enqueue_at_max_attempts_rejected() {
144 let queue = RetryQueue::<String>::new(RetryConfig::default());
145 assert!(!queue.enqueue("doomed".into(), 5));
146 assert_eq!(queue.len(), 0);
147 }
148
149 #[test]
150 fn drain_removes_all() {
151 let queue = RetryQueue::<String>::new(RetryConfig::default());
152 queue.enqueue("a".into(), 0);
153 queue.enqueue("b".into(), 1);
154 queue.enqueue("c".into(), 2);
155 let drained = queue.drain_ready();
156 assert_eq!(drained.len(), 3);
157 assert!(queue.is_empty());
158 }
159
160 #[test]
161 fn retry_entry_carries_attempt_count() {
162 let queue = RetryQueue::<u32>::new(RetryConfig::default());
163 queue.enqueue(42, 3);
164 let drained = queue.drain_ready();
165 assert_eq!(drained[0].item, 42);
166 assert_eq!(drained[0].attempts, 3);
167 }
168
169 #[test]
170 fn delay_increases_with_attempts() {
171 let queue = RetryQueue::<u32>::new(RetryConfig::default());
172 queue.enqueue(1, 0);
173 queue.enqueue(2, 2);
174 queue.enqueue(3, 4);
175 let drained = queue.drain_ready();
176 assert!(drained[0].next_delay < drained[1].next_delay);
177 assert!(drained[1].next_delay < drained[2].next_delay);
178 }
179
180 #[test]
181 fn empty_queue_drains_to_empty() {
182 let queue = RetryQueue::<u32>::new(RetryConfig::default());
183 assert!(queue.drain_ready().is_empty());
184 }
185
186 #[test]
187 fn config_accessible() {
188 let config = RetryConfig {
189 max_attempts: 10,
190 ..Default::default()
191 };
192 let queue = RetryQueue::<u32>::new(config);
193 assert_eq!(queue.config().max_attempts, 10);
194 }
195
196 #[test]
197 fn custom_multiplier_changes_growth() {
198 let config = RetryConfig {
199 initial_delay: Duration::from_millis(1000),
200 max_delay: Duration::from_secs(600),
201 max_attempts: 5,
202 backoff_multiplier: 3.0,
203 };
204 assert_eq!(delay_for_attempt(&config, 0), Duration::from_millis(1000));
205 assert_eq!(delay_for_attempt(&config, 1), Duration::from_millis(3000));
206 assert_eq!(delay_for_attempt(&config, 2), Duration::from_millis(9000));
207 }
208}