Skip to main content

loonfs_api/
retry.rs

1//! Monotonic timers and bounded transport retry policy.
2
3use std::sync::OnceLock;
4use std::time::{Duration, Instant};
5
6/// Supplies monotonic milliseconds for retry deadlines and deterministic test injection.
7pub trait MonotonicTimer: std::fmt::Debug + Send + Sync {
8    /// Returns milliseconds since an arbitrary per-timer origin.
9    fn monotonic_now_ms(&self) -> u64;
10}
11
12/// Process-clock implementation backed by [`std::time::Instant`].
13#[derive(Debug, Default)]
14pub struct StdMonotonicTimer {
15    origin: OnceLock<Instant>,
16}
17
18impl MonotonicTimer for StdMonotonicTimer {
19    fn monotonic_now_ms(&self) -> u64 {
20        // This monotonic boundary controls local retry timing, so it cannot affect durable state.
21        #[allow(clippy::disallowed_methods)]
22        let now = Instant::now();
23        let origin = self.origin.get_or_init(|| now);
24        u64::try_from(now.saturating_duration_since(*origin).as_millis()).unwrap_or(u64::MAX)
25    }
26}
27
28/// Bounded retry configuration for replay-safe transport operations.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TransportRetryPolicy {
31    /// Maximum retries after the first attempt.
32    pub max_retries: u32,
33    /// Backoff before the first retry.
34    pub initial_backoff: Duration,
35    /// Maximum backoff between attempts.
36    pub max_backoff: Duration,
37    /// Total time allowed for the logical operation.
38    pub operation_deadline: Duration,
39}
40
41/// Elapsed-time state for one logical operation's retry loop.
42pub struct OperationDeadline<'timer> {
43    timer: &'timer dyn MonotonicTimer,
44    started_ms: u64,
45    deadline: Duration,
46}
47
48impl<'timer> OperationDeadline<'timer> {
49    /// Starts a deadline at the timer's current monotonic reading.
50    pub fn start(timer: &'timer dyn MonotonicTimer, deadline: Duration) -> Self {
51        Self {
52            timer,
53            started_ms: timer.monotonic_now_ms(),
54            deadline,
55        }
56    }
57
58    /// Returns the remaining duration, or `None` once the deadline expires.
59    pub fn remaining(&self) -> Option<Duration> {
60        let elapsed_ms = self
61            .timer
62            .monotonic_now_ms()
63            .saturating_sub(self.started_ms);
64        let deadline_ms = u64::try_from(self.deadline.as_millis()).unwrap_or(u64::MAX);
65        if elapsed_ms >= deadline_ms {
66            return None;
67        }
68        Some(Duration::from_millis(deadline_ms - elapsed_ms))
69    }
70
71    /// Returns the total duration assigned to the operation.
72    pub fn deadline(&self) -> Duration {
73        self.deadline
74    }
75}
76
77/// Computes capped exponential backoff for a retry number starting at one.
78pub fn transport_retry_backoff(policy: &TransportRetryPolicy, retry: u32) -> Duration {
79    let doublings = retry.saturating_sub(1).min(16);
80    policy
81        .initial_backoff
82        .saturating_mul(1u32 << doublings)
83        .min(policy.max_backoff)
84}