Skip to main content

citadel_sync/
node_id.rs

1/// Unique node identifier for replication.
2///
3/// Each database instance generates a random `NodeId` on creation.
4/// Used as a deterministic tiebreaker in LWW conflict resolution
5/// when two entries have identical HLC timestamps.
6///
7/// Serialized as 8 bytes big-endian for consistent byte ordering.
8#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct NodeId(u64);
10
11impl NodeId {
12    pub fn random() -> Self {
13        use rand::Rng;
14        Self(rand::thread_rng().gen())
15    }
16
17    #[inline]
18    pub fn from_u64(v: u64) -> Self {
19        Self(v)
20    }
21
22    #[inline]
23    pub fn as_u64(&self) -> u64 {
24        self.0
25    }
26
27    #[inline]
28    pub fn to_bytes(&self) -> [u8; 8] {
29        self.0.to_be_bytes()
30    }
31
32    #[inline]
33    pub fn from_bytes(b: [u8; 8]) -> Self {
34        Self(u64::from_be_bytes(b))
35    }
36}
37
38impl std::fmt::Debug for NodeId {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "NodeId({:016x})", self.0)
41    }
42}
43
44impl std::fmt::Display for NodeId {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{:016x}", self.0)
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn random_generates_unique_ids() {
56        let ids: Vec<NodeId> = (0..100).map(|_| NodeId::random()).collect();
57        for i in 0..ids.len() {
58            for j in (i + 1)..ids.len() {
59                assert_ne!(ids[i], ids[j], "collision at indices {i} and {j}");
60            }
61        }
62    }
63
64    #[test]
65    fn u64_roundtrip() {
66        let id = NodeId::from_u64(0xDEADBEEF_CAFEBABE);
67        assert_eq!(id.as_u64(), 0xDEADBEEF_CAFEBABE);
68    }
69
70    #[test]
71    fn bytes_roundtrip() {
72        let id = NodeId::from_u64(0x0123_4567_89AB_CDEF);
73        let bytes = id.to_bytes();
74        let id2 = NodeId::from_bytes(bytes);
75        assert_eq!(id, id2);
76    }
77
78    #[test]
79    fn bytes_big_endian() {
80        let id = NodeId::from_u64(0x0102_0304_0506_0708);
81        let bytes = id.to_bytes();
82        assert_eq!(bytes, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
83    }
84
85    #[test]
86    fn ordering_consistent() {
87        let a = NodeId::from_u64(100);
88        let b = NodeId::from_u64(200);
89        assert!(a < b);
90
91        let c = NodeId::from_u64(100);
92        assert_eq!(a, c);
93    }
94
95    #[test]
96    fn display_hex() {
97        let id = NodeId::from_u64(0xFF);
98        assert_eq!(format!("{id}"), "00000000000000ff");
99    }
100
101    #[test]
102    fn debug_hex() {
103        let id = NodeId::from_u64(0xFF);
104        assert_eq!(format!("{id:?}"), "NodeId(00000000000000ff)");
105    }
106
107    #[test]
108    fn hash_consistency() {
109        use std::collections::HashSet;
110        let a = NodeId::from_u64(42);
111        let b = NodeId::from_u64(42);
112        let c = NodeId::from_u64(43);
113
114        let mut set = HashSet::new();
115        set.insert(a);
116        assert!(set.contains(&b));
117        assert!(!set.contains(&c));
118    }
119}