use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::bus::LogicalTime;
pub trait ClockSource: Send + Sync + 'static {
fn now(&self) -> LogicalTime;
}
pub struct RealClock {
epoch: u64,
last_ns: Mutex<u64>,
}
impl RealClock {
pub fn new() -> Self {
RealClock {
epoch: 0,
last_ns: Mutex::new(0),
}
}
fn unix_now_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
}
impl Default for RealClock {
fn default() -> Self {
RealClock::new()
}
}
impl ClockSource for RealClock {
fn now(&self) -> LogicalTime {
let now = Self::unix_now_ns();
let mut last = self.last_ns.lock().expect("real clock poisoned");
*last = (*last).max(now);
LogicalTime::new(self.epoch, *last)
}
}
#[derive(Clone)]
pub struct TestClock {
state: Arc<Mutex<(u64, u64)>>, }
impl TestClock {
pub fn new() -> Self {
TestClock {
state: Arc::new(Mutex::new((0, 0))),
}
}
pub fn advance(&self, delta: Duration) {
let mut state = self.state.lock().expect("test clock poisoned");
let ns = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
state.1 = state.1.saturating_add(ns);
}
pub fn bump_epoch(&self) {
let mut state = self.state.lock().expect("test clock poisoned");
state.0 = state.0.saturating_add(1);
state.1 = 0;
}
}
impl Default for TestClock {
fn default() -> Self {
TestClock::new()
}
}
impl ClockSource for TestClock {
fn now(&self) -> LogicalTime {
let state = self.state.lock().expect("test clock poisoned");
LogicalTime::new(state.0, state.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn real_clock_uses_host_wide_unix_domain() {
let unix_before = RealClock::unix_now_ns();
let a = RealClock::new();
let b = RealClock::new();
let ta = a.now().time_ns();
let tb = b.now().time_ns();
let unix_after = RealClock::unix_now_ns();
assert!(
ta >= unix_before && ta <= unix_after,
"clock a ({ta}) is not in the UNIX-epoch domain [{unix_before}, {unix_after}]"
);
assert!(
tb >= unix_before && tb <= unix_after,
"clock b ({tb}) is not in the UNIX-epoch domain [{unix_before}, {unix_after}]"
);
let gap = ta.abs_diff(tb);
assert!(
gap <= unix_after.saturating_sub(unix_before),
"two host clocks disagree by {gap}ns, more than the sampling window"
);
}
#[test]
fn real_clock_never_regresses_within_epoch() {
let clock = RealClock::new();
let mut last = clock.now().time_ns();
for _ in 0..1000 {
let next = clock.now().time_ns();
assert!(next >= last, "time_ns regressed: {next} < {last}");
last = next;
}
}
#[test]
fn test_clock_is_deterministic() {
let clock = TestClock::new();
assert_eq!(clock.now(), LogicalTime::new(0, 0));
clock.advance(Duration::from_nanos(5));
assert_eq!(clock.now(), LogicalTime::new(0, 5));
clock.advance(Duration::from_nanos(7));
assert_eq!(clock.now(), LogicalTime::new(0, 12));
clock.bump_epoch();
assert_eq!(clock.now(), LogicalTime::new(1, 0));
}
}