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#[derive(Clone, Debug)]
15pub struct RetryConfig {
16 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
48pub 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}