Skip to main content

frink_api/
request_id.rs

1//! Server-assigned request ids.
2//!
3//! The id is stated by the server in the response (and in the *first*
4//! streamed chunk, before any content), so a client never has to invent
5//! one or correlate by heuristic. The alternative -- matching a live
6//! request against a metrics snapshot by "the newest one that looks like
7//! mine" -- is what a UI is forced into when the server stays silent,
8//! and it mis-attributes as soon as two chats run at once.
9//!
10//! Format: `chatcmpl-` + a per-process random-ish stamp + a monotonic
11//! counter. The stamp makes ids from two runs of the same binary
12//! distinguishable (so a log spanning a restart does not collide); the
13//! counter makes them unique within a run without a lock or an RNG
14//! dependency. The prefix matches OpenAI's, which some clients display.
15
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// OpenAI uses this prefix for chat completion ids; clients occasionally
19/// display or match on it.
20pub const PREFIX: &str = "chatcmpl-";
21
22static COUNTER: AtomicU64 = AtomicU64::new(0);
23
24/// Per-process stamp: the low bits of the wall clock at first use. Not
25/// a security token -- it only needs to differ between restarts.
26///
27/// Public because `frink-server`'s conversation ids need the same
28/// stamp for the same reason, and had a byte-identical copy of this
29/// function whose doc comment said it was mirroring this one. A copy
30/// that says it is a copy is still a copy.
31pub fn process_stamp() -> u64 {
32    use std::sync::OnceLock;
33    static STAMP: OnceLock<u64> = OnceLock::new();
34    *STAMP.get_or_init(|| {
35        std::time::SystemTime::now()
36            .duration_since(std::time::UNIX_EPOCH)
37            .map(|d| d.as_nanos() as u64)
38            .unwrap_or(0)
39    })
40}
41
42/// A fresh request id. Unique within this process, and in practice
43/// across restarts.
44pub fn next_request_id() -> String {
45    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
46    format!(
47        "{PREFIX}{:012x}{:06x}",
48        process_stamp() & 0xffff_ffff_ffff,
49        n
50    )
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn ids_are_unique_and_prefixed() {
59        let a = next_request_id();
60        let b = next_request_id();
61        assert_ne!(a, b);
62        assert!(a.starts_with(PREFIX), "{a}");
63        assert!(b.starts_with(PREFIX), "{b}");
64    }
65
66    #[test]
67    fn ids_are_stable_width_for_the_first_million_requests() {
68        // A UI that column-aligns a request log should not have the
69        // column jump on request 16.
70        let first = next_request_id();
71        for _ in 0..64 {
72            assert_eq!(next_request_id().len(), first.len());
73        }
74    }
75
76    #[test]
77    fn ids_are_unique_across_threads() {
78        let handles: Vec<_> = (0..8)
79            .map(|_| std::thread::spawn(|| (0..64).map(|_| next_request_id()).collect::<Vec<_>>()))
80            .collect();
81        let mut seen = std::collections::BTreeSet::new();
82        for h in handles {
83            for id in h.join().unwrap() {
84                assert!(seen.insert(id.clone()), "duplicate id {id}");
85            }
86        }
87    }
88}