Skip to main content

tsoracle_core/
clock.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24// #[PerformanceCriticalPath]
25//! Source of physical-time milliseconds for the TSO algorithm.
26//!
27//! The Allocator's monotonicity is independent of clock correctness — a clock
28//! jumping backward cannot cause timestamp regression because the persisted
29//! high-water always wins. A clock pinned far in the past stalls new windows
30//! until wall time catches up past the persisted high-water.
31
32#[cfg(any(test, feature = "test-clock"))]
33use core::sync::atomic::{AtomicU64, Ordering};
34
35pub trait Clock: Send + Sync + 'static {
36    /// Milliseconds since Unix epoch.
37    fn now_ms(&self) -> u64;
38}
39
40/// Default implementation backed by `std::time::SystemTime`.
41///
42/// The conversion to `u64` milliseconds saturates at both ends rather than
43/// wrapping or panicking, so a misconfigured clock surfaces visibly instead of
44/// silently stalling window advance:
45///
46/// - A time so far in the future that the millisecond count exceeds `u64::MAX`
47///   saturates to `u64::MAX`. A bare `as u64` cast would wrap to a small value,
48///   making a far-future clock masquerade as the distant past and stall the
49///   allocator — the opposite of the truth. `u64::MAX` instead drives the
50///   allocator straight into visible window exhaustion.
51/// - A pre-Unix-epoch time saturates to `0` (the earliest representable
52///   instant). Per the module docs, a clock pinned in the past stalls new
53///   windows until wall time catches up past the persisted high-water; `0` is
54///   the faithful representation of such a clock, not a swallowed error.
55pub struct SystemClock;
56
57impl Clock for SystemClock {
58    fn now_ms(&self) -> u64 {
59        std::time::SystemTime::now()
60            .duration_since(std::time::UNIX_EPOCH)
61            .map(saturating_millis)
62            .unwrap_or(0)
63    }
64}
65
66/// Milliseconds in `d`, saturating to `u64::MAX` rather than truncating when
67/// the count overflows `u64`.
68fn saturating_millis(d: std::time::Duration) -> u64 {
69    u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
70}
71
72#[cfg(any(test, feature = "test-clock"))]
73pub mod testing {
74    use super::*;
75    use std::sync::Arc;
76
77    /// Hand-driven clock for deterministic tests.
78    #[derive(Clone, Default)]
79    pub struct MockClock {
80        ms: Arc<AtomicU64>,
81    }
82
83    impl MockClock {
84        pub fn new(start_ms: u64) -> Self {
85            MockClock {
86                ms: Arc::new(AtomicU64::new(start_ms)),
87            }
88        }
89        pub fn advance(&self, by_ms: u64) {
90            self.ms.fetch_add(by_ms, Ordering::AcqRel);
91        }
92        pub fn set(&self, to_ms: u64) {
93            self.ms.store(to_ms, Ordering::Release);
94        }
95    }
96
97    impl Clock for MockClock {
98        fn now_ms(&self) -> u64 {
99            self.ms.load(Ordering::Acquire)
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use testing::MockClock;
108
109    #[test]
110    fn system_clock_returns_nonzero() {
111        let clock = SystemClock;
112        let now = clock.now_ms();
113        assert!(now > 1_700_000_000_000, "current time after 2023-11");
114    }
115
116    #[test]
117    fn saturating_millis_passes_through_in_range() {
118        use std::time::Duration;
119        assert_eq!(saturating_millis(Duration::from_millis(123)), 123);
120    }
121
122    #[test]
123    fn saturating_millis_saturates_instead_of_truncating() {
124        use std::time::Duration;
125        // u64::MAX seconds is ~1000x more milliseconds than u64 can hold, so a
126        // bare `as u64` cast would wrap to a small value; saturation must pin
127        // it to u64::MAX so a far-future clock never masquerades as the past.
128        assert_eq!(saturating_millis(Duration::from_secs(u64::MAX)), u64::MAX);
129    }
130
131    #[test]
132    fn mock_clock_starts_at_seed() {
133        let clock = MockClock::new(42);
134        assert_eq!(clock.now_ms(), 42);
135    }
136
137    #[test]
138    fn mock_clock_advance() {
139        let clock = MockClock::new(100);
140        clock.advance(50);
141        assert_eq!(clock.now_ms(), 150);
142    }
143
144    #[test]
145    fn mock_clock_set() {
146        let clock = MockClock::new(100);
147        clock.set(999);
148        assert_eq!(clock.now_ms(), 999);
149    }
150}