use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; pub const TID_BASE32: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; /// Process-local monotonic counter used to disambiguate TIDs that would /// otherwise collide on the same microsecond. /// /// The wall clock gives us 13 base32 chars of timestamp (≈52 bits of /// micros). Two writes in the same microsecond on the same PDS would /// otherwise produce the identical TID, and `put_record` would silently /// overwrite the prior record in the MST (same rkey, but actually /// different content — the value CIDs differ but the MST key is the /// TID, so the prior record becomes unreachable from the head commit). /// /// We tack 12 low bits of a `fetch_add` counter into the encoded value /// so back-to-back calls — even inside the same microsecond — always /// yield different TIDs. The counter starts at 0; the first call's /// fetch_add returns 0 and produces a TID encoding /// `(now_micros << 12) | 0`. The counter is monotonic per process, /// not globally — a process restart will reset it to 0, which means /// a TID emitted by the new process may sort *before* a TID emitted /// by its predecessor on the same wall-clock microsecond. That's /// acceptable because TIDs are only used as MST rkeys within a /// single repo's history; the protocol doesn't require cross-process /// monotonicity. static TID_COUNTER: AtomicU64 = AtomicU64::new(0); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Tid { pub raw: String, } impl Tid { pub fn new() -> Self { Self { raw: generate_tid(), } } pub fn from_string(s: impl Into) -> Self { Self { raw: s.into() } } pub fn as_str(&self) -> &str { &self.raw } } impl Default for Tid { fn default() -> Self { Self::new() } } impl std::fmt::Display for Tid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.raw) } } pub fn generate_tid() -> String { // Phase 5b H10 — the counter is 12 bits wide, so it would wrap after // 4096 calls inside a single microsecond. To prevent that, we // block until the wall clock advances whenever the low-12 counter // has cycled back to 0 inside the same microsecond. In practice // this never fires (4096 TIDs/µs ≈ 4 billion/sec from one process) // but it's a cheap insurance policy. let mut now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_micros() as u64; let counter = loop { let prev = TID_COUNTER.fetch_add(1, Ordering::Relaxed); // The counter is reset to 0 at process start; the low 12 bits // are an in-microsecond disambiguator. If we've wrapped back to // 0 mid-microsecond, spin until the clock advances. if (prev & 0xFFF) == 0 && prev != 0 { let next = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_micros() as u64; if next == now { std::hint::spin_loop(); continue; } now = next; } break prev; }; // Combine timestamp + 12-bit disambiguator. The wall clock fits // comfortably in 52 bits, so the counter never spills into the // timestamp portion for any realistic process lifetime (~year 2400). let combined: u64 = (now << 12) | (counter & 0xFFF); let mut s = String::with_capacity(13); let mut n = combined; for _ in 0..13 { let idx = (n & 0x1F) as usize; s.push(TID_BASE32[idx] as char); n >>= 5; } s.chars().rev().collect() } pub fn compare_tid(a: &str, b: &str) -> std::cmp::Ordering { a.cmp(b) } #[cfg(test)] mod tests { use super::*; use std::collections::HashSet; #[test] fn tid_increases() { let t1 = generate_tid(); std::thread::sleep(std::time::Duration::from_millis(2)); let t2 = generate_tid(); // Strict ordering: t2 must be greater than t1. Accepting `is_le` // would mask the very bug this test exists to catch. assert!(compare_tid(&t1, &t2).is_lt()); } #[test] fn tid_uses_lowercase_base32() { let t = generate_tid(); for c in t.chars() { assert!(matches!(c, '2'..='7' | 'a'..='z')); } } /// Phase 5b H10 — two writes in the same microsecond used to /// produce identical TIDs, which caused `put_record` to silently /// overwrite the prior record (different value CID, but the same /// rkey, so the new MST entry eclipsed the old). Verify a tight /// burst of N calls yields N distinct TIDs. #[test] fn generate_tid_is_monotonic_per_process() { let n = 1_000; let mut seen = HashSet::with_capacity(n); let mut prev: Option = None; for _ in 0..n { let t = generate_tid(); assert!( seen.insert(t.clone()), "duplicate TID produced in tight loop: {t}" ); if let Some(p) = prev.as_ref() { assert!( compare_tid(p, &t).is_lt(), "TID must strictly increase per process: {p} >= {t}" ); } prev = Some(t); } assert_eq!(seen.len(), n); } /// Same as above but explicitly constructs the "same microsecond" /// worst case by sampling TIDs back-to-back without sleeping. The /// counter overlay must keep them distinct even when the wall /// clock doesn't tick. #[test] fn generate_tid_avoids_same_microsecond_collisions() { let n = 100; let mut seen = HashSet::with_capacity(n); for _ in 0..n { let t = generate_tid(); assert!(seen.insert(t.clone()), "collision at {t}"); } assert_eq!(seen.len(), n); } }