Skip to main content

wowlab_supabase/
retry.rs

1use std::time::Duration;
2
3use tokio::time::sleep;
4use wowlab_common::retry::ExponentialSchedule;
5
6use crate::{Result, SupabaseError};
7
8const DEFAULT_MAX_ATTEMPTS: u32 = 3;
9const DEFAULT_INITIAL_DELAY_MS: u64 = 100;
10const DEFAULT_MAX_DELAY_MS: u64 = 5000;
11const DEFAULT_BACKOFF_FACTOR: u32 = 2;
12
13/// Bounded retry policy for Supabase operations.
14#[derive(Clone, Debug)]
15pub struct RetryConfig {
16    /// Maximum operation invocations, with zero retaining the single initial invocation.
17    pub max_attempts: u32,
18    pub initial_delay_ms: u64,
19    pub max_delay_ms: u64,
20    pub backoff_factor: u32,
21}
22
23impl RetryConfig {
24    fn schedule(&self) -> ExponentialSchedule {
25        ExponentialSchedule::new(
26            Duration::from_millis(self.initial_delay_ms),
27            Duration::from_millis(self.max_delay_ms),
28            self.backoff_factor,
29        )
30    }
31}
32
33impl Default for RetryConfig {
34    fn default() -> Self {
35        Self {
36            max_attempts: DEFAULT_MAX_ATTEMPTS,
37            initial_delay_ms: DEFAULT_INITIAL_DELAY_MS,
38            max_delay_ms: DEFAULT_MAX_DELAY_MS,
39            backoff_factor: DEFAULT_BACKOFF_FACTOR,
40        }
41    }
42}
43
44fn log_retry(attempt: u32, max_attempts: u32, delay: Duration, error: &SupabaseError) {
45    tracing::warn!(attempt, max_attempts, ?delay, %error, "Supabase request failed; retrying");
46}
47
48/// Runs an asynchronous Supabase operation using the configured retry policy.
49///
50/// # Errors
51///
52/// Returns the final operation error when it is not retryable or the maximum number of attempts is reached.
53pub async fn with_retry<T, F, Fut>(config: &RetryConfig, mut operation: F) -> Result<T>
54where
55    F: FnMut() -> Fut,
56    Fut: Future<Output = Result<T>>,
57{
58    let mut schedule = config.schedule();
59    let mut attempt = 0;
60
61    loop {
62        attempt += 1;
63
64        match operation().await {
65            Ok(result) => return Ok(result),
66            Err(error) if error.is_retryable() && attempt < config.max_attempts => {
67                let delay = error
68                    .retry_after_ms()
69                    .map_or_else(|| schedule.next_delay(), Duration::from_millis);
70
71                log_retry(attempt, config.max_attempts, delay, &error);
72                sleep(delay).await;
73            }
74            Err(error) => return Err(error),
75        }
76    }
77}