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}