Skip to main content

dialcache/
clock.rs

1//! Wall and elapsed clocks.
2
3use std::fmt;
4use std::sync::{Arc, OnceLock};
5use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
6
7/// Separates wall timestamps from monotonic elapsed time.
8///
9/// Wall time stamps frames and invalidation markers. Elapsed time governs
10/// deadlines and process-local expiry. Timers live on the [`Runtime`](crate::Runtime).
11pub trait Clock: Send + Sync + 'static {
12    /// Epoch milliseconds from the application wall clock.
13    fn wall_ms(&self) -> i64;
14    /// Monotonic elapsed time since this clock's origin, with native precision.
15    fn elapsed(&self) -> Duration;
16    /// Whole-millisecond elapsed reading used by process-local storage.
17    ///
18    /// Insertion and lookup share this integer grid, so an entry inserted at
19    /// 0.7 ms with a 1,000 ms TTL expires when this reading reaches 1,000.
20    fn elapsed_ms(&self) -> i64 {
21        self.elapsed().as_millis().min(i64::MAX as u128) as i64
22    }
23}
24
25static PROCESS_ORIGIN: OnceLock<Instant> = OnceLock::new();
26
27/// The origin of a clock constructed `now_ns` after the process origin so that
28/// its whole-millisecond readings fall on the process-wide grid: the nearest
29/// aligned instant at or before construction.
30pub fn grid_origin_ns(now_ns: u128) -> u128 {
31    now_ns - now_ns % 1_000_000
32}
33
34type MonotonicSource = Arc<dyn Fn() -> Duration + Send + Sync>;
35type WallSource = Arc<dyn Fn() -> i64 + Send + Sync>;
36
37/// The default clock: system wall time and a monotonic origin aligned to the
38/// process-wide millisecond grid, so default instances share one local expiry grid.
39#[derive(Clone)]
40pub struct SystemClock {
41    monotonic: MonotonicSource,
42    wall: WallSource,
43    /// Nanoseconds from the monotonic source's zero to this clock's origin.
44    origin_ns: u128,
45}
46
47impl SystemClock {
48    /// System sources: `SystemTime` for wall time and the `Instant` elapsed
49    /// since the first default clock of this process for monotonic time.
50    pub fn new() -> Self {
51        let process = *PROCESS_ORIGIN.get_or_init(Instant::now);
52        Self::with_sources(move || process.elapsed(), system_wall_ms)
53    }
54
55    /// The production alignment over caller-supplied sources.
56    ///
57    /// `monotonic` reads elapsed time since one fixed instant shared by every
58    /// clock built over it; this clock's origin is the nearest whole
59    /// millisecond of that reading at or before construction, so clocks built
60    /// at different fractional times share one expiry grid. `wall` supplies
61    /// epoch milliseconds. Controlled tests use this to run the default
62    /// alignment over a virtual clock.
63    pub fn with_sources(
64        monotonic: impl Fn() -> Duration + Send + Sync + 'static,
65        wall: impl Fn() -> i64 + Send + Sync + 'static,
66    ) -> Self {
67        let since_process = monotonic().as_nanos();
68        SystemClock {
69            monotonic: Arc::new(monotonic),
70            wall: Arc::new(wall),
71            origin_ns: grid_origin_ns(since_process),
72        }
73    }
74}
75
76fn system_wall_ms() -> i64 {
77    SystemTime::now()
78        .duration_since(UNIX_EPOCH)
79        .map(|d| d.as_millis().min(i64::MAX as u128) as i64)
80        .unwrap_or(0)
81}
82
83impl Default for SystemClock {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl fmt::Debug for SystemClock {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.debug_struct("SystemClock")
92            .field("origin_ns", &self.origin_ns)
93            .finish_non_exhaustive()
94    }
95}
96
97impl Clock for SystemClock {
98    fn wall_ms(&self) -> i64 {
99        (self.wall)()
100    }
101
102    fn elapsed(&self) -> Duration {
103        let now_ns = (self.monotonic)().as_nanos();
104        Duration::from_nanos(now_ns.saturating_sub(self.origin_ns).min(u64::MAX as u128) as u64)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use parking_lot::Mutex;
112
113    #[test]
114    fn clocks_built_at_different_fractions_share_one_grid() {
115        let now = Arc::new(Mutex::new(Duration::from_micros(700)));
116        let source = {
117            let now = now.clone();
118            move || *now.lock()
119        };
120        let first = SystemClock::with_sources(source.clone(), || 0);
121        *now.lock() = Duration::from_micros(1_300);
122        let second = SystemClock::with_sources(source, || 0);
123        *now.lock() = Duration::from_micros(1_000_900);
124        // Origins are 0 ms and 1 ms: the same grid, one millisecond apart.
125        assert_eq!(first.elapsed_ms(), 1_000);
126        assert_eq!(second.elapsed_ms(), 999);
127        *now.lock() = Duration::from_micros(1_001_000);
128        assert_eq!(second.elapsed_ms(), 1_000);
129    }
130
131    #[test]
132    fn default_clock_is_monotonic_and_shares_the_process_origin() {
133        let first = SystemClock::new();
134        let second = SystemClock::new();
135        let a = first.elapsed();
136        assert!(first.elapsed() >= a);
137        // Both origins lie on the process grid: whole milliseconds after the
138        // process origin, so the two instances share one expiry grid.
139        assert_eq!(first.origin_ns % 1_000_000, 0);
140        assert_eq!(second.origin_ns % 1_000_000, 0);
141        assert!(second.origin_ns >= first.origin_ns);
142        assert!(first.wall_ms() > 1_700_000_000_000);
143    }
144}