use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hlc {
pub wall_ms: u64,
pub counter: u32,
pub node: String,
}
impl Hlc {
pub fn to_canonical(&self) -> String {
format!("{:013}.{:010}.{}", self.wall_ms, self.counter, self.node)
}
pub fn parse(s: &str) -> Option<Hlc> {
let mut it = s.splitn(3, '.');
let wall_ms = it.next()?.parse::<u64>().ok()?;
let counter = it.next()?.parse::<u32>().ok()?;
let node = it.next()?.to_string();
Some(Hlc {
wall_ms,
counter,
node,
})
}
}
struct State {
wall_ms: u64,
counter: u32,
}
fn state() -> &'static Mutex<State> {
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
STATE.get_or_init(|| {
Mutex::new(State {
wall_ms: 0,
counter: 0,
})
})
}
fn node_cell() -> &'static OnceLock<String> {
static NODE: OnceLock<String> = OnceLock::new();
&NODE
}
pub fn set_node(node: impl Into<String>) {
let n = node.into();
if !n.trim().is_empty() {
let _ = node_cell().set(n);
}
}
fn node() -> String {
node_cell()
.get()
.cloned()
.unwrap_or_else(|| "local".to_string())
}
fn physical_now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn now() -> Hlc {
let mut st = state().lock().unwrap_or_else(|p| p.into_inner());
let phys = physical_now_ms();
if phys > st.wall_ms {
st.wall_ms = phys;
st.counter = 0;
} else {
st.counter = st.counter.saturating_add(1);
}
Hlc {
wall_ms: st.wall_ms,
counter: st.counter,
node: node(),
}
}
pub fn observe(remote: &Hlc) {
let mut st = state().lock().unwrap_or_else(|p| p.into_inner());
let phys = physical_now_ms();
let max_wall = st.wall_ms.max(remote.wall_ms).max(phys);
if max_wall == st.wall_ms && max_wall == remote.wall_ms {
st.counter = st.counter.max(remote.counter).saturating_add(1);
} else if max_wall == remote.wall_ms {
st.wall_ms = max_wall;
st.counter = remote.counter.saturating_add(1);
} else if max_wall == st.wall_ms {
st.counter = st.counter.saturating_add(1);
} else {
st.wall_ms = max_wall;
st.counter = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_is_strictly_increasing() {
let a = now();
let b = now();
let c = now();
assert!(
a.to_canonical() < b.to_canonical(),
"{} !< {}",
a.to_canonical(),
b.to_canonical()
);
assert!(b.to_canonical() < c.to_canonical());
}
#[test]
fn observe_advances_past_far_future_remote() {
let remote = Hlc {
wall_ms: physical_now_ms() + 1_000_000, counter: 42,
node: "other".to_string(),
};
observe(&remote);
let next = now();
assert!(
next.to_canonical() > remote.to_canonical(),
"local clock must advance past an observed future remote: {} !> {}",
next.to_canonical(),
remote.to_canonical()
);
}
#[test]
fn canonical_sorts_chronologically_and_breaks_ties_by_node() {
let earlier = Hlc {
wall_ms: 100,
counter: 5,
node: "z".into(),
};
let later_wall = Hlc {
wall_ms: 101,
counter: 0,
node: "a".into(),
};
assert!(earlier.to_canonical() < later_wall.to_canonical());
let same_a = Hlc {
wall_ms: 100,
counter: 5,
node: "a".into(),
};
let same_b = Hlc {
wall_ms: 100,
counter: 5,
node: "b".into(),
};
assert!(
same_a.to_canonical() < same_b.to_canonical(),
"equal (wall,counter) must break by node"
);
}
#[test]
fn parse_roundtrips_including_dotted_node() {
let h = Hlc {
wall_ms: 1234567890,
counter: 7,
node: "laptop.local".into(),
};
let parsed = Hlc::parse(&h.to_canonical()).expect("parse");
assert_eq!(parsed, h);
}
}