use std::cmp::Ordering;
use std::fmt;
pub const MAX_LOGICAL: u64 = (1u64 << 48) - 1;
pub const MAX_COUNTER: u16 = u16::MAX;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HlcError {
LogicalOverflow,
CounterOverflow,
BadEncoding,
}
impl fmt::Display for HlcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HlcError::LogicalOverflow => {
write!(f, "hlc logical component exceeds 48 bits")
}
HlcError::CounterOverflow => {
write!(f, "hlc counter exceeds 16 bits (physical clock wedged)")
}
HlcError::BadEncoding => write!(f, "hlc encoding must be exactly 8 bytes"),
}
}
}
impl std::error::Error for HlcError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Hlc {
l: u64,
c: u16,
}
impl Default for Hlc {
fn default() -> Self {
Self::zero()
}
}
impl fmt::Display for Hlc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hlc(l={}, c={})", self.l, self.c)
}
}
impl Hlc {
#[must_use]
pub const fn zero() -> Self {
Self { l: 0, c: 0 }
}
pub const fn from_parts(l: u64, c: u16) -> Result<Self, HlcError> {
if l > MAX_LOGICAL {
return Err(HlcError::LogicalOverflow);
}
Ok(Self { l, c })
}
#[must_use]
pub const fn logical(&self) -> u64 {
self.l
}
#[must_use]
pub const fn counter(&self) -> u16 {
self.c
}
#[must_use]
pub fn tick(&mut self, physical_now: u64) -> Hlc {
self.try_tick(physical_now)
.expect("invariant: physical_now within 48 bits and counter not wedged")
}
pub fn try_tick(&mut self, physical_now: u64) -> Result<Hlc, HlcError> {
if physical_now > MAX_LOGICAL {
return Err(HlcError::LogicalOverflow);
}
let prev_l = self.l;
let new_l = prev_l.max(physical_now);
let new_c = if new_l == prev_l {
self.c.checked_add(1).ok_or(HlcError::CounterOverflow)?
} else {
0
};
self.l = new_l;
self.c = new_c;
Ok(*self)
}
#[must_use]
pub fn update(&mut self, received: &Hlc, physical_now: u64) -> Hlc {
self.try_update(received, physical_now)
.expect("invariant: components within 48 bits and counter not wedged")
}
pub fn try_update(&mut self, received: &Hlc, physical_now: u64) -> Result<Hlc, HlcError> {
if physical_now > MAX_LOGICAL || received.l > MAX_LOGICAL {
return Err(HlcError::LogicalOverflow);
}
let prev_l = self.l;
let l_m = received.l;
let new_l = prev_l.max(l_m).max(physical_now);
let new_c = if new_l == prev_l && new_l == l_m {
self.c
.max(received.c)
.checked_add(1)
.ok_or(HlcError::CounterOverflow)?
} else if new_l == prev_l {
self.c.checked_add(1).ok_or(HlcError::CounterOverflow)?
} else if new_l == l_m {
received.c.checked_add(1).ok_or(HlcError::CounterOverflow)?
} else {
0
};
self.l = new_l;
self.c = new_c;
Ok(*self)
}
#[must_use]
pub const fn pack(&self) -> u64 {
(self.l << 16) | (self.c as u64)
}
#[must_use]
pub const fn unpack(packed: u64) -> Self {
Self {
l: packed >> 16,
c: (packed & 0xffff) as u16,
}
}
#[must_use]
pub fn encode(&self) -> [u8; 8] {
self.pack().to_be_bytes()
}
pub fn decode(bytes: &[u8]) -> Result<Self, HlcError> {
let arr: [u8; 8] = bytes.try_into().map_err(|_| HlcError::BadEncoding)?;
Ok(Self::unpack(u64::from_be_bytes(arr)))
}
#[must_use]
pub fn now_from_wall_clock(&mut self) -> Hlc {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_millis());
let pt = u64::try_from(millis)
.unwrap_or(MAX_LOGICAL)
.min(MAX_LOGICAL);
self.tick(pt)
}
}
#[must_use]
pub fn hlc_cmp(a: &Hlc, b: &Hlc) -> Ordering {
a.cmp(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_is_the_minimum() {
let z = Hlc::zero();
assert_eq!(z.logical(), 0);
assert_eq!(z.counter(), 0);
assert!(z <= Hlc::from_parts(0, 1).unwrap());
assert!(z <= Hlc::from_parts(1, 0).unwrap());
}
#[test]
fn tick_adopts_advancing_physical_time_and_resets_counter() {
let mut c = Hlc::zero();
let a = c.tick(10);
assert_eq!((a.logical(), a.counter()), (10, 0));
let b = c.tick(20);
assert_eq!((b.logical(), b.counter()), (20, 0));
assert!(b > a);
}
#[test]
fn tick_stalled_physical_time_increments_counter() {
let mut c = Hlc::zero();
let a = c.tick(10);
let b = c.tick(10);
let d = c.tick(5); assert_eq!((a.logical(), a.counter()), (10, 0));
assert_eq!((b.logical(), b.counter()), (10, 1));
assert_eq!((d.logical(), d.counter()), (10, 2));
assert!(a < b && b < d);
}
#[test]
fn update_adopts_higher_received_logical() {
let mut c = Hlc::zero();
let _ = c.tick(10);
let remote = Hlc::from_parts(25, 3).unwrap();
let r = c.update(&remote, 12);
assert_eq!((r.logical(), r.counter()), (25, 4));
assert!(r > remote);
}
#[test]
fn update_full_tie_takes_max_counter_plus_one() {
let mut c = Hlc::from_parts(30, 2).unwrap();
let remote = Hlc::from_parts(30, 5).unwrap();
let r = c.update(&remote, 30);
assert_eq!((r.logical(), r.counter()), (30, 6));
}
#[test]
fn update_physical_time_dominates_resets_counter() {
let mut c = Hlc::from_parts(30, 9).unwrap();
let remote = Hlc::from_parts(20, 4).unwrap();
let r = c.update(&remote, 40);
assert_eq!((r.logical(), r.counter()), (40, 0));
}
#[test]
fn receive_is_strictly_after_send_when_local_stalled() {
let mut c = Hlc::from_parts(50, 1).unwrap();
let remote = Hlc::from_parts(40, 7).unwrap();
let r = c.update(&remote, 10);
assert_eq!((r.logical(), r.counter()), (50, 2));
assert!(r > remote);
}
#[test]
fn pack_unpack_round_trip_and_orders() {
let a = Hlc::from_parts(10, 5).unwrap();
let b = Hlc::from_parts(10, 6).unwrap();
let d = Hlc::from_parts(11, 0).unwrap();
assert_eq!(Hlc::unpack(a.pack()), a);
assert!(a.pack() < b.pack());
assert!(b.pack() < d.pack());
assert_eq!(a.pack().cmp(&d.pack()), a.cmp(&d));
}
#[test]
fn encode_decode_round_trip() {
let a = Hlc::from_parts(123_456, 789).unwrap();
let bytes = a.encode();
assert_eq!(Hlc::decode(&bytes).unwrap(), a);
assert_eq!(Hlc::decode(&[0u8; 4]), Err(HlcError::BadEncoding));
assert_eq!(Hlc::decode(&[0u8; 9]), Err(HlcError::BadEncoding));
}
#[test]
fn from_parts_rejects_oversized_logical() {
assert_eq!(
Hlc::from_parts(MAX_LOGICAL + 1, 0),
Err(HlcError::LogicalOverflow)
);
assert!(Hlc::from_parts(MAX_LOGICAL, MAX_COUNTER).is_ok());
}
#[test]
fn try_tick_reports_counter_overflow_on_wedged_clock() {
let mut c = Hlc::from_parts(10, MAX_COUNTER).unwrap();
assert_eq!(c.try_tick(10), Err(HlcError::CounterOverflow));
assert_eq!(c.try_tick(5), Err(HlcError::CounterOverflow));
assert!(c.try_tick(11).is_ok());
}
#[test]
fn try_update_reports_overflow_and_logical_range() {
let mut c = Hlc::from_parts(10, MAX_COUNTER).unwrap();
let remote = Hlc::from_parts(10, 3).unwrap();
assert_eq!(c.try_update(&remote, 10), Err(HlcError::CounterOverflow));
let mut d = Hlc::zero();
let big = Hlc {
l: MAX_LOGICAL + 1,
c: 0,
};
assert_eq!(d.try_update(&big, 0), Err(HlcError::LogicalOverflow));
assert_eq!(d.try_tick(MAX_LOGICAL + 1), Err(HlcError::LogicalOverflow));
}
#[test]
fn monotone_across_arbitrary_local_schedule() {
let mut c = Hlc::zero();
let mut prev = Hlc::zero();
for pt in [5u64, 5, 5, 3, 10, 10, 9, 100, 1] {
let t = c.tick(pt);
assert!(t > prev, "{t} !> {prev} at pt={pt}");
prev = t;
}
}
#[test]
fn hlc_cmp_matches_ord() {
let a = Hlc::from_parts(1, 2).unwrap();
let b = Hlc::from_parts(1, 3).unwrap();
assert_eq!(hlc_cmp(&a, &b), Ordering::Less);
assert_eq!(hlc_cmp(&b, &a), Ordering::Greater);
assert_eq!(hlc_cmp(&a, &a), Ordering::Equal);
}
}