1use std::future::Future;
7use std::time::Duration;
8
9use ferrin_provider_util::retry::retry_after_within;
10use ferrin_spec::error::ProviderError;
11use futures_util::future::Either;
12use futures_util::future::select;
13use rand::RngExt;
14use tokio_util::sync::CancellationToken;
15
16use crate::error::Error;
17use crate::error::RetryReason;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct RetryPolicy {
22 pub max_retries: u32,
24 pub initial_delay: Duration,
26 pub backoff_factor: f64,
28 pub max_retry_after: Duration,
30 pub jitter: Jitter,
32}
33
34impl Default for RetryPolicy {
35 fn default() -> Self {
36 Self {
37 max_retries: 2,
38 initial_delay: Duration::from_secs(2),
39 backoff_factor: 2.0,
40 max_retry_after: Duration::from_secs(60),
41 jitter: Jitter::None,
42 }
43 }
44}
45
46impl RetryPolicy {
47 #[must_use]
49 pub fn with_max_retries(max_retries: u32) -> Self {
50 Self {
51 max_retries,
52 ..Self::default()
53 }
54 }
55
56 #[must_use]
58 pub fn none() -> Self {
59 Self::with_max_retries(0)
60 }
61
62 #[must_use]
68 pub fn delay_for(&self, retry: u32, error: &ProviderError) -> Duration {
69 let header_delay = error
70 .as_api_call()
71 .and_then(|api| api.response_headers.as_ref())
72 .and_then(|headers| retry_after_within(headers, self.max_retry_after));
73 let base = header_delay.unwrap_or_else(|| {
74 let exponent = retry.saturating_sub(1);
75 let factor = self
76 .backoff_factor
77 .powi(i32::try_from(exponent).unwrap_or(i32::MAX));
78 if self.initial_delay.is_zero() {
79 Duration::ZERO
80 } else {
81 Duration::try_from_secs_f64(self.initial_delay.as_secs_f64() * factor.max(0.0))
82 .unwrap_or(Duration::MAX)
83 }
84 });
85 match self.jitter {
86 Jitter::None => base,
87 Jitter::Full => {
88 let millis = u64::try_from(base.as_millis()).unwrap_or(u64::MAX);
89 if millis == 0 {
90 base
91 } else {
92 Duration::from_millis(rand::rng().random_range(0..=millis))
93 }
94 }
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101#[non_exhaustive]
102pub enum Jitter {
103 #[default]
105 None,
106 Full,
108}
109
110pub(crate) async fn retry<T, F, Fut>(
118 policy: &RetryPolicy,
119 cancellation: &CancellationToken,
120 operation: F,
121) -> Result<T, Error>
122where
123 F: FnMut(u32) -> Fut,
124 Fut: Future<Output = Result<T, Error>>,
125{
126 retry_with(policy, cancellation, ProviderError::is_retryable, operation).await
127}
128
129pub(crate) async fn retry_with<T, F, Fut>(
132 policy: &RetryPolicy,
133 cancellation: &CancellationToken,
134 is_retryable: impl Fn(&ProviderError) -> bool,
135 mut operation: F,
136) -> Result<T, Error>
137where
138 F: FnMut(u32) -> Fut,
139 Fut: Future<Output = Result<T, Error>>,
140{
141 if !policy.backoff_factor.is_finite() || policy.backoff_factor < 0.0 {
142 return Err(Error::invalid_argument(
143 "retry_policy.backoff_factor",
144 "must be finite and nonnegative",
145 ));
146 }
147 let mut errors: Vec<ProviderError> = Vec::new();
148 loop {
149 if cancellation.is_cancelled() {
150 return Err(abort_error(errors));
151 }
152 let attempt = u32::try_from(errors.len()).unwrap_or(u32::MAX);
153 match operation(attempt).await {
154 Ok(value) => return Ok(value),
155 Err(Error::Cancelled) => return Err(abort_error(errors)),
156 Err(Error::Provider(error)) => {
157 let error = *error;
158 if policy.max_retries == 0 {
159 return Err(Error::from(error));
160 }
161 let retry_number = attempt.saturating_add(1);
162 let retryable = retry_number <= policy.max_retries && is_retryable(&error);
163 let delay = retryable.then(|| policy.delay_for(retry_number, &error));
164 errors.push(error);
165 let attempts = retry_number;
166 if attempts > policy.max_retries {
167 return Err(Error::Retry {
168 reason: RetryReason::MaxRetriesExceeded,
169 attempts,
170 errors,
171 });
172 }
173 if !retryable {
174 if attempts == 1 {
175 return Err(Error::from(
176 errors.pop().unwrap_or(ProviderError::Cancelled),
177 ));
178 }
179 return Err(Error::Retry {
180 reason: RetryReason::ErrorNotRetryable,
181 attempts,
182 errors,
183 });
184 }
185 let sleep = Box::pin(async {
186 match tokio::time::Instant::now().checked_add(delay.unwrap_or_default()) {
187 Some(deadline) => tokio::time::sleep_until(deadline).await,
188 None => std::future::pending().await,
189 }
190 });
191 let cancelled = Box::pin(cancellation.cancelled());
192 if let Either::Right(_) = select(sleep, cancelled).await {
193 return Err(Error::Retry {
194 reason: RetryReason::Abort,
195 attempts,
196 errors,
197 });
198 }
199 }
200 Err(other) => return Err(other),
201 }
202 }
203}
204
205fn abort_error(errors: Vec<ProviderError>) -> Error {
206 if errors.is_empty() {
207 Error::Cancelled
208 } else {
209 Error::Retry {
210 reason: RetryReason::Abort,
211 attempts: u32::try_from(errors.len()).unwrap_or(u32::MAX),
212 errors,
213 }
214 }
215}