Skip to main content

nodedb_array/sync/
replica_id.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Stable per-replica identity.
4//!
5//! Each array-engine replica (Lite instance or Origin shard) is assigned a
6//! [`ReplicaId`] on first open and stored under `Namespace::Meta::"replica_id"`.
7//! The id is derived from the low 64 bits of a UUID v7, giving wall-clock
8//! ordering with sufficient entropy.
9
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use crate::error::{ArrayError, ArrayResult};
17
18/// Stable 64-bit replica identity.
19///
20/// Represented as the low 64 bits of a UUID v7. Total order matches `u64`
21/// numeric order, so HLC tiebreaks are deterministic.
22#[derive(
23    Copy,
24    Clone,
25    Debug,
26    PartialEq,
27    Eq,
28    Hash,
29    PartialOrd,
30    Ord,
31    Serialize,
32    Deserialize,
33    zerompk::ToMessagePack,
34    zerompk::FromMessagePack,
35)]
36pub struct ReplicaId(pub u64);
37
38impl ReplicaId {
39    /// Construct a [`ReplicaId`] from a known `u64` value (e.g. loaded from
40    /// persistent storage).
41    pub const fn new(id: u64) -> Self {
42        Self(id)
43    }
44
45    /// Generate a fresh, probabilistically unique [`ReplicaId`] using UUID v7.
46    ///
47    /// Takes the low 64 bits of the UUID's 128-bit representation. UUID v7
48    /// encodes a millisecond timestamp in the high bits, so the low 64 bits
49    /// carry a mix of sub-ms precision and random bits — unique enough for
50    /// replica identity.
51    pub fn generate() -> Self {
52        Self(Uuid::now_v7().as_u128() as u64)
53    }
54
55    /// Return the underlying `u64`.
56    pub fn as_u64(&self) -> u64 {
57        self.0
58    }
59}
60
61impl fmt::Display for ReplicaId {
62    /// Format as a 16-character lowercase hex string.
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{:016x}", self.0)
65    }
66}
67
68impl FromStr for ReplicaId {
69    type Err = ArrayError;
70
71    /// Parse a 16-character lowercase hex string back into a [`ReplicaId`].
72    fn from_str(s: &str) -> ArrayResult<Self> {
73        u64::from_str_radix(s, 16)
74            .map(ReplicaId)
75            .map_err(|_| ArrayError::InvalidReplicaId {
76                detail: format!("invalid replica_id hex: {s}"),
77            })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn generate_is_unique() {
87        let a = ReplicaId::generate();
88        let b = ReplicaId::generate();
89        // Statistically guaranteed; would require a UUID collision to fail.
90        assert_ne!(a, b);
91    }
92
93    #[test]
94    fn roundtrip_hex() {
95        let id = ReplicaId::new(0xdeadbeef_cafebabe);
96        let s = id.to_string();
97        assert_eq!(s, "deadbeefcafebabe");
98        let parsed: ReplicaId = s.parse().unwrap();
99        assert_eq!(parsed, id);
100    }
101
102    #[test]
103    fn parse_invalid_returns_err() {
104        let result: ArrayResult<ReplicaId> = "not_hex_at_all!".parse();
105        assert!(matches!(result, Err(ArrayError::InvalidReplicaId { .. })));
106    }
107
108    #[test]
109    fn as_u64_round_trips() {
110        let val = 0x0102030405060708_u64;
111        let id = ReplicaId::new(val);
112        assert_eq!(id.as_u64(), val);
113    }
114
115    #[test]
116    fn serialize_roundtrip() {
117        let id = ReplicaId::generate();
118        let bytes = zerompk::to_msgpack_vec(&id).expect("serialize");
119        let back: ReplicaId = zerompk::from_msgpack(&bytes).expect("deserialize");
120        assert_eq!(id, back);
121    }
122}