use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
pub const MAX_DRIFT_MS: u64 = 24 * 60 * 60 * 1000;
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct Hlc {
pub wall_ms: u64,
pub logical: u32,
}
pub trait Clock: Send + Sync {
fn now_ms(&self) -> u64;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now_ms(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
}
impl Hlc {
pub fn tick(self, now_ms: u64) -> Hlc {
if now_ms > self.wall_ms {
Hlc {
wall_ms: now_ms,
logical: 0,
}
} else if self.logical == u32::MAX {
Hlc {
wall_ms: self.wall_ms + 1,
logical: 0,
}
} else {
Hlc {
wall_ms: self.wall_ms,
logical: self.logical + 1,
}
}
}
pub fn observe(self, remote: Hlc, now_ms: u64) -> Hlc {
let cap = now_ms.saturating_add(MAX_DRIFT_MS);
let remote = if remote.wall_ms > cap {
tracing::warn!(
remote_wall_ms = remote.wall_ms,
now_ms,
"remote clock is more than 24h ahead; clamping"
);
Hlc {
wall_ms: cap,
logical: 0,
}
} else {
remote
};
self.max(remote)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tick_is_strictly_monotonic_even_when_the_wall_clock_stalls() {
let a = Hlc::default().tick(100);
let b = a.tick(100);
let c = b.tick(50);
assert!(a < b && b < c);
assert_eq!(
a,
Hlc {
wall_ms: 100,
logical: 0
}
);
assert_eq!(
c,
Hlc {
wall_ms: 100,
logical: 2
}
);
}
#[test]
fn observe_takes_the_later_clock() {
let local = Hlc {
wall_ms: 10,
logical: 3,
};
let remote = Hlc {
wall_ms: 20,
logical: 1,
};
assert_eq!(local.observe(remote, 15), remote);
assert!(local.observe(remote, 15).tick(15) > remote);
}
#[test]
fn observe_clamps_far_future_remotes() {
let remote = Hlc {
wall_ms: 1_000 + MAX_DRIFT_MS + 5,
logical: 0,
};
let got = Hlc::default().observe(remote, 1_000);
assert_eq!(
got,
Hlc {
wall_ms: 1_000 + MAX_DRIFT_MS,
logical: 0
}
);
}
}