Skip to main content

sim_lib_server/
clock.rs

1use std::{
2    sync::atomic::{AtomicU64, Ordering},
3    time::{Duration, SystemTime, UNIX_EPOCH},
4};
5
6use sim_kernel::{Error, Result};
7
8/// One observed host wall-clock instant, represented as milliseconds since the Unix epoch.
9///
10/// A wall timestamp is human-facing evidence and a scheduling input. It is not
11/// monotonic: callers must use logical ticks, revisions, or [`std::time::Instant`]
12/// when correctness depends on ordering or elapsed time.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct WallTimestamp(u64);
15
16impl WallTimestamp {
17    /// Creates an explicit Unix-millisecond wall-clock observation.
18    pub const fn from_unix_millis(unix_millis: u64) -> Self {
19        Self(unix_millis)
20    }
21
22    /// Returns the observed Unix milliseconds.
23    pub const fn unix_millis(self) -> u64 {
24        self.0
25    }
26
27    /// Converts a system wall-clock reading, rejecting pre-epoch and unrepresentable values.
28    pub fn from_system_time(time: SystemTime) -> Result<Self> {
29        let elapsed = time
30            .duration_since(UNIX_EPOCH)
31            .map_err(|err| Error::Eval(format!("system wall clock is before UNIX_EPOCH: {err}")))?;
32        Self::from_epoch_duration(elapsed)
33    }
34
35    fn from_epoch_duration(elapsed: Duration) -> Result<Self> {
36        let unix_millis = u64::try_from(elapsed.as_millis()).map_err(|_| {
37            Error::Eval("system wall-clock timestamp exceeds u64 milliseconds".to_owned())
38        })?;
39        Ok(Self(unix_millis))
40    }
41}
42
43/// Object-safe source of host wall-clock observations.
44///
45/// Implementations may move backward between observations. Code that needs a
46/// correctness clock must instead use a logical tick, revision, or monotonic
47/// deadline.
48pub trait WallClock: Send + Sync {
49    /// Observes the current host wall-clock timestamp.
50    fn now(&self) -> Result<WallTimestamp>;
51
52    /// Observes the current host wall clock as Unix milliseconds.
53    fn now_ms(&self) -> Result<u64> {
54        self.now().map(WallTimestamp::unix_millis)
55    }
56}
57
58/// [`WallClock`] backed by the host system clock.
59#[derive(Clone, Copy, Debug, Default)]
60pub struct SystemWallClock;
61
62impl WallClock for SystemWallClock {
63    fn now(&self) -> Result<WallTimestamp> {
64        WallTimestamp::from_system_time(SystemTime::now())
65    }
66}
67
68/// Thread-safe [`WallClock`] that advances by a fixed step on each observation.
69///
70/// The clock starts at `start_ms` and advances by `step_ms` after every read,
71/// producing reproducible observations without consulting ambient host time.
72#[derive(Debug)]
73pub struct DeterministicWallClock {
74    next_ms: AtomicU64,
75    step_ms: u64,
76}
77
78impl DeterministicWallClock {
79    /// Builds a deterministic clock with an initial Unix-millisecond value and fixed step.
80    pub const fn new(start_ms: u64, step_ms: u64) -> Self {
81        Self {
82            next_ms: AtomicU64::new(start_ms),
83            step_ms,
84        }
85    }
86}
87
88impl Clone for DeterministicWallClock {
89    fn clone(&self) -> Self {
90        Self::new(self.next_ms.load(Ordering::Acquire), self.step_ms)
91    }
92}
93
94impl WallClock for DeterministicWallClock {
95    fn now(&self) -> Result<WallTimestamp> {
96        self.next_ms
97            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
98                current.checked_add(self.step_ms)
99            })
100            .map(WallTimestamp::from_unix_millis)
101            .map_err(|_| Error::Eval("deterministic wall clock overflow".to_owned()))
102    }
103}
104
105#[cfg(test)]
106mod tests;