tsoracle_server/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//! Source of physical-time milliseconds for the TSO algorithm.
25//!
26//! The allocator's monotonicity is independent of clock correctness — a clock
27//! jumping backward cannot cause timestamp regression because the persisted
28//! high-water always wins. A clock pinned far in the past stalls new windows
29//! until wall time catches up past the persisted high-water.
30//!
31//! The `Send + Sync + 'static` bound exists because the server stores its clock
32//! as an `Arc<dyn Clock>` shared across request-handler tasks; it is not a
33//! property the underlying `tsoracle-core` allocator requires.
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(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn system_clock_returns_nonzero() {
78 let clock = SystemClock;
79 let now = clock.now_ms();
80 assert!(now > 1_700_000_000_000, "current time after 2023-11");
81 }
82
83 #[test]
84 fn saturating_millis_passes_through_in_range() {
85 use std::time::Duration;
86 assert_eq!(saturating_millis(Duration::from_millis(123)), 123);
87 }
88
89 #[test]
90 fn saturating_millis_saturates_instead_of_truncating() {
91 use std::time::Duration;
92 // u64::MAX seconds is ~1000x more milliseconds than u64 can hold, so a
93 // bare `as u64` cast would wrap to a small value; saturation must pin
94 // it to u64::MAX so a far-future clock never masquerades as the past.
95 assert_eq!(saturating_millis(Duration::from_secs(u64::MAX)), u64::MAX);
96 }
97}