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, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClockError {
Skew(ClockSkewError),
Exhausted,
}
impl std::fmt::Display for ClockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Skew(error) => error.fmt(f),
Self::Exhausted => f.write_str("hybrid clock version range exhausted"),
}
}
}
impl std::error::Error for ClockError {}
use actordb_protocol::RecordVersion;
#[derive(Debug, Clone)]
pub struct HlcClock {
last: RecordVersion,
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: RecordVersion::default(),
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) -> Result<RecordVersion, ClockError> {
self.tick_at(self.wall())
}
fn successor(&self, version: RecordVersion, wall: u64) -> Result<RecordVersion, ClockError> {
if let Some(logical) = version.logical_counter.checked_add(1) {
return Ok(RecordVersion::new(version.timestamp_ms, logical));
}
let physical = version
.timestamp_ms
.checked_add(1)
.ok_or(ClockError::Exhausted)?;
self.validate(physical, wall).map_err(ClockError::Skew)?;
Ok(RecordVersion::new(physical, 0))
}
fn tick_at(&mut self, wall: u64) -> Result<RecordVersion, ClockError> {
let next = if wall > self.last.timestamp_ms {
RecordVersion::from(wall)
} else {
self.successor(self.last, wall)?
};
self.validate(next.timestamp_ms, wall)
.map_err(ClockError::Skew)?;
self.last = next;
Ok(next)
}
pub fn observe(&mut self, timestamp_ms: u64) -> Result<(), ClockError> {
self.observe_version(RecordVersion::from(timestamp_ms))
}
pub fn observe_version(&mut self, remote: RecordVersion) -> Result<(), ClockError> {
self.observe_at(remote, self.wall())
}
fn observe_at(&mut self, remote: RecordVersion, wall: u64) -> Result<(), ClockError> {
self.validate(remote.timestamp_ms, wall)
.map_err(ClockError::Skew)?;
let floor = self.last.max(remote);
self.last = if wall > floor.timestamp_ms {
RecordVersion::from(wall)
} else {
self.successor(floor, wall)?
};
Ok(())
}
pub fn seed_persisted(&mut self, timestamp_ms: u64) -> Result<(), ClockSkewError> {
self.seed_version(RecordVersion::from(timestamp_ms))
}
pub fn seed_version(&mut self, version: RecordVersion) -> Result<(), ClockSkewError> {
self.validate(version.timestamp_ms, self.wall())?;
self.last = self.last.max(version);
Ok(())
}
fn validate(&self, timestamp_ms: u64, wall: u64) -> Result<(), ClockSkewError> {
let max_allowed_ms = wall.saturating_add(self.max_skew_ms);
if timestamp_ms > max_allowed_ms {
Err(ClockSkewError {
timestamp_ms,
max_allowed_ms,
})
} else {
Ok(())
}
}
}
impl Default for HlcClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn millions_of_events_do_not_create_physical_skew() {
let mut clock = HlcClock::new().with_max_skew(300_000);
let wall = 1_000_000;
for _ in 0..1_000_000 {
clock.tick_at(wall).unwrap();
}
assert_eq!(clock.last, RecordVersion::new(wall, 999_999));
let mut receiver = HlcClock::new().with_max_skew(300_000);
receiver.observe_at(clock.last, wall).unwrap();
assert!(receiver.last > clock.last);
}
#[test]
fn observations_order_both_components_and_reject_skew_without_changes() {
let mut clock = HlcClock::new().with_max_skew(100);
clock
.observe_at(RecordVersion::new(1_010, 500), 1_000)
.unwrap();
assert_eq!(clock.last, RecordVersion::new(1_010, 501));
assert_eq!(
clock.tick_at(1_000).unwrap(),
RecordVersion::new(1_010, 502)
);
let before = clock.last;
assert!(matches!(
clock.observe_at(RecordVersion::new(1_101, 0), 1_000),
Err(ClockError::Skew(_))
));
assert_eq!(clock.last, before);
assert_eq!(clock.tick_at(1_020).unwrap(), RecordVersion::new(1_020, 0));
}
#[test]
fn exhaustion_is_explicit_and_does_not_reuse_versions() {
let mut clock = HlcClock::new();
clock.last = RecordVersion::new(u64::MAX, u64::MAX);
assert_eq!(clock.tick_at(u64::MAX), Err(ClockError::Exhausted));
clock.last = RecordVersion::new(100, u64::MAX);
assert_eq!(clock.tick_at(100).unwrap(), RecordVersion::new(101, 0));
clock.last = RecordVersion::new(100, u64::MAX);
clock.max_skew_ms = 0;
assert!(matches!(clock.tick_at(100), Err(ClockError::Skew(_))));
}
#[test]
fn durable_logical_high_water_survives_restart() {
let mut clock = HlcClock::new().with_max_skew(300_000);
let version = RecordVersion::new(clock.wall() + 60_000, 42);
clock.seed_version(version).unwrap();
assert!(clock.tick().unwrap() > version);
assert!(clock.seed_persisted(u64::MAX).is_err());
}
}