1use crate::config::RetryConfig;
4use crate::traits::IsRetryable;
5
6pub async fn with_backoff<F, Fut, T, E>(
22 config: &RetryConfig,
23 mut f: F,
24) -> Result<T, RetryError<E>>
25where
26 F: FnMut() -> Fut,
27 Fut: std::future::Future<Output = Result<T, E>>,
28 E: IsRetryable + std::fmt::Display,
29{
30 let mut last_error = None;
31
32 for attempt in 0..=config.max_retries {
33 match f().await {
34 Ok(value) => return Ok(value),
35 Err(err) => {
36 if !err.is_retryable() || attempt == config.max_retries {
37 return Err(RetryError::FinalError(err, attempt));
38 }
39
40 let delay = config.delay_for_attempt(attempt);
41 last_error = Some(err);
42 tokio::time::sleep(delay).await;
43 }
44 }
45 }
46
47 Err(RetryError::FinalError(
48 last_error.expect("loop must have executed at least once"),
49 config.max_retries,
50 ))
51}
52
53#[derive(Debug, thiserror::Error)]
55pub enum RetryError<E> {
56 #[error("all {1} retries exhausted: {0}")]
58 FinalError(E, u32),
59}
60
61impl<E: IsRetryable + std::fmt::Display> RetryError<E> {
62 pub fn into_inner(self) -> E {
64 match self {
65 Self::FinalError(e, _) => e,
66 }
67 }
68
69 pub fn retries(&self) -> u32 {
71 match self {
72 Self::FinalError(_, r) => *r,
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use std::sync::atomic::{AtomicU32, Ordering};
81 use std::sync::Arc;
82 use std::time::Duration;
83
84 #[derive(Debug)]
85 struct TestError(bool);
86
87 impl std::fmt::Display for TestError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "test error (retryable={})", self.0)
90 }
91 }
92
93 impl IsRetryable for TestError {
94 fn is_retryable(&self) -> bool {
95 self.0
96 }
97 }
98
99 #[tokio::test]
100 async fn succeeds_on_first_try() {
101 let config = RetryConfig::no_retries();
102 let result = with_backoff(&config, || async { Ok::<_, TestError>(42) }).await;
103 assert_eq!(result.unwrap(), 42);
104 }
105
106 #[tokio::test]
107 async fn retries_then_succeeds() {
108 let attempts = Arc::new(AtomicU32::new(0));
109 let attempts_clone = attempts.clone();
110 let config = RetryConfig {
111 initial_delay: Duration::from_millis(1),
112 max_retries: 3,
113 jitter: false,
114 ..Default::default()
115 };
116
117 let result = with_backoff(&config, || {
118 let a = attempts_clone.clone();
119 async move {
120 let count = a.fetch_add(1, Ordering::Relaxed);
121 if count < 2 {
122 Err(TestError(true))
123 } else {
124 Ok(42)
125 }
126 }
127 })
128 .await;
129
130 assert_eq!(result.unwrap(), 42);
131 assert_eq!(attempts.load(Ordering::Relaxed), 3);
132 }
133
134 #[tokio::test]
135 async fn non_retryable_error_returns_immediately() {
136 let config = RetryConfig::aggressive();
137 let result = with_backoff(&config, || async { Err::<i32, _>(TestError(false)) }).await;
138 assert!(result.is_err());
139 match result.unwrap_err() {
140 RetryError::FinalError(_, retries) => assert_eq!(retries, 0),
141 }
142 }
143
144 #[tokio::test]
145 async fn max_retries_exhausted() {
146 let config = RetryConfig {
147 initial_delay: Duration::from_millis(1),
148 max_retries: 2,
149 jitter: false,
150 ..Default::default()
151 };
152
153 let result = with_backoff(&config, || async { Err::<i32, _>(TestError(true)) }).await;
154 assert!(result.is_err());
155 match result.unwrap_err() {
156 RetryError::FinalError(_, retries) => assert_eq!(retries, 2),
157 }
158 }
159}