pub fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClockSkewError {
pub timestamp_ms: u64,
pub max_allowed_ms: u64,
}
impl std::fmt::Display for ClockSkewError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"timestamp {} exceeds maximum allowed future time {}",
self.timestamp_ms, self.max_allowed_ms
)
}
}
impl std::error::Error for ClockSkewError {}
#[derive(Debug, Clone)]
pub struct HlcClock {
last_ms: u64,
offset_ms: i64,
max_skew_ms: u64,
}
impl HlcClock {
pub fn new() -> Self {
Self::with_offset(0)
}
pub fn with_offset(offset_ms: i64) -> Self {
Self {
last_ms: 0,
offset_ms,
max_skew_ms: u64::MAX,
}
}
pub fn with_max_skew(mut self, max_skew_ms: u64) -> Self {
self.max_skew_ms = max_skew_ms;
self
}
fn wall(&self) -> u64 {
let now = now_ms();
if self.offset_ms >= 0 {
now.saturating_add(self.offset_ms as u64)
} else {
now.saturating_sub(self.offset_ms.unsigned_abs())
}
}
pub fn tick(&mut self) -> u64 {
let wall = self.wall();
self.last_ms = wall.max(self.last_ms.saturating_add(1));
self.last_ms
}
pub fn observe(&mut self, remote_ms: u64) -> Result<(), ClockSkewError> {
let wall = self.wall();
self.validate(remote_ms, wall)?;
self.last_ms = wall
.max(self.last_ms.saturating_add(1))
.max(remote_ms.saturating_add(1));
Ok(())
}
pub fn seed_persisted(&mut self, timestamp_ms: u64) -> Result<(), ClockSkewError> {
let wall = self.wall();
self.validate(timestamp_ms, wall)?;
self.last_ms = self.last_ms.max(timestamp_ms);
Ok(())
}
fn validate(&self, timestamp_ms: u64, wall_ms: u64) -> Result<(), ClockSkewError> {
let max_allowed_ms = wall_ms.saturating_add(self.max_skew_ms);
if timestamp_ms > max_allowed_ms {
return Err(ClockSkewError {
timestamp_ms,
max_allowed_ms,
});
}
Ok(())
}
}
impl Default for HlcClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hlc_is_monotonic_and_observes_remote() {
let mut hlc = HlcClock::new();
let t1 = hlc.tick();
let t2 = hlc.tick();
assert!(t2 >= t1, "ticks must be monotonic");
hlc.observe(t2 + 100_000).unwrap();
let t3 = hlc.tick();
assert!(t3 > t2 + 100_000, "clock must move past observed remote");
hlc.observe(1).unwrap();
let t4 = hlc.tick();
assert!(t4 >= t3, "clock must never go backward");
}
#[test]
fn hlc_rejects_far_future_remote_timestamps() {
let mut hlc = HlcClock::new().with_max_skew(300_000); let t1 = hlc.tick();
let error = hlc.observe(u64::MAX).unwrap_err();
assert_eq!(error.timestamp_ms, u64::MAX);
let t2 = hlc.tick();
let wall = hlc.wall();
assert!(
t2 <= wall.saturating_add(10),
"far-future remote must be rejected, got t2={t2} wall={wall}"
);
assert!(t2 >= t1, "clock must still be monotonic");
hlc.observe(wall + 60_000).unwrap();
let t3 = hlc.tick();
assert!(t3 > wall + 60_000, "in-skew remote must be observed");
}
#[test]
fn hlc_seeds_from_persisted_versions() {
let mut hlc = HlcClock::new().with_max_skew(300_000);
let persisted = hlc.wall() + 60_000;
hlc.seed_persisted(persisted).unwrap();
assert!(
hlc.tick() > persisted,
"the first post-restart write must beat durable state"
);
}
}