1use async_trait::async_trait;
2use camel_component_api::{CamelError, Exchange, NetworkRetryPolicy};
3#[cfg(test)]
5use camel_component_api::retry_async;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use tokio::sync::Mutex;
9
10use crate::config::{RedisCommand, is_transient_redis_error};
11
12#[async_trait]
17pub trait RedisCommandExecutor: Send + Sync {
18 async fn execute_command(
20 &mut self,
21 cmd: &RedisCommand,
22 exchange: &mut Exchange,
23 ) -> Result<(), CamelError>;
24
25 async fn reconnect(&mut self) -> Result<(), CamelError>;
27}
28
29pub struct FakeExecutor {
35 results: Arc<Mutex<Vec<Result<(), FakeError>>>>,
37 pub call_count: Arc<AtomicUsize>,
39 pub reconnect_count: Arc<AtomicUsize>,
41}
42
43#[derive(Debug, Clone)]
45pub struct FakeError {
46 pub message: String,
47 pub is_transient: bool,
48}
49
50impl std::fmt::Display for FakeError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{}", self.message)
53 }
54}
55
56impl FakeExecutor {
57 pub fn new(results: Vec<Result<(), FakeError>>) -> Self {
59 Self {
60 results: Arc::new(Mutex::new(results)),
61 call_count: Arc::new(AtomicUsize::new(0)),
62 reconnect_count: Arc::new(AtomicUsize::new(0)),
63 }
64 }
65
66 pub fn call_count(&self) -> Arc<AtomicUsize> {
68 Arc::clone(&self.call_count)
69 }
70
71 pub fn reconnect_count(&self) -> Arc<AtomicUsize> {
73 Arc::clone(&self.reconnect_count)
74 }
75}
76
77#[async_trait]
78impl RedisCommandExecutor for FakeExecutor {
79 async fn execute_command(
80 &mut self,
81 _cmd: &RedisCommand,
82 _exchange: &mut Exchange,
83 ) -> Result<(), CamelError> {
84 self.call_count.fetch_add(1, Ordering::SeqCst);
85
86 let result = {
87 let mut results = self.results.lock().await;
88 results.pop().unwrap_or(Ok(()))
89 };
90
91 match result {
92 Ok(()) => Ok(()),
93 Err(fake_err) => {
94 if fake_err.is_transient {
95 Err(CamelError::ProcessorError(format!(
96 "Connection error: {}",
97 fake_err.message
98 )))
99 } else {
100 Err(CamelError::ProcessorError(fake_err.message))
101 }
102 }
103 }
104 }
105
106 async fn reconnect(&mut self) -> Result<(), CamelError> {
107 self.reconnect_count.fetch_add(1, Ordering::SeqCst);
108 Ok(())
109 }
110}
111
112pub async fn execute_with_retry<E: RedisCommandExecutor>(
131 executor: &mut E,
132 cmd: &RedisCommand,
133 exchange: &mut Exchange,
134 is_idempotent: bool,
135 policy: &NetworkRetryPolicy,
136) -> Result<(), CamelError> {
137 let effective_policy = if is_idempotent {
139 policy.clone()
140 } else {
141 NetworkRetryPolicy::disabled()
142 };
143
144 let mut attempt: u32 = 0;
145
146 loop {
147 if attempt > 0 {
150 executor.reconnect().await?;
151 }
152
153 match executor.execute_command(cmd, exchange).await {
154 Ok(()) => return Ok(()),
155 Err(err) => {
156 if !is_transient_redis_error(&err) || !effective_policy.should_retry(attempt + 1) {
157 return Err(err);
158 }
159 let delay = effective_policy.delay_for(attempt);
160 tracing::warn!(
161 attempt,
162 delay_ms = delay.as_millis(),
163 error = %err,
164 "transient error — retrying"
165 );
166 tokio::time::sleep(delay).await;
167 attempt += 1;
168 }
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::time::Duration;
177
178 fn transient_err(msg: &str) -> Result<(), FakeError> {
179 Err(FakeError {
180 message: msg.to_string(),
181 is_transient: true,
182 })
183 }
184
185 fn non_transient_err(msg: &str) -> Result<(), FakeError> {
186 Err(FakeError {
187 message: msg.to_string(),
188 is_transient: false,
189 })
190 }
191
192 #[tokio::test]
193 async fn test_retry_succeeds_after_transient_failures() {
194 let executor = FakeExecutor::new(vec![
197 Ok(()),
198 transient_err("connection reset"),
199 transient_err("connection reset"),
200 transient_err("connection reset"),
201 ]);
202 let call_count = executor.call_count();
203 let reconnect_count = executor.reconnect_count();
204
205 let mut exchange = Exchange::default();
206 let cmd = RedisCommand::Get; let policy = NetworkRetryPolicy {
209 max_attempts: 10,
210 initial_delay: Duration::from_millis(1),
211 max_delay: Duration::from_millis(10),
212 ..NetworkRetryPolicy::default()
213 };
214
215 let result = execute_with_retry(
216 &mut { executor },
217 &cmd,
218 &mut exchange,
219 true, &policy,
221 )
222 .await;
223
224 assert!(result.is_ok(), "should succeed after retries: {:?}", result);
225 assert_eq!(call_count.load(Ordering::SeqCst), 4);
227 assert_eq!(reconnect_count.load(Ordering::SeqCst), 3);
229 }
230
231 #[tokio::test]
232 async fn test_retry_exhausted_after_max_retries() {
233 let mut results = vec![transient_err("connection refused"); 10];
235 results.push(transient_err("final failure")); let executor = FakeExecutor::new(results);
237 let call_count = executor.call_count();
238
239 let mut exchange = Exchange::default();
240 let cmd = RedisCommand::Get;
241
242 let policy = NetworkRetryPolicy {
243 max_attempts: 11, initial_delay: Duration::from_millis(1),
245 max_delay: Duration::from_millis(10),
246 ..NetworkRetryPolicy::default()
247 };
248
249 let result =
250 execute_with_retry(&mut { executor }, &cmd, &mut exchange, true, &policy).await;
251
252 assert!(result.is_err(), "should fail after exhausting retries");
253 assert_eq!(call_count.load(Ordering::SeqCst), 11);
255 }
256
257 #[tokio::test]
258 async fn test_non_idempotent_command_not_retried() {
259 let executor = FakeExecutor::new(vec![transient_err("connection reset")]);
260 let call_count = executor.call_count();
261
262 let mut exchange = Exchange::default();
263 let cmd = RedisCommand::Incr; let policy = NetworkRetryPolicy {
266 max_attempts: 10,
267 initial_delay: Duration::from_millis(1),
268 max_delay: Duration::from_millis(10),
269 ..NetworkRetryPolicy::default()
270 };
271
272 let result = execute_with_retry(
273 &mut { executor },
274 &cmd,
275 &mut exchange,
276 false, &policy,
278 )
279 .await;
280
281 assert!(result.is_err(), "non-idempotent should not be retried");
282 assert_eq!(call_count.load(Ordering::SeqCst), 1);
284 }
285
286 #[tokio::test]
287 async fn test_non_transient_error_not_retried() {
288 let executor = FakeExecutor::new(vec![non_transient_err("WRONGTYPE")]);
289 let call_count = executor.call_count();
290
291 let mut exchange = Exchange::default();
292 let cmd = RedisCommand::Get; let policy = NetworkRetryPolicy {
295 max_attempts: 10,
296 initial_delay: Duration::from_millis(1),
297 max_delay: Duration::from_millis(10),
298 ..NetworkRetryPolicy::default()
299 };
300
301 let result = execute_with_retry(
302 &mut { executor },
303 &cmd,
304 &mut exchange,
305 true, &policy,
307 )
308 .await;
309
310 assert!(result.is_err(), "non-transient error should not be retried");
311 assert_eq!(call_count.load(Ordering::SeqCst), 1);
313 }
314
315 #[tokio::test]
316 async fn test_idempotent_command_retried_on_transient() {
317 let executor = FakeExecutor::new(vec![
318 Ok(()),
319 transient_err("EOF"),
320 transient_err("timed out"),
321 ]);
322 let call_count = executor.call_count();
323
324 let mut exchange = Exchange::default();
325 let cmd = RedisCommand::Set; let policy = NetworkRetryPolicy {
328 max_attempts: 10,
329 initial_delay: Duration::from_millis(1),
330 max_delay: Duration::from_millis(10),
331 ..NetworkRetryPolicy::default()
332 };
333
334 let result =
335 execute_with_retry(&mut { executor }, &cmd, &mut exchange, true, &policy).await;
336
337 assert!(result.is_ok());
338 assert_eq!(call_count.load(Ordering::SeqCst), 3);
340 }
341
342 #[tokio::test]
343 async fn test_immediate_success_no_retries() {
344 let executor = FakeExecutor::new(vec![Ok(())]);
345 let call_count = executor.call_count();
346 let reconnect_count = executor.reconnect_count();
347
348 let mut exchange = Exchange::default();
349 let cmd = RedisCommand::Get;
350
351 let policy = NetworkRetryPolicy {
352 max_attempts: 10,
353 initial_delay: Duration::from_millis(1),
354 max_delay: Duration::from_millis(10),
355 ..NetworkRetryPolicy::default()
356 };
357
358 let result =
359 execute_with_retry(&mut { executor }, &cmd, &mut exchange, true, &policy).await;
360
361 assert!(result.is_ok());
362 assert_eq!(call_count.load(Ordering::SeqCst), 1);
363 assert_eq!(reconnect_count.load(Ordering::SeqCst), 0);
364 }
365
366 #[tokio::test]
367 async fn network_retry_policy_retries_on_transient() {
368 let policy = NetworkRetryPolicy {
369 max_attempts: 2,
370 initial_delay: Duration::from_millis(1),
371 max_delay: Duration::from_millis(5),
372 ..NetworkRetryPolicy::default()
373 };
374
375 let attempts = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
376 let attempts_clone = attempts.clone();
377
378 let result = retry_async::<(), _, _, _, CamelError>(
379 &policy,
380 None,
381 || {
382 let c = attempts_clone.clone();
383 async move {
384 let n = c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
385 if n == 0 {
386 Err(CamelError::ProcessorError("transient".into()))
387 } else {
388 Ok(())
389 }
390 }
391 },
392 |_| true,
393 )
394 .await;
395
396 assert!(result.is_ok());
397 assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 2);
398 }
399}