rovenue/transport/
retry.rs

1use std::time::Duration;
2
3/// We never wait more than this on a server-driven Retry-After.
4/// Beyond this the client should fail open with RateLimited.
5pub const RETRY_AFTER_MAX: Duration = Duration::from_secs(30);
6
7/// What to do with an HTTP attempt's outcome.
8#[derive(Debug, PartialEq, Eq)]
9pub enum RetryDecision {
10    /// Treat as success (2xx/3xx).
11    Success,
12    /// Should be retried with normal exponential backoff.
13    Retryable,
14    /// Should be retried only after the specified duration (server-driven).
15    RetryAfter(Duration),
16    /// Do not retry — surface immediately.
17    Fatal,
18}
19
20/// `status = None` indicates a network-level failure (no response).
21pub fn classify(status: Option<u16>, retry_after: Option<Duration>) -> RetryDecision {
22    match status {
23        None => RetryDecision::Retryable,
24        Some(s) if (500..600).contains(&s) => RetryDecision::Retryable,
25        Some(429) => match retry_after {
26            Some(d) => RetryDecision::RetryAfter(d),
27            None => RetryDecision::Retryable,
28        },
29        // Every 4xx — 409 included — is an answer the caller must see. No
30        // route the SDK calls answers 409 for "already applied": replays are
31        // deduplicated downstream behind a 2xx. A 409 is a refusal (e.g.
32        // SUBSCRIBER_UNRESOLVABLE, the identify label-switch guard,
33        // STORE_ID_TAKEN), and reading one as delivered made durable queues
34        // delete events the server never accepted.
35        Some(s) if (400..500).contains(&s) => RetryDecision::Fatal,
36        Some(_) => RetryDecision::Success,
37    }
38}
39
40/// Compute backoff for attempt index (0-based). exp 1s→2s→4s…, jitter ±20%, cap 5min.
41pub fn backoff(attempt: u32, rng: &mut impl rand::RngCore) -> Duration {
42    use rand::Rng;
43    let base = (1u64 << attempt.min(8)).saturating_mul(1000);
44    let capped = base.min(5 * 60 * 1000);
45    let jitter: i64 = rng.gen_range(-((capped as i64) / 5)..=((capped as i64) / 5));
46    let total = (capped as i64 + jitter).max(0) as u64;
47    Duration::from_millis(total)
48}