Skip to main content

amalgam/
time.rs

1//! Time abstractions.
2//!
3//! Cache logic must never read the wall clock directly (it would be untestable
4//! and non-deterministic). Instead, every timestamp flows from an injected
5//! [`Clock`]. This mirrors FusionCache's use of `DateTimeOffset.UtcNow.UtcTicks`
6//! while keeping the domain pure: see the "Time" rule in the project guidelines.
7//!
8//! Two distinct notions of time exist in a hybrid cache:
9//!
10//! * **Logical / physical expiration** within a node — handled by comparing
11//!   [`Timestamp`]s produced by the same [`Clock`].
12//! * **Cross-node ordering** (backplane messages, tag markers, "newer wins") —
13//!   also a [`Timestamp`], deliberately a wall-clock value so independent nodes
14//!   can compare them.
15
16use std::sync::atomic::{AtomicI64, Ordering};
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18
19/// Number of 100-nanosecond ticks in one second.
20///
21/// A tick is the same unit FusionCache uses (`DateTime.Ticks`), chosen so that
22/// sub-microsecond expiration math stays in cheap integer arithmetic.
23pub const TICKS_PER_SECOND: i64 = 10_000_000;
24
25const NANOS_PER_TICK: i64 = 100;
26
27/// A point in time, measured in 100-nanosecond ticks since the Unix epoch.
28///
29/// Unlike a raw `i64`, a `Timestamp` cannot be accidentally mixed with a
30/// duration or another integer quantity — it is a value object with explicit,
31/// total ordering. It is `Copy` and allocation-free.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct Timestamp(i64);
34
35impl Timestamp {
36    /// The earliest representable timestamp.
37    pub const MIN: Timestamp = Timestamp(i64::MIN);
38    /// The latest representable timestamp.
39    pub const MAX: Timestamp = Timestamp(i64::MAX);
40
41    /// Creates a timestamp from raw 100ns ticks since the Unix epoch.
42    #[must_use]
43    pub const fn from_ticks(ticks: i64) -> Self {
44        Self(ticks)
45    }
46
47    /// Returns the raw 100ns tick count since the Unix epoch.
48    #[must_use]
49    pub const fn ticks(self) -> i64 {
50        self.0
51    }
52
53    /// Adds a [`Duration`], saturating at [`Timestamp::MAX`] instead of
54    /// overflowing. Used to derive expiration points from a "now".
55    #[must_use]
56    pub fn saturating_add(self, duration: Duration) -> Self {
57        Self(self.0.saturating_add(duration_to_ticks(duration)))
58    }
59
60    /// Returns the duration elapsed from `earlier` to `self`, or
61    /// [`Duration::ZERO`] if `self` is not after `earlier`.
62    #[must_use]
63    pub fn saturating_duration_since(self, earlier: Timestamp) -> Duration {
64        let delta = self.0.saturating_sub(earlier.0);
65        if delta <= 0 {
66            Duration::ZERO
67        } else {
68            ticks_to_duration(delta)
69        }
70    }
71
72    /// `true` if `self` is strictly before `other`.
73    #[must_use]
74    pub fn is_before(self, other: Timestamp) -> bool {
75        self < other
76    }
77}
78
79/// Converts a [`Duration`] to 100ns ticks, saturating on overflow.
80#[must_use]
81pub fn duration_to_ticks(duration: Duration) -> i64 {
82    let nanos = duration.as_nanos();
83    let ticks = nanos / (NANOS_PER_TICK as u128);
84    i64::try_from(ticks).unwrap_or(i64::MAX)
85}
86
87/// Converts a non-negative tick count to a [`Duration`].
88#[must_use]
89pub fn ticks_to_duration(ticks: i64) -> Duration {
90    let nanos = (ticks.max(0) as u64).saturating_mul(NANOS_PER_TICK as u64);
91    Duration::from_nanos(nanos)
92}
93
94/// Source of the current time.
95///
96/// Inject a custom implementation in tests to make every expiration, throttle
97/// and timeout window deterministic. Production code uses [`SystemClock`].
98pub trait Clock: Send + Sync {
99    /// Returns the current wall-clock instant as a [`Timestamp`].
100    fn now(&self) -> Timestamp;
101}
102
103impl<T: Clock + ?Sized> Clock for std::sync::Arc<T> {
104    fn now(&self) -> Timestamp {
105        (**self).now()
106    }
107}
108
109/// The real system clock, backed by [`SystemTime`].
110#[derive(Debug, Clone, Copy, Default)]
111pub struct SystemClock;
112
113impl Clock for SystemClock {
114    fn now(&self) -> Timestamp {
115        let since_epoch = SystemTime::now()
116            .duration_since(UNIX_EPOCH)
117            .unwrap_or(Duration::ZERO);
118        Timestamp(duration_to_ticks(since_epoch))
119    }
120}
121
122/// A manually-controlled clock for tests.
123///
124/// Start at an arbitrary epoch and [`advance`](ManualClock::advance) it to drive
125/// expiration, throttling and timeout behaviour deterministically.
126#[derive(Debug)]
127pub struct ManualClock {
128    ticks: AtomicI64,
129}
130
131impl ManualClock {
132    /// Creates a clock starting at the given number of ticks since the epoch.
133    #[must_use]
134    pub fn new(start: Timestamp) -> Self {
135        Self {
136            ticks: AtomicI64::new(start.0),
137        }
138    }
139
140    /// Moves the clock forward by `duration`.
141    pub fn advance(&self, duration: Duration) {
142        self.ticks
143            .fetch_add(duration_to_ticks(duration), Ordering::SeqCst);
144    }
145
146    /// Sets the clock to an absolute timestamp.
147    pub fn set(&self, at: Timestamp) {
148        self.ticks.store(at.0, Ordering::SeqCst);
149    }
150}
151
152impl Default for ManualClock {
153    fn default() -> Self {
154        // An arbitrary, comfortably-positive starting point (~2001-09-09).
155        Self::new(Timestamp(1_000_000_000 * TICKS_PER_SECOND))
156    }
157}
158
159impl Clock for ManualClock {
160    fn now(&self) -> Timestamp {
161        Timestamp(self.ticks.load(Ordering::SeqCst))
162    }
163}
164
165/// A timeout that is either unbounded or a finite [`Duration`].
166///
167/// FusionCache encodes "no timeout" as `Timeout.InfiniteTimeSpan` (a `-1ms`
168/// sentinel). Modelling that as a negative `Duration` is a footgun; an explicit
169/// two-variant enum makes "infinite" a first-class, unmistakable state and keeps
170/// every illegal negative-duration combination unrepresentable.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
172pub enum Timeout {
173    /// Wait forever / never time out.
174    #[default]
175    Infinite,
176    /// Time out after the given finite duration.
177    After(Duration),
178}
179
180impl Timeout {
181    /// Builds a timeout from an optional duration (`None` ⇒ [`Timeout::Infinite`]).
182    #[must_use]
183    pub fn from_option(duration: Option<Duration>) -> Self {
184        match duration {
185            Some(d) => Timeout::After(d),
186            None => Timeout::Infinite,
187        }
188    }
189
190    /// `true` if this timeout never fires.
191    #[must_use]
192    pub fn is_infinite(self) -> bool {
193        matches!(self, Timeout::Infinite)
194    }
195
196    /// `true` if this is a finite, zero-length timeout (fire immediately).
197    #[must_use]
198    pub fn is_immediate(self) -> bool {
199        matches!(self, Timeout::After(d) if d.is_zero())
200    }
201
202    /// The finite duration, or `None` when infinite.
203    #[must_use]
204    pub fn as_duration(self) -> Option<Duration> {
205        match self {
206            Timeout::Infinite => None,
207            Timeout::After(d) => Some(d),
208        }
209    }
210
211    /// Returns the shorter of two timeouts (infinite is treated as longest).
212    #[must_use]
213    pub fn min(self, other: Timeout) -> Timeout {
214        match (self, other) {
215            (Timeout::Infinite, o) => o,
216            (s, Timeout::Infinite) => s,
217            (Timeout::After(a), Timeout::After(b)) => Timeout::After(a.min(b)),
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn duration_round_trips_through_ticks() {
228        let d = Duration::from_millis(1500);
229        assert_eq!(ticks_to_duration(duration_to_ticks(d)), d);
230    }
231
232    #[test]
233    fn saturating_add_then_since_recovers_duration() {
234        let now = Timestamp::from_ticks(5 * TICKS_PER_SECOND);
235        let later = now.saturating_add(Duration::from_secs(3));
236        assert_eq!(later.saturating_duration_since(now), Duration::from_secs(3));
237        assert_eq!(now.saturating_duration_since(later), Duration::ZERO);
238    }
239
240    #[test]
241    fn manual_clock_advances() {
242        let clock = ManualClock::new(Timestamp::from_ticks(0));
243        assert_eq!(clock.now(), Timestamp::from_ticks(0));
244        clock.advance(Duration::from_secs(2));
245        assert_eq!(clock.now(), Timestamp::from_ticks(2 * TICKS_PER_SECOND));
246    }
247
248    #[test]
249    fn timeout_min_treats_infinite_as_longest() {
250        assert_eq!(
251            Timeout::Infinite.min(Timeout::After(Duration::from_secs(1))),
252            Timeout::After(Duration::from_secs(1))
253        );
254        assert_eq!(
255            Timeout::After(Duration::from_secs(2)).min(Timeout::After(Duration::from_secs(1))),
256            Timeout::After(Duration::from_secs(1))
257        );
258        assert_eq!(Timeout::Infinite.min(Timeout::Infinite), Timeout::Infinite);
259    }
260}