Skip to main content

orderbook_rs/utils/
time.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
4use thiserror::Error;
5use tracing::warn;
6
7/// Error returned by [`try_current_time_millis`] when the wall clock cannot be
8/// represented as `u64` milliseconds since the UNIX epoch.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
10pub enum TimeError {
11    /// The system clock reads a time before the UNIX epoch (1970-01-01).
12    #[error("system clock is {behind:?} before the UNIX epoch")]
13    ClockBeforeEpoch {
14        /// How far before the epoch the clock reads.
15        behind: Duration,
16    },
17    /// The number of milliseconds since the epoch does not fit in `u64`.
18    #[error("{millis} ms since the UNIX epoch does not fit in u64")]
19    MillisOverflow {
20        /// The unrepresentable millisecond count.
21        millis: u128,
22    },
23}
24
25/// Converts a `Duration` since the UNIX epoch to `u64` milliseconds.
26///
27/// # Errors
28///
29/// [`TimeError::MillisOverflow`] when the millisecond count exceeds `u64::MAX`.
30pub(crate) fn duration_to_millis(since_epoch: Duration) -> Result<u64, TimeError> {
31    let millis = since_epoch.as_millis();
32    u64::try_from(millis).map_err(|_| TimeError::MillisOverflow { millis })
33}
34
35/// Converts a `SystemTime` to `u64` milliseconds since the UNIX epoch.
36///
37/// # Errors
38///
39/// - [`TimeError::ClockBeforeEpoch`] when `time` is before the epoch.
40/// - [`TimeError::MillisOverflow`] when the millisecond count exceeds `u64::MAX`.
41pub(crate) fn system_time_to_millis(time: SystemTime) -> Result<u64, TimeError> {
42    let since_epoch = time
43        .duration_since(UNIX_EPOCH)
44        .map_err(|e| TimeError::ClockBeforeEpoch {
45            behind: e.duration(),
46        })?;
47    duration_to_millis(since_epoch)
48}
49
50/// Returns the current wall-clock time in milliseconds since the UNIX epoch,
51/// or an error if it cannot be represented as `u64`.
52///
53/// Prefer this over [`current_time_millis`] wherever the caller can propagate
54/// or act on a broken clock.
55///
56/// # Errors
57///
58/// - [`TimeError::ClockBeforeEpoch`] when the system clock is set before
59///   1970-01-01.
60/// - [`TimeError::MillisOverflow`] when the millisecond count exceeds
61///   `u64::MAX` (roughly 584 million years after the epoch).
62///
63/// # Determinism
64///
65/// Same caveats as [`current_time_millis`]: wall clock, non-monotonic, not
66/// reproducible. Never call it on a deterministic or replay-critical path.
67pub fn try_current_time_millis() -> Result<u64, TimeError> {
68    system_time_to_millis(SystemTime::now())
69}
70
71/// Latches so each fallback is logged once per process, not once per call.
72static WARNED_BEFORE_EPOCH: AtomicBool = AtomicBool::new(false);
73static WARNED_OVERFLOW: AtomicBool = AtomicBool::new(false);
74
75/// Maps a [`TimeError`] to the documented fallback of [`current_time_millis`]
76/// and emits a `tracing::warn!` the first time each kind is seen.
77pub(crate) fn fallback_millis(err: TimeError) -> u64 {
78    let (latch, value) = match err {
79        TimeError::ClockBeforeEpoch { .. } => (&WARNED_BEFORE_EPOCH, 0),
80        TimeError::MillisOverflow { .. } => (&WARNED_OVERFLOW, u64::MAX),
81    };
82    if !latch.swap(true, Ordering::Relaxed) {
83        warn!(
84            error = %err,
85            fallback_ms = value,
86            "wall clock not representable as u64 ms since the UNIX epoch; \
87             using fallback (logged once)"
88        );
89    }
90    value
91}
92
93/// Returns the current wall-clock time in milliseconds since the UNIX epoch.
94///
95/// Infallible wrapper over [`try_current_time_millis`] for callers whose
96/// contract has no error channel (for example
97/// [`Clock::now_millis`](crate::Clock::now_millis) on
98/// [`MonotonicClock`](crate::MonotonicClock), trade-event stamping in the
99/// book managers, and NATS batch timestamps).
100///
101/// # Fallback
102///
103/// The conversion never truncates. When the clock cannot be represented the
104/// function returns an explicit sentinel and logs a `tracing::warn!` once per
105/// process for each kind:
106///
107/// - clock before the UNIX epoch: returns `0`;
108/// - millisecond count above `u64::MAX`: returns `u64::MAX`.
109///
110/// Callers that need to distinguish these cases must use
111/// [`try_current_time_millis`].
112///
113/// # Determinism
114///
115/// This reads the **wall clock**, so it is **non-monotonic** (it can jump
116/// forward or backward on NTP steps / clock adjustments) and is **not
117/// reproducible** across runs. Do **not** call it on any deterministic or
118/// replay-critical path — the matching engine, sequencer, and journal must take
119/// their time from the injected [`Clock`](crate::Clock) trait
120/// ([`MonotonicClock`](crate::MonotonicClock) in production,
121/// [`StubClock`](crate::StubClock) in tests) so replays reproduce
122/// engine-assigned timestamps byte-for-byte. This helper is for logging,
123/// metrics, and other non-deterministic, non-journaled uses only.
124#[must_use = "the current time is returned and should be used"]
125pub fn current_time_millis() -> u64 {
126    try_current_time_millis().unwrap_or_else(fallback_millis)
127}