1pub const HIGHWATER_STATE_KEY: &str = "hlc_highwater";
9
10pub const MAX_FUTURE_SKEW_MS: u64 = 30 * 24 * 60 * 60 * 1000;
19
20pub(crate) const COUNTER_MAX: u16 = 9999;
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
26pub struct Timestamp {
27 pub millis: u64,
28 pub counter: u16,
29 pub device_id: String,
30}
31
32impl Timestamp {
33 pub fn new(millis: u64, counter: u16, device_id: String) -> Self {
34 Self {
35 millis,
36 counter,
37 device_id,
38 }
39 }
40
41 pub fn is_within_future_bound(&self, receiver_wall_ms: u64) -> bool {
48 self.millis <= receiver_wall_ms.saturating_add(MAX_FUTURE_SKEW_MS)
49 }
50
51 pub fn parse(s: &str) -> Option<Self> {
53 let mut parts = s.splitn(3, '-');
54 let millis = parts.next()?.parse::<u64>().ok()?;
55 let counter = parts.next()?.parse::<u16>().ok()?;
56 let device_id = parts.next()?;
57 if device_id.is_empty() {
58 return None;
59 }
60 if counter > COUNTER_MAX {
61 return None;
62 }
63 Some(Self {
64 millis,
65 counter,
66 device_id: device_id.to_string(),
67 })
68 }
69}
70
71impl std::fmt::Display for Timestamp {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(
74 f,
75 "{:013}-{:04}-{}",
76 self.millis, self.counter, self.device_id
77 )
78 }
79}
80
81use std::sync::{Arc, Mutex};
105
106struct HlcState {
111 millis: u64,
112 counter: u16,
113}
114
115fn increment(state: &mut HlcState) {
116 if state.counter < COUNTER_MAX {
117 state.counter += 1;
118 } else if let Some(next_millis) = state.millis.checked_add(1) {
119 state.millis = next_millis;
120 state.counter = 0;
121 } else {
122 state.counter = COUNTER_MAX;
123 }
124}
125
126pub struct Hlc {
127 device_id: String,
128 state: Mutex<HlcState>,
129 clock: coven_foundation::clock::ClockRef,
130}
131
132impl Hlc {
133 pub fn try_new(
134 device_id: String,
135 clock: coven_foundation::clock::ClockRef,
136 ) -> Result<Self, coven_foundation::store_dir::PathTokenError> {
137 coven_foundation::store_dir::validate_path_token(&device_id)?;
138 Ok(Self {
139 device_id,
140 state: Mutex::new(HlcState {
141 millis: 0,
142 counter: 0,
143 }),
144 clock,
145 })
146 }
147
148 pub fn new(device_id: String, clock: coven_foundation::clock::ClockRef) -> Self {
150 Self::try_new(device_id, clock).expect("device_id must be a safe path token")
151 }
152
153 pub fn device_id(&self) -> &str {
154 &self.device_id
155 }
156
157 fn wall_millis(&self) -> u64 {
158 self.clock
159 .now()
160 .timestamp_millis()
161 .try_into()
162 .expect("clock before UNIX epoch")
163 }
164
165 pub fn seed(&self, high_water: &Timestamp) {
172 let mut state = self.state.lock().unwrap();
173 if high_water.millis > state.millis
174 || (high_water.millis == state.millis && high_water.counter > state.counter)
175 {
176 state.millis = high_water.millis;
177 state.counter = high_water.counter;
178 }
179 }
180
181 pub fn high_water(&self) -> Timestamp {
186 let state = self.state.lock().unwrap();
187 Timestamp::new(state.millis, state.counter, self.device_id.clone())
188 }
189
190 pub fn wall_now_ms(&self) -> u64 {
197 self.wall_millis()
198 }
199
200 pub fn now(&self) -> Timestamp {
203 let wall = self.wall_millis();
204 let mut state = self.state.lock().unwrap();
205
206 if wall > state.millis {
207 state.millis = wall;
208 state.counter = 0;
209 } else {
210 increment(&mut state);
211 }
212
213 Timestamp::new(state.millis, state.counter, self.device_id.clone())
214 }
215
216 pub fn advance_past(&self, remote: &Timestamp) {
226 let wall = self.wall_millis();
227 let mut state = self.state.lock().unwrap();
228
229 if wall > state.millis && wall > remote.millis {
230 state.millis = wall;
232 state.counter = 0;
233 } else if remote.millis > state.millis {
234 state.millis = remote.millis;
236 state.counter = remote.counter;
237 } else if state.millis == remote.millis && remote.counter > state.counter {
238 state.counter = remote.counter;
240 }
241 }
242}
243
244#[derive(Clone)]
254pub struct UpdatedAtStamper {
255 hlc: Arc<Hlc>,
256}
257
258impl UpdatedAtStamper {
259 pub fn new(hlc: Arc<Hlc>) -> Self {
260 Self { hlc }
261 }
262
263 pub fn stamp(&self) -> String {
267 self.hlc.now().to_string()
268 }
269}
270
271#[cfg(any(test, feature = "test-utils"))]
278pub fn now_wall_ms(clock: &dyn coven_foundation::clock::Clock) -> u64 {
279 clock
280 .now()
281 .timestamp_millis()
282 .try_into()
283 .expect("clock before UNIX epoch")
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use coven_foundation::clock::{ClosureClock, FixedClock, SystemClock};
290 use std::sync::atomic::{AtomicU64, Ordering};
291
292 fn instant(ms: u64) -> chrono::DateTime<chrono::Utc> {
293 chrono::DateTime::from_timestamp_millis(ms.try_into().expect("test millis fit in i64"))
294 .expect("valid test clock instant")
295 }
296
297 fn fixed_clock(ms: u64) -> coven_foundation::clock::ClockRef {
298 Arc::new(FixedClock(instant(ms)))
299 }
300
301 fn advancing_clock(start: u64) -> (Arc<AtomicU64>, coven_foundation::clock::ClockRef) {
302 let time = Arc::new(AtomicU64::new(start));
303 let time_clone = time.clone();
304 (
305 time,
306 Arc::new(ClosureClock(move || {
307 instant(time_clone.load(Ordering::SeqCst))
308 })),
309 )
310 }
311
312 #[test]
313 fn basic_monotonicity() {
314 let hlc = Hlc::new("dev-1".into(), Arc::new(SystemClock));
315 let t1 = hlc.now();
316 let t2 = hlc.now();
317 let t3 = hlc.now();
318
319 assert!(t2 > t1, "t2={t2} should be > t1={t1}");
320 assert!(t3 > t2, "t3={t3} should be > t2={t2}");
321 }
322
323 #[test]
324 fn new_rejects_empty_device_id() {
325 assert!(matches!(
326 Hlc::try_new(String::new(), Arc::new(SystemClock)),
327 Err(coven_foundation::store_dir::PathTokenError::Empty),
328 ));
329 }
330
331 #[test]
332 fn counter_increments_when_clock_stalls() {
333 let hlc = Hlc::new("dev-1".into(), fixed_clock(1000));
334
335 let t1 = hlc.now();
336 assert_eq!(t1.millis, 1000);
337 assert_eq!(t1.counter, 0);
338
339 let t2 = hlc.now();
340 assert_eq!(t2.millis, 1000);
341 assert_eq!(t2.counter, 1);
342
343 let t3 = hlc.now();
344 assert_eq!(t3.millis, 1000);
345 assert_eq!(t3.counter, 2);
346
347 assert!(t3 > t2);
348 assert!(t2 > t1);
349 }
350
351 #[test]
352 fn wall_clock_advance_resets_counter() {
353 let (time, clock) = advancing_clock(1000);
354 let hlc = Hlc::new("dev-1".into(), clock);
355
356 let t1 = hlc.now();
357 assert_eq!(t1.millis, 1000);
358 assert_eq!(t1.counter, 0);
359
360 let t2 = hlc.now();
362 assert_eq!(t2.counter, 1);
363
364 time.store(2000, Ordering::SeqCst);
366 let t3 = hlc.now();
367 assert_eq!(t3.millis, 2000);
368 assert_eq!(t3.counter, 0);
369
370 assert!(t3 > t2);
371 }
372
373 #[test]
374 fn advance_past_remote_ahead() {
375 let hlc = Hlc::new("dev-local".into(), fixed_clock(1000));
376
377 let remote = Timestamp::new(5000, 3, "dev-remote".into());
379 hlc.advance_past(&remote);
380
381 let t = hlc.now();
383 assert!(
384 t.to_string() > remote.to_string(),
385 "t={t} must beat {remote}"
386 );
387 assert_eq!(t.millis, 5000);
388 assert_eq!(t.device_id, "dev-local");
389 }
390
391 #[test]
392 fn advance_past_remote_behind() {
393 let hlc = Hlc::new("dev-local".into(), fixed_clock(5000));
394
395 let primed = hlc.now();
397
398 let remote = Timestamp::new(1000, 10, "dev-remote".into());
400 hlc.advance_past(&remote);
401
402 let t = hlc.now();
403 assert!(
404 t > primed,
405 "t={t} must stay above the primed clock {primed}"
406 );
407 assert_eq!(t.millis, 5000);
408 }
409
410 #[test]
417 fn advance_past_far_future_applied_row_is_not_capped() {
418 let hlc = Hlc::new("dev-local".into(), fixed_clock(1000));
419
420 let far_future = 1000 + 48 * 60 * 60 * 1000;
423 let applied = Timestamp::new(far_future, 7, "dev-remote".into());
424 hlc.advance_past(&applied);
425
426 let next = hlc.now();
429 assert!(
430 next.to_string() > applied.to_string(),
431 "next stamp {next} regressed below applied row {applied}: the clock \
432 refused to advance past an authoritative register value",
433 );
434 assert_eq!(next.millis, far_future);
435 }
436
437 #[test]
438 fn string_roundtrip() {
439 let ts = Timestamp::new(1707580800000, 42, "dev-abc123".into());
440 let s = ts.to_string();
441 let parsed = Timestamp::parse(&s).expect("parse should succeed");
442
443 assert_eq!(parsed, ts);
444 assert_eq!(s, "1707580800000-0042-dev-abc123");
445 }
446
447 #[test]
448 fn string_format_is_zero_padded() {
449 let ts = Timestamp::new(1000, 0, "d".into());
450 assert_eq!(ts.to_string(), "0000000001000-0000-d");
451
452 let ts2 = Timestamp::new(9999999999999, 9999, "d".into());
453 assert_eq!(ts2.to_string(), "9999999999999-9999-d");
454 }
455
456 #[test]
457 fn lexicographic_ordering_matches_causal_ordering() {
458 let timestamps = [
459 Timestamp::new(1000, 0, "dev-a".into()),
460 Timestamp::new(1000, 1, "dev-a".into()),
461 Timestamp::new(1000, 1, "dev-b".into()),
462 Timestamp::new(2000, 0, "dev-a".into()),
463 Timestamp::new(2000, 0, "dev-b".into()),
464 ];
465
466 let strings: Vec<String> = timestamps.iter().map(|t| t.to_string()).collect();
467
468 for i in 1..strings.len() {
470 assert!(
471 strings[i] > strings[i - 1],
472 "Expected {:?} > {:?}",
473 strings[i],
474 strings[i - 1]
475 );
476 }
477 }
478
479 #[test]
480 fn device_id_breaks_ties() {
481 let ts_a = Timestamp::new(5000, 3, "aaa".into());
482 let ts_b = Timestamp::new(5000, 3, "bbb".into());
483
484 assert!(ts_b > ts_a);
486
487 assert!(ts_b.to_string() > ts_a.to_string());
489 }
490
491 #[test]
492 fn future_bound_admits_honest_and_rejects_grossly_future() {
493 let wall: u64 = 1_700_000_000_000;
494
495 assert!(Timestamp::new(wall, 0, "d".into()).is_within_future_bound(wall));
497 assert!(Timestamp::new(0, 0, "d".into()).is_within_future_bound(wall));
498
499 let just_inside = wall + MAX_FUTURE_SKEW_MS - 1;
501 assert!(Timestamp::new(just_inside, 0, "d".into()).is_within_future_bound(wall));
502 let at_bound = wall + MAX_FUTURE_SKEW_MS;
504 assert!(Timestamp::new(at_bound, 0, "d".into()).is_within_future_bound(wall));
505
506 let just_beyond = wall + MAX_FUTURE_SKEW_MS + 1;
508 assert!(!Timestamp::new(just_beyond, 0, "d".into()).is_within_future_bound(wall));
509 assert!(!Timestamp::new(u64::MAX, 0, "d".into()).is_within_future_bound(wall));
511
512 assert!(Timestamp::new(u64::MAX, 0, "d".into()).is_within_future_bound(u64::MAX));
515 }
516
517 #[test]
518 fn parse_rejects_invalid_input() {
519 assert!(Timestamp::parse("").is_none());
520 assert!(Timestamp::parse("not-a-timestamp").is_none());
521 assert!(Timestamp::parse("1000-0000").is_none()); assert!(Timestamp::parse("1000-0000-").is_none()); assert!(Timestamp::parse("abc-0000-dev").is_none()); assert!(Timestamp::parse("1000-xyz-dev").is_none()); }
526
527 #[test]
528 fn parse_rejects_counter_above_format_width() {
529 assert!(Timestamp::parse("0000000001000-10000-dev").is_none());
530 assert!(Timestamp::parse("0000000001000-65535-dev").is_none());
531 }
532
533 #[test]
534 fn advance_past_counter_bound_carries_into_millis() {
535 let hlc = Hlc::new("dev-local".into(), fixed_clock(1000));
536 let observed = Timestamp::new(1000, 9999, "dev-remote".into());
537
538 hlc.advance_past(&observed);
539 let next = hlc.now();
540
541 assert!(
542 next.to_string() > observed.to_string(),
543 "next stamp {next} must sort after observed stamp {observed}",
544 );
545 assert_eq!(next.millis, 1001);
546 assert_eq!(next.counter, 0);
547 }
548
549 #[test]
550 fn now_counter_bound_carries_into_millis() {
551 let hlc = Hlc::new("dev-local".into(), fixed_clock(1000));
552 hlc.seed(&Timestamp::new(1000, 9999, "dev-remote".into()));
553
554 let next = hlc.now();
555
556 assert_eq!(next.millis, 1001);
557 assert_eq!(next.counter, 0);
558 assert_eq!(next.to_string(), "0000000001001-0000-dev-local");
559 }
560
561 #[test]
562 fn minted_counters_keep_fixed_width_lexical_order() {
563 let hlc = Hlc::new("dev-local".into(), fixed_clock(1000));
564 hlc.seed(&Timestamp::new(1000, 9998, "dev-remote".into()));
565
566 let stamps = [hlc.now(), hlc.now(), hlc.now()];
567 for stamp in &stamps {
568 let rendered = stamp.to_string();
569 let counter = rendered
570 .split('-')
571 .nth(1)
572 .expect("timestamp has a counter field");
573 assert_eq!(counter.len(), 4);
574 }
575 for pair in stamps.windows(2) {
576 assert!(
577 pair[1] > pair[0],
578 "timestamp ordering must advance from {} to {}",
579 pair[0],
580 pair[1],
581 );
582 assert!(
583 pair[1].to_string() > pair[0].to_string(),
584 "string ordering must advance from {} to {}",
585 pair[0],
586 pair[1],
587 );
588 }
589 }
590
591 #[test]
592 fn parse_handles_device_id_with_dashes() {
593 let ts = Timestamp::new(1000, 0, "550e8400-e29b-41d4-a716-446655440000".into());
596 let s = ts.to_string();
597 let parsed = Timestamp::parse(&s).expect("parse should handle UUID device_id");
598 assert_eq!(parsed.device_id, "550e8400-e29b-41d4-a716-446655440000");
599 assert_eq!(parsed, ts);
600 }
601}