Skip to main content

ferrin_core/
retry.rs

1//! Retry policy and the retry loop used around provider calls.
2//!
3//! Derived from the Vercel AI SDK (Apache-2.0, Copyright 2023 Vercel, Inc.),
4//! translated from TypeScript to Rust and modified; see `NOTICE`.
5
6use 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/// How failed provider calls are retried.
20#[derive(Debug, Clone, PartialEq)]
21pub struct RetryPolicy {
22    /// Maximum number of retries after the first attempt (default 2).
23    pub max_retries: u32,
24    /// Delay before the first retry (default 2 s).
25    pub initial_delay: Duration,
26    /// Multiplier applied to the delay after each retry (default 2.0).
27    pub backoff_factor: f64,
28    /// Largest `Retry-After` value that is honoured (default 60 s).
29    pub max_retry_after: Duration,
30    /// Random jitter applied to the computed delay (default none).
31    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    /// Creates the default policy with a different retry count.
48    #[must_use]
49    pub fn with_max_retries(max_retries: u32) -> Self {
50        Self {
51            max_retries,
52            ..Self::default()
53        }
54    }
55
56    /// Disables retries.
57    #[must_use]
58    pub fn none() -> Self {
59        Self::with_max_retries(0)
60    }
61
62    /// Computes the delay before retry number `retry` (1-based), honouring
63    /// `Retry-After` headers when they fall within `max_retry_after`.
64    ///
65    /// Arithmetic overflow saturates at [`Duration::MAX`]. Provider calls
66    /// reject negative or non-finite backoff factors before using this policy.
67    #[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/// Jitter applied to retry delays.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101#[non_exhaustive]
102pub enum Jitter {
103    /// No jitter: deterministic delays.
104    #[default]
105    None,
106    /// Uniformly random delay between zero and the computed delay.
107    Full,
108}
109
110/// Runs `operation` with retries.
111///
112/// `operation` receives the attempt number (0 for the first try). Only
113/// [`Error::Provider`] failures are retried: a non-retryable error on the
114/// first try is returned unwrapped, later failures are reported as
115/// [`Error::Retry`], cancellation stops immediately and every other error is
116/// returned as is.
117pub(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
129/// Like [`retry`], with a custom decision of which provider errors are
130/// retried.
131pub(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}