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