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    /// Generate a random node ID using OS entropy.
13    pub fn random() -> Self {
14        use rand::Rng;
15        Self(rand::thread_rng().gen())
16    }
17
18    /// Create from a raw `u64` value.
19    #[inline]
20    pub fn from_u64(v: u64) -> Self {
21        Self(v)
22    }
23
24    /// Get the raw `u64` value.
25    #[inline]
26    pub fn as_u64(&self) -> u64 {
27        self.0
28    }
29
30    /// Big-endian byte serialization.
31    #[inline]
32    pub fn to_bytes(&self) -> [u8; 8] {
33        self.0.to_be_bytes()
34    }
35
36    /// Reconstruct from big-endian bytes.
37    #[inline]
38    pub fn from_bytes(b: [u8; 8]) -> Self {
39        Self(u64::from_be_bytes(b))
40    }
41}
42
43impl std::fmt::Debug for NodeId {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "NodeId({:016x})", self.0)
46    }
47}
48
49impl std::fmt::Display for NodeId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{:016x}", self.0)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn random_generates_unique_ids() {
61        let ids: Vec<NodeId> = (0..100).map(|_| NodeId::random()).collect();
62        for i in 0..ids.len() {
63            for j in (i + 1)..ids.len() {
64                assert_ne!(ids[i], ids[j], "collision at indices {i} and {j}");
65            }
66        }
67    }
68
69    #[test]
70    fn u64_roundtrip() {
71        let id = NodeId::from_u64(0xDEADBEEF_CAFEBABE);
72        assert_eq!(id.as_u64(), 0xDEADBEEF_CAFEBABE);
73    }
74
75    #[test]
76    fn bytes_roundtrip() {
77        let id = NodeId::from_u64(0x0123_4567_89AB_CDEF);
78        let bytes = id.to_bytes();
79        let id2 = NodeId::from_bytes(bytes);
80        assert_eq!(id, id2);
81    }
82
83    #[test]
84    fn bytes_big_endian() {
85        let id = NodeId::from_u64(0x0102_0304_0506_0708);
86        let bytes = id.to_bytes();
87        assert_eq!(bytes, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
88    }
89
90    #[test]
91    fn ordering_consistent() {
92        let a = NodeId::from_u64(100);
93        let b = NodeId::from_u64(200);
94        assert!(a < b);
95
96        let c = NodeId::from_u64(100);
97        assert_eq!(a, c);
98    }
99
100    #[test]
101    fn display_hex() {
102        let id = NodeId::from_u64(0xFF);
103        assert_eq!(format!("{id}"), "00000000000000ff");
104    }
105
106    #[test]
107    fn debug_hex() {
108        let id = NodeId::from_u64(0xFF);
109        assert_eq!(format!("{id:?}"), "NodeId(00000000000000ff)");
110    }
111
112    #[test]
113    fn hash_consistency() {
114        use std::collections::HashSet;
115        let a = NodeId::from_u64(42);
116        let b = NodeId::from_u64(42);
117        let c = NodeId::from_u64(43);
118
119        let mut set = HashSet::new();
120        set.insert(a);
121        assert!(set.contains(&b));
122        assert!(!set.contains(&c));
123    }
124}