Skip to main content

oxibonsai_runtime/
request_id.rs

1//! Structured request identifiers for end-to-end tracing.
2//!
3//! Each request gets a 128-bit `RequestId` rendered as a 32-hex-character
4//! UUIDv4-style string (with the version + variant bits set per RFC 4122).
5//!
6//! No external `uuid` or `rand` dependency is required: the generator is
7//! a thread-safe SplitMix64 stream seeded from the process start time and
8//! a per-thread counter, which is sufficient for trace-correlation purposes
9//! (uniqueness within a single server lifetime).
10//!
11//! ## Wire format
12//!
13//! ```text
14//! xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
15//! ```
16//!
17//! The version nibble is fixed at `4` and the variant nibble starts with
18//! `8`/`9`/`a`/`b` (RFC 4122 §4.4).
19//!
20//! ## Usage
21//!
22//! ```
23//! use oxibonsai_runtime::request_id::RequestId;
24//!
25//! let a = RequestId::new();
26//! let b = RequestId::new();
27//! assert_ne!(a, b);
28//! assert_eq!(a.as_hex().len(), 32);
29//! assert_eq!(a.as_uuid().len(), 36);
30//! ```
31
32use std::sync::atomic::{AtomicU64, Ordering};
33use std::time::{SystemTime, UNIX_EPOCH};
34
35// ─── Core type ─────────────────────────────────────────────────────────────
36
37/// 128-bit request identifier rendered as RFC 4122 UUIDv4.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct RequestId {
40    high: u64,
41    low: u64,
42}
43
44impl RequestId {
45    /// Construct a fresh `RequestId` from the global generator.
46    pub fn new() -> Self {
47        let high = next_u64();
48        let low = next_u64();
49        Self::from_pair(high, low)
50    }
51
52    /// Construct a `RequestId` from raw 64-bit halves, applying the UUIDv4
53    /// version (0x4) and variant (0b10) bits.
54    pub fn from_pair(high: u64, low: u64) -> Self {
55        // UUIDv4: high nibble of byte 6 = 0x4
56        let high = (high & !0x0000_0000_0000_F000) | 0x0000_0000_0000_4000;
57        // Variant: top two bits of byte 8 = 0b10 (i.e. nibble in {8,9,a,b})
58        let low = (low & !0xC000_0000_0000_0000) | 0x8000_0000_0000_0000;
59        Self { high, low }
60    }
61
62    /// 32-character lowercase hex (no dashes).
63    pub fn as_hex(&self) -> String {
64        format!("{:016x}{:016x}", self.high, self.low)
65    }
66
67    /// 36-character UUID format with dashes (`8-4-4-4-12`).
68    pub fn as_uuid(&self) -> String {
69        let h = self.as_hex();
70        // Bytes 0-3 (8 hex), 4-5 (4), 6-7 (4), 8-9 (4), 10-15 (12)
71        format!(
72            "{}-{}-{}-{}-{}",
73            &h[0..8],
74            &h[8..12],
75            &h[12..16],
76            &h[16..20],
77            &h[20..32]
78        )
79    }
80
81    /// High 64 bits.
82    pub fn high(&self) -> u64 {
83        self.high
84    }
85
86    /// Low 64 bits.
87    pub fn low(&self) -> u64 {
88        self.low
89    }
90
91    /// Parse a 32-char hex string (no dashes) back into a [`RequestId`].
92    ///
93    /// Returns `None` if the input is malformed.
94    pub fn from_hex(s: &str) -> Option<Self> {
95        if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
96            return None;
97        }
98        let high = u64::from_str_radix(&s[0..16], 16).ok()?;
99        let low = u64::from_str_radix(&s[16..32], 16).ok()?;
100        Some(Self { high, low })
101    }
102
103    /// Parse a UUID-formatted string (with dashes) back into a [`RequestId`].
104    pub fn from_uuid(s: &str) -> Option<Self> {
105        if s.len() != 36 {
106            return None;
107        }
108        // Strip the four dashes and dispatch to `from_hex`.
109        let mut buf = String::with_capacity(32);
110        for (i, c) in s.chars().enumerate() {
111            match i {
112                8 | 13 | 18 | 23 => {
113                    if c != '-' {
114                        return None;
115                    }
116                }
117                _ => buf.push(c),
118            }
119        }
120        Self::from_hex(&buf)
121    }
122
123    /// Return the raw 16 bytes of this request id in big-endian order
124    /// (high half first).
125    ///
126    /// Equivalent to `[high.to_be_bytes(), low.to_be_bytes()].concat()`,
127    /// without an allocation. Useful for binary protocols that store
128    /// request ids alongside other binary payloads.
129    pub fn as_bytes(&self) -> [u8; 16] {
130        let h = self.high.to_be_bytes();
131        let l = self.low.to_be_bytes();
132        let mut out = [0u8; 16];
133        out[..8].copy_from_slice(&h);
134        out[8..].copy_from_slice(&l);
135        out
136    }
137
138    /// Reconstruct a [`RequestId`] from its 16-byte big-endian representation.
139    ///
140    /// Note: this preserves the bytes as-is (UUIDv4 version + variant nibbles
141    /// are NOT re-imposed), so round-tripping through `as_bytes -> from_bytes`
142    /// yields the same id. To enforce the v4 layout from arbitrary bytes,
143    /// pipe through [`RequestId::from_pair`] explicitly.
144    pub fn from_bytes(bytes: [u8; 16]) -> Self {
145        let mut h_arr = [0u8; 8];
146        let mut l_arr = [0u8; 8];
147        h_arr.copy_from_slice(&bytes[..8]);
148        l_arr.copy_from_slice(&bytes[8..]);
149        Self {
150            high: u64::from_be_bytes(h_arr),
151            low: u64::from_be_bytes(l_arr),
152        }
153    }
154}
155
156impl Default for RequestId {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl std::fmt::Display for RequestId {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        write!(f, "{}", self.as_uuid())
165    }
166}
167
168// ─── Thread-safe SplitMix64 generator ──────────────────────────────────────
169
170static GLOBAL_STATE: AtomicU64 = AtomicU64::new(0);
171
172fn ensure_seeded() {
173    if GLOBAL_STATE.load(Ordering::Relaxed) == 0 {
174        // Seed from process start time; mix in a constant to ensure non-zero.
175        let nanos = SystemTime::now()
176            .duration_since(UNIX_EPOCH)
177            .map(|d| d.as_nanos() as u64)
178            .unwrap_or(0xa3b1_c4d5_e6f7_8901);
179        let seed = nanos ^ 0x9E37_79B9_7F4A_7C15; // golden-ratio constant
180        let _ = GLOBAL_STATE.compare_exchange(0, seed, Ordering::Relaxed, Ordering::Relaxed);
181    }
182}
183
184fn next_u64() -> u64 {
185    ensure_seeded();
186    // SplitMix64 step on the global counter.
187    let prev = GLOBAL_STATE.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
188    let mut z = prev.wrapping_add(0x9E37_79B9_7F4A_7C15);
189    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
190    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
191    z ^ (z >> 31)
192}
193
194// ─── Tests ─────────────────────────────────────────────────────────────────
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use std::collections::HashSet;
200
201    #[test]
202    fn new_is_unique() {
203        let mut set = HashSet::new();
204        for _ in 0..2000 {
205            let id = RequestId::new();
206            assert!(set.insert(id), "duplicate request id observed");
207        }
208    }
209
210    #[test]
211    fn hex_is_32_chars() {
212        let id = RequestId::new();
213        let h = id.as_hex();
214        assert_eq!(h.len(), 32);
215        assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
216    }
217
218    #[test]
219    fn uuid_format_is_well_formed() {
220        let id = RequestId::new();
221        let s = id.as_uuid();
222        assert_eq!(s.len(), 36);
223        let parts: Vec<&str> = s.split('-').collect();
224        assert_eq!(parts.len(), 5);
225        assert_eq!(parts[0].len(), 8);
226        assert_eq!(parts[1].len(), 4);
227        assert_eq!(parts[2].len(), 4);
228        assert_eq!(parts[3].len(), 4);
229        assert_eq!(parts[4].len(), 12);
230        // Version must be 4
231        assert!(parts[2].starts_with('4'));
232        // Variant must be 8/9/a/b
233        let variant = parts[3].chars().next().expect("non-empty variant nibble");
234        assert!(matches!(variant, '8' | '9' | 'a' | 'b'));
235    }
236
237    #[test]
238    fn from_pair_sets_version_and_variant() {
239        let id = RequestId::from_pair(0xFFFF_FFFF_FFFF_FFFF, 0xFFFF_FFFF_FFFF_FFFF);
240        let h = id.as_hex();
241        // Position 12 is the version nibble
242        assert_eq!(&h[12..13], "4");
243        // Position 16 is the variant nibble — must be one of 8,9,a,b
244        let v = h.chars().nth(16).expect("variant nibble");
245        assert!(matches!(v, '8' | '9' | 'a' | 'b'));
246    }
247
248    #[test]
249    fn round_trip_hex() {
250        let id = RequestId::new();
251        let s = id.as_hex();
252        let parsed = RequestId::from_hex(&s).expect("hex parse");
253        assert_eq!(id, parsed);
254    }
255
256    #[test]
257    fn round_trip_uuid() {
258        let id = RequestId::new();
259        let s = id.as_uuid();
260        let parsed = RequestId::from_uuid(&s).expect("uuid parse");
261        assert_eq!(id, parsed);
262    }
263
264    #[test]
265    fn rejects_bad_hex() {
266        assert!(RequestId::from_hex("").is_none());
267        assert!(RequestId::from_hex("too-short").is_none());
268        assert!(RequestId::from_hex(&"x".repeat(32)).is_none());
269        // Wrong length but valid hex chars
270        assert!(RequestId::from_hex(&"a".repeat(31)).is_none());
271        assert!(RequestId::from_hex(&"a".repeat(33)).is_none());
272    }
273
274    #[test]
275    fn rejects_bad_uuid() {
276        assert!(RequestId::from_uuid("not-a-uuid").is_none());
277        // Right length, wrong dash positions
278        assert!(RequestId::from_uuid(&"a".repeat(36)).is_none());
279        // Right length and dashes, but non-hex
280        assert!(RequestId::from_uuid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").is_none());
281    }
282
283    #[test]
284    fn display_uses_uuid_format() {
285        let id = RequestId::new();
286        let s = format!("{id}");
287        assert_eq!(s, id.as_uuid());
288    }
289
290    #[test]
291    fn as_bytes_round_trip() {
292        let id = RequestId::new();
293        let bytes = id.as_bytes();
294        let recovered = RequestId::from_bytes(bytes);
295        assert_eq!(id, recovered);
296    }
297
298    #[test]
299    fn as_bytes_big_endian_layout() {
300        let id = RequestId::from_pair(0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210);
301        let bytes = id.as_bytes();
302        // First 8 bytes are the high half in big-endian order.
303        assert_eq!(bytes[0], 0x01);
304        assert_eq!(bytes[1], 0x23);
305        assert_eq!(bytes[6], 0x4D); // 0x4 nibble (UUIDv4 version) was set on byte 6
306                                    // Byte 8 has the variant nibble set in the high 2 bits.
307        let variant = bytes[8] >> 6;
308        assert_eq!(variant, 0b10);
309    }
310
311    #[test]
312    fn from_bytes_preserves_arbitrary_bytes() {
313        // from_bytes does NOT re-impose the v4 layout — round-trip is exact.
314        let bytes = [0u8; 16];
315        let id = RequestId::from_bytes(bytes);
316        assert_eq!(id.as_bytes(), bytes);
317    }
318
319    #[test]
320    fn high_low_recoverable() {
321        let id = RequestId::from_pair(0x1234_5678_9abc_def0, 0xfedc_ba98_7654_3210);
322        // After version/variant masking, high() should still match the
323        // exact 64-bit half stored.
324        let h_hex = format!("{:016x}", id.high());
325        let l_hex = format!("{:016x}", id.low());
326        assert_eq!(id.as_hex(), format!("{h_hex}{l_hex}"));
327    }
328
329    #[test]
330    fn concurrent_generation_is_unique() {
331        use std::sync::Arc;
332        use std::sync::Mutex;
333        use std::thread;
334
335        let collected = Arc::new(Mutex::new(HashSet::new()));
336        let mut handles = Vec::new();
337        for _ in 0..8 {
338            let collected = Arc::clone(&collected);
339            handles.push(thread::spawn(move || {
340                let mut local = HashSet::new();
341                for _ in 0..500 {
342                    local.insert(RequestId::new());
343                }
344                let mut g = collected.lock().expect("lock poisoned");
345                for id in local {
346                    assert!(g.insert(id), "duplicate id from concurrent generation");
347                }
348            }));
349        }
350        for h in handles {
351            h.join().expect("thread panic");
352        }
353        assert_eq!(collected.lock().expect("lock").len(), 8 * 500);
354    }
355}