Skip to main content

taskfast_client/
retry.rs

1// SPDX-License-Identifier: MIT
2//! Retry policy and exponential-backoff driver used by [`crate::TaskFastClient`].
3//!
4//! Only `Server`, `Network`, and `RateLimited` errors are retried; `Auth`,
5//! `Validation`, and `Decode` short-circuit immediately because retrying them
6//! cannot change the outcome.
7
8use std::future::Future;
9use std::time::Duration;
10
11use crate::errors::{Error, Result};
12
13/// Configuration for [`with_backoff`].
14///
15/// `max_attempts` includes the first try (so `max_attempts = 3` means up to
16/// two retries). `base_delay` is the initial backoff; subsequent delays
17/// double via `base_delay * 2^(attempt-1)`.
18#[derive(Debug, Clone, Copy)]
19pub struct RetryPolicy {
20    /// Total attempts including the first try.
21    pub max_attempts: u32,
22    /// Initial backoff duration; doubles each retry.
23    pub base_delay: Duration,
24}
25
26impl Default for RetryPolicy {
27    fn default() -> Self {
28        Self {
29            max_attempts: 3,
30            base_delay: Duration::from_millis(500),
31        }
32    }
33}
34
35/// Run `op` under `policy`, retrying transient failures with exponential backoff.
36///
37/// Retries on [`Error::Server`] / [`Error::Network`] (doubled `base_delay`) and
38/// [`Error::RateLimited`] (sleeps for the server-supplied `retry_after`). Any
39/// other variant — or exhausting `max_attempts` — propagates the last error.
40///
41/// `op` receives the 1-indexed attempt number so callers can log per-try state.
42pub async fn with_backoff<T, F, Fut>(policy: RetryPolicy, mut op: F) -> Result<T>
43where
44    F: FnMut(u32) -> Fut,
45    Fut: Future<Output = Result<T>>,
46{
47    let mut attempt = 1u32;
48    loop {
49        match op(attempt).await {
50            Ok(v) => return Ok(v),
51            Err(Error::Server(_) | Error::Network(_)) if attempt < policy.max_attempts => {
52                let delay = policy.base_delay * 2u32.pow(attempt - 1);
53                tracing::warn!(attempt, ?delay, "retrying transient error");
54                tokio::time::sleep(delay).await;
55                attempt += 1;
56            }
57            Err(Error::RateLimited { retry_after }) if attempt < policy.max_attempts => {
58                tracing::warn!(attempt, ?retry_after, "rate limited, honoring retry_after");
59                tokio::time::sleep(retry_after).await;
60                attempt += 1;
61            }
62            Err(e) => return Err(e),
63        }
64    }
65}