spacetraders-client 0.1.0

Async Rust client for the SpaceTraders API v2 with priority-aware rate limiting and retry
Documentation
use tokio::time::Duration;

/// Automatic-retry budget for transient failures (429 / 5xx / communication).
pub(crate) const MAX_RETRIES: u32 = 4;
/// Base delay for exponential backoff when the server gives no `Retry-After`.
pub(crate) const BASE_BACKOFF: Duration = Duration::from_millis(500);
/// Upper bound on a single backoff sleep.
pub(crate) const MAX_BACKOFF: Duration = Duration::from_secs(30);

/// Deterministic exponential backoff (no jitter): `BASE_BACKOFF * 2^attempt`,
/// capped at `MAX_BACKOFF`.
pub(crate) fn backoff_base(attempt: u32) -> Duration {
    BASE_BACKOFF
        .saturating_mul(1u32 << attempt.min(20))
        .min(MAX_BACKOFF)
}

/// A pseudo-random duration in `[0, span]`. Seeded from the wall clock so it is
/// independent of any paused test clock; the precision here is irrelevant.
fn jitter(span: Duration) -> Duration {
    if span.is_zero() {
        return Duration::ZERO;
    }
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.subsec_nanos())
        .unwrap_or(0);
    span.mul_f64(nanos as f64 / 1_000_000_000.0)
}

/// Backoff for retry attempt `attempt` (0-based): half of the exponential delay
/// is fixed and the other half is randomized to avoid a thundering herd.
pub(crate) fn backoff_delay(attempt: u32) -> Duration {
    let half = backoff_base(attempt) / 2;
    half + jitter(half)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn backoff_base_grows_and_caps() {
        assert_eq!(backoff_base(0), BASE_BACKOFF);
        assert_eq!(backoff_base(1), BASE_BACKOFF * 2);
        assert_eq!(backoff_base(2), BASE_BACKOFF * 4);
        assert_eq!(backoff_base(30), MAX_BACKOFF);
    }
}