Skip to main content

ferrin_core/
retry.rs

1//! Retry policy and the retry loop used around provider calls.
2
3use std::future::Future;
4use std::time::Duration;
5
6use ferrin_provider_util::retry::retry_after_within;
7use ferrin_spec::error::ProviderError;
8use futures_util::future::Either;
9use futures_util::future::select;
10use rand::RngExt;
11use tokio_util::sync::CancellationToken;
12
13use crate::error::Error;
14use crate::error::RetryReason;
15
16/// How failed provider calls are retried.
17#[derive(Debug, Clone, PartialEq)]
18pub struct RetryPolicy {
19    /// Maximum number of retries after the first attempt (default 2).
20    pub max_retries: u32,
21    /// Delay before the first retry (default 2 s).
22    pub initial_delay: Duration,
23    /// Multiplier applied to the delay after each retry (default 2.0).
24    pub backoff_factor: f64,
25    /// Largest `Retry-After` value that is honoured (default 60 s).
26    pub max_retry_after: Duration,
27    /// Random jitter applied to the computed delay (default none).
28    pub jitter: Jitter,
29}
30
31impl Default for RetryPolicy {
32    fn default() -> Self {
33        Self {
34            max_retries: 2,
35            initial_delay: Duration::from_secs(2),
36            backoff_factor: 2.0,
37            max_retry_after: Duration::from_secs(60),
38            jitter: Jitter::None,
39        }
40    }
41}
42
43impl RetryPolicy {
44    /// Creates the default policy with a different retry count.
45    #[must_use]
46    pub fn with_max_retries(max_retries: u32) -> Self {
47        Self {
48            max_retries,
49            ..Self::default()
50        }
51    }
52
53    /// Disables retries.
54    #[must_use]
55    pub fn none() -> Self {
56        Self::with_max_retries(0)
57    }
58
59    /// Computes the delay before retry number `retry` (1-based), honouring
60    /// `Retry-After` headers when they fall within `max_retry_after`.
61    #[must_use]
62    pub fn delay_for(&self, retry: u32, error: &ProviderError) -> Duration {
63        let header_delay = error
64            .as_api_call()
65            .and_then(|api| api.response_headers.as_ref())
66            .and_then(|headers| retry_after_within(headers, self.max_retry_after));
67        let base = header_delay.unwrap_or_else(|| {
68            let exponent = retry.saturating_sub(1);
69            let factor = self
70                .backoff_factor
71                .powi(i32::try_from(exponent).unwrap_or(i32::MAX));
72            self.initial_delay.mul_f64(factor.max(0.0))
73        });
74        match self.jitter {
75            Jitter::None => base,
76            Jitter::Full => {
77                let millis = u64::try_from(base.as_millis()).unwrap_or(u64::MAX);
78                if millis == 0 {
79                    base
80                } else {
81                    Duration::from_millis(rand::rng().random_range(0..=millis))
82                }
83            }
84        }
85    }
86}
87
88/// Jitter applied to retry delays.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90#[non_exhaustive]
91pub enum Jitter {
92    /// No jitter: deterministic delays.
93    #[default]
94    None,
95    /// Uniformly random delay between zero and the computed delay.
96    Full,
97}
98
99/// Runs `operation` with retries.
100///
101/// `operation` receives the attempt number (0 for the first try). Only
102/// [`Error::Provider`] failures are retried: a non-retryable error on the
103/// first try is returned unwrapped, later failures are reported as
104/// [`Error::Retry`], cancellation stops immediately and every other error is
105/// returned as is.
106pub(crate) async fn retry<T, F, Fut>(
107    policy: &RetryPolicy,
108    cancellation: &CancellationToken,
109    operation: F,
110) -> Result<T, Error>
111where
112    F: FnMut(u32) -> Fut,
113    Fut: Future<Output = Result<T, Error>>,
114{
115    retry_with(policy, cancellation, ProviderError::is_retryable, operation).await
116}
117
118/// Like [`retry`], with a custom decision of which provider errors are
119/// retried.
120pub(crate) async fn retry_with<T, F, Fut>(
121    policy: &RetryPolicy,
122    cancellation: &CancellationToken,
123    is_retryable: impl Fn(&ProviderError) -> bool,
124    mut operation: F,
125) -> Result<T, Error>
126where
127    F: FnMut(u32) -> Fut,
128    Fut: Future<Output = Result<T, Error>>,
129{
130    let mut errors: Vec<ProviderError> = Vec::new();
131    loop {
132        if cancellation.is_cancelled() {
133            return Err(abort_error(errors));
134        }
135        let attempt = u32::try_from(errors.len()).unwrap_or(u32::MAX);
136        match operation(attempt).await {
137            Ok(value) => return Ok(value),
138            Err(Error::Cancelled) => return Err(abort_error(errors)),
139            Err(Error::Provider(error)) => {
140                let error = *error;
141                if policy.max_retries == 0 {
142                    return Err(Error::from(error));
143                }
144                let retryable = is_retryable(&error);
145                let retry_number = attempt.saturating_add(1);
146                let delay = policy.delay_for(retry_number, &error);
147                errors.push(error);
148                let attempts = retry_number;
149                if attempts > policy.max_retries {
150                    return Err(Error::Retry {
151                        reason: RetryReason::MaxRetriesExceeded,
152                        attempts,
153                        errors,
154                    });
155                }
156                if !retryable {
157                    if attempts == 1 {
158                        return Err(Error::from(
159                            errors.pop().unwrap_or(ProviderError::Cancelled),
160                        ));
161                    }
162                    return Err(Error::Retry {
163                        reason: RetryReason::ErrorNotRetryable,
164                        attempts,
165                        errors,
166                    });
167                }
168                let sleep = Box::pin(tokio::time::sleep(delay));
169                let cancelled = Box::pin(cancellation.cancelled());
170                if let Either::Right(_) = select(sleep, cancelled).await {
171                    return Err(Error::Retry {
172                        reason: RetryReason::Abort,
173                        attempts,
174                        errors,
175                    });
176                }
177            }
178            Err(other) => return Err(other),
179        }
180    }
181}
182
183fn abort_error(errors: Vec<ProviderError>) -> Error {
184    if errors.is_empty() {
185        Error::Cancelled
186    } else {
187        Error::Retry {
188            reason: RetryReason::Abort,
189            attempts: u32::try_from(errors.len()).unwrap_or(u32::MAX),
190            errors,
191        }
192    }
193}