Skip to main content

riffdb_client_rust/
ids.rs

1//! Stateless client-side UUIDv7 generation.
2
3use std::error::Error;
4use std::fmt;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use riffdb_types::{
8    AgentSessionId, ApplicationExportOperationId, ApplicationInstallationCampaignId, CapabilityId,
9    ContractMigrationOperationId, OfflineMaintenanceOperationId, RequestId,
10    UuidV7ConstructionError,
11};
12
13const MAX_UUID_V7_UNIX_MILLISECONDS: u128 = 0xffff_ffff_ffff;
14
15/// A safe local failure while generating one client identifier.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum IdentifierGenerationError {
18    /// The host clock is before the Unix epoch.
19    ClockBeforeUnixEpoch,
20    /// The host clock cannot be represented by the UUIDv7 48-bit timestamp.
21    TimestampOutOfRange,
22    /// The operating-system entropy provider failed.
23    EntropyUnavailable,
24}
25
26impl fmt::Display for IdentifierGenerationError {
27    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28        formatter.write_str(match self {
29            Self::ClockBeforeUnixEpoch => "system clock is before the Unix epoch",
30            Self::TimestampOutOfRange => "system clock is outside the UUIDv7 range",
31            Self::EntropyUnavailable => "operating-system entropy is unavailable",
32        })
33    }
34}
35
36impl Error for IdentifierGenerationError {}
37
38/// Stateless production source for client-owned UUIDv7 values.
39///
40/// Every method samples the system clock once, performs one ten-byte entropy
41/// fill, and retains no ordering or uniqueness state.
42#[derive(Clone, Copy, Debug, Default)]
43pub struct SystemIdSource;
44
45impl SystemIdSource {
46    /// Constructs the stateless production source.
47    #[must_use]
48    pub const fn new() -> Self {
49        Self
50    }
51
52    /// Generates one fresh outer transport request identifier.
53    pub fn request_id(self) -> Result<RequestId, IdentifierGenerationError> {
54        generate_with(
55            SystemTime::now,
56            fill_system_random,
57            RequestId::from_unix_milliseconds_and_random,
58        )
59    }
60
61    /// Generates one caller-selected capability identifier.
62    pub fn capability_id(self) -> Result<CapabilityId, IdentifierGenerationError> {
63        generate_with(
64            SystemTime::now,
65            fill_system_random,
66            CapabilityId::from_unix_milliseconds_and_random,
67        )
68    }
69
70    /// Generates one optional caller agent-session identifier.
71    pub fn agent_session_id(self) -> Result<AgentSessionId, IdentifierGenerationError> {
72        generate_with(
73            SystemTime::now,
74            fill_system_random,
75            AgentSessionId::from_unix_milliseconds_and_random,
76        )
77    }
78
79    /// Generates one caller-stable offline-maintenance operation identifier.
80    pub fn offline_maintenance_operation_id(
81        self,
82    ) -> Result<OfflineMaintenanceOperationId, IdentifierGenerationError> {
83        generate_with(
84            SystemTime::now,
85            fill_system_random,
86            OfflineMaintenanceOperationId::from_unix_milliseconds_and_random,
87        )
88    }
89
90    /// Generates one caller-stable contract-migration operation identifier.
91    pub fn contract_migration_operation_id(
92        self,
93    ) -> Result<ContractMigrationOperationId, IdentifierGenerationError> {
94        generate_with(
95            SystemTime::now,
96            fill_system_random,
97            ContractMigrationOperationId::from_unix_milliseconds_and_random,
98        )
99    }
100
101    /// Generates one caller-stable symbolic-export operation identifier.
102    pub fn application_export_operation_id(
103        self,
104    ) -> Result<ApplicationExportOperationId, IdentifierGenerationError> {
105        generate_with(
106            SystemTime::now,
107            fill_system_random,
108            ApplicationExportOperationId::from_unix_milliseconds_and_random,
109        )
110    }
111
112    /// Generates one caller-stable application installation or reimport campaign identifier.
113    pub fn application_installation_campaign_id(
114        self,
115    ) -> Result<ApplicationInstallationCampaignId, IdentifierGenerationError> {
116        generate_with(
117            SystemTime::now,
118            fill_system_random,
119            ApplicationInstallationCampaignId::from_unix_milliseconds_and_random,
120        )
121    }
122}
123
124/// Generates one fresh outer transport request identifier.
125pub fn generate_request_id() -> Result<RequestId, IdentifierGenerationError> {
126    SystemIdSource::new().request_id()
127}
128
129/// Generates one caller-selected capability identifier.
130pub fn generate_capability_id() -> Result<CapabilityId, IdentifierGenerationError> {
131    SystemIdSource::new().capability_id()
132}
133
134/// Generates one optional caller agent-session identifier.
135pub fn generate_agent_session_id() -> Result<AgentSessionId, IdentifierGenerationError> {
136    SystemIdSource::new().agent_session_id()
137}
138
139/// Generates one caller-stable offline-maintenance operation identifier.
140pub fn generate_offline_maintenance_operation_id()
141-> Result<OfflineMaintenanceOperationId, IdentifierGenerationError> {
142    SystemIdSource::new().offline_maintenance_operation_id()
143}
144
145/// Generates one caller-stable contract-migration operation identifier.
146pub fn generate_contract_migration_operation_id()
147-> Result<ContractMigrationOperationId, IdentifierGenerationError> {
148    SystemIdSource::new().contract_migration_operation_id()
149}
150
151/// Generates one caller-stable symbolic-export operation identifier.
152pub fn generate_application_export_operation_id()
153-> Result<ApplicationExportOperationId, IdentifierGenerationError> {
154    SystemIdSource::new().application_export_operation_id()
155}
156
157/// Generates one caller-stable application installation or reimport campaign identifier.
158pub fn generate_application_installation_campaign_id()
159-> Result<ApplicationInstallationCampaignId, IdentifierGenerationError> {
160    SystemIdSource::new().application_installation_campaign_id()
161}
162
163const SYSTEM_RANDOM_CHUNK_COUNT: usize = 25;
164const SYSTEM_RANDOM_BUFFER_BYTES: usize = SYSTEM_RANDOM_CHUNK_COUNT * 10;
165
166struct ThreadRandomBuffer {
167    bytes: [u8; SYSTEM_RANDOM_BUFFER_BYTES],
168    offset: usize,
169}
170
171impl Default for ThreadRandomBuffer {
172    fn default() -> Self {
173        Self {
174            bytes: [0_u8; SYSTEM_RANDOM_BUFFER_BYTES],
175            offset: SYSTEM_RANDOM_BUFFER_BYTES,
176        }
177    }
178}
179
180std::thread_local! {
181    static THREAD_RANDOM_BUFFER: std::cell::RefCell<ThreadRandomBuffer> =
182        std::cell::RefCell::new(ThreadRandomBuffer::default());
183}
184
185fn fill_system_random(output: &mut [u8; 10]) -> Result<(), IdentifierGenerationError> {
186    THREAD_RANDOM_BUFFER.with(|cell| {
187        let Ok(mut buf) = cell.try_borrow_mut() else {
188            return getrandom::fill(output)
189                .map_err(|_| IdentifierGenerationError::EntropyUnavailable);
190        };
191        if buf.offset + 10 > SYSTEM_RANDOM_BUFFER_BYTES {
192            if getrandom::fill(&mut buf.bytes).is_err() {
193                buf.offset = SYSTEM_RANDOM_BUFFER_BYTES;
194                return Err(IdentifierGenerationError::EntropyUnavailable);
195            }
196            buf.offset = 0;
197        }
198        let start = buf.offset;
199        let end = start + 10;
200        output.copy_from_slice(&buf.bytes[start..end]);
201        buf.bytes[start..end].fill(0);
202        buf.offset = end;
203        Ok(())
204    })
205}
206
207fn generate_with<T>(
208    now: impl FnOnce() -> SystemTime,
209    fill: impl FnOnce(&mut [u8; 10]) -> Result<(), IdentifierGenerationError>,
210    construct: fn(u64, [u8; 10]) -> Result<T, UuidV7ConstructionError>,
211) -> Result<T, IdentifierGenerationError> {
212    let elapsed = now()
213        .duration_since(UNIX_EPOCH)
214        .map_err(|_| IdentifierGenerationError::ClockBeforeUnixEpoch)?;
215    let milliseconds = elapsed.as_millis();
216    if milliseconds > MAX_UUID_V7_UNIX_MILLISECONDS {
217        return Err(IdentifierGenerationError::TimestampOutOfRange);
218    }
219    let milliseconds =
220        u64::try_from(milliseconds).map_err(|_| IdentifierGenerationError::TimestampOutOfRange)?;
221
222    let mut random = [0_u8; 10];
223    fill(&mut random)?;
224    construct(milliseconds, random).map_err(|_| IdentifierGenerationError::TimestampOutOfRange)
225}
226
227#[cfg(test)]
228mod tests {
229    use std::sync::Mutex;
230    use std::sync::atomic::{AtomicUsize, Ordering};
231    use std::time::Duration;
232
233    use super::*;
234
235    #[test]
236    fn one_candidate_samples_clock_then_fills_exactly_ten_bytes_once() {
237        let calls = Mutex::new(Vec::new());
238        let clock_calls = AtomicUsize::new(0);
239        let entropy_calls = AtomicUsize::new(0);
240
241        let request_id = generate_with(
242            || {
243                clock_calls.fetch_add(1, Ordering::SeqCst);
244                calls.lock().expect("call log").push("clock");
245                UNIX_EPOCH + Duration::from_millis(0x0123_4567_89ab)
246            },
247            |output| {
248                entropy_calls.fetch_add(1, Ordering::SeqCst);
249                calls.lock().expect("call log").push("entropy");
250                assert_eq!(output.len(), 10);
251                output.copy_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
252                Ok(())
253            },
254            RequestId::from_unix_milliseconds_and_random,
255        )
256        .expect("candidate");
257
258        assert_eq!(clock_calls.load(Ordering::SeqCst), 1);
259        assert_eq!(entropy_calls.load(Ordering::SeqCst), 1);
260        assert_eq!(*calls.lock().expect("call log"), ["clock", "entropy"]);
261        assert_eq!(
262            request_id.to_string(),
263            "01234567-89ab-7001-8203-040506070809"
264        );
265    }
266
267    #[test]
268    fn invalid_clock_never_requests_entropy() {
269        let entropy_calls = AtomicUsize::new(0);
270        let result = generate_with(
271            || {
272                UNIX_EPOCH
273                    .checked_sub(Duration::from_millis(1))
274                    .expect("time")
275            },
276            |_| {
277                entropy_calls.fetch_add(1, Ordering::SeqCst);
278                Ok(())
279            },
280            CapabilityId::from_unix_milliseconds_and_random,
281        );
282        assert_eq!(result, Err(IdentifierGenerationError::ClockBeforeUnixEpoch));
283        assert_eq!(entropy_calls.load(Ordering::SeqCst), 0);
284    }
285
286    #[test]
287    fn entropy_failure_returns_no_partial_identifier_and_does_not_retry() {
288        let entropy_calls = AtomicUsize::new(0);
289        let result = generate_with(
290            || UNIX_EPOCH,
291            |_| {
292                entropy_calls.fetch_add(1, Ordering::SeqCst);
293                Err(IdentifierGenerationError::EntropyUnavailable)
294            },
295            AgentSessionId::from_unix_milliseconds_and_random,
296        );
297        assert_eq!(result, Err(IdentifierGenerationError::EntropyUnavailable));
298        assert_eq!(entropy_calls.load(Ordering::SeqCst), 1);
299    }
300
301    #[test]
302    fn timestamp_above_uuid_v7_range_never_requests_entropy() {
303        let entropy_calls = AtomicUsize::new(0);
304        let duration = Duration::from_millis(0x1_0000_0000_0000);
305        let result = generate_with(
306            || UNIX_EPOCH + duration,
307            |_| {
308                entropy_calls.fetch_add(1, Ordering::SeqCst);
309                Ok(())
310            },
311            RequestId::from_unix_milliseconds_and_random,
312        );
313        assert_eq!(result, Err(IdentifierGenerationError::TimestampOutOfRange));
314        assert_eq!(entropy_calls.load(Ordering::SeqCst), 0);
315    }
316}