Skip to main content

qubit_id/snowflake/
sonyflake_generator.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Sonyflake-style 63-bit ID generator.
11
12use std::sync::{
13    Arc,
14    Mutex,
15};
16use std::thread;
17use std::time::{
18    Duration,
19    SystemTime,
20    UNIX_EPOCH,
21};
22
23use super::time_slice::TimeSlice;
24use crate::{
25    IdError,
26    IdGenerator,
27};
28
29const DEFAULT_BITS_SEQUENCE: u8 = 8;
30const DEFAULT_BITS_MACHINE: u8 = 16;
31const DEFAULT_TIME_UNIT_NANOS: u128 = 10_000_000;
32const MIN_TIME_UNIT_NANOS: u128 = 1_000_000;
33const DEFAULT_START_MILLIS: u64 = 1_735_689_600_000;
34
35/// Sonyflake-style generator using configurable time, sequence, and machine bits.
36///
37/// By default, the layout is compatible with Sonyflake's commonly documented
38/// allocation: 39 bits of time in 10 ms units, 8 sequence bits, and 16 machine
39/// bits. The sign bit is not used.
40pub struct SonyflakeGenerator {
41    bits_time: u8,
42    bits_sequence: u8,
43    bits_machine: u8,
44    time_unit: Duration,
45    start_time: SystemTime,
46    machine_id: u64,
47    clock: Arc<dyn Fn() -> SystemTime + Send + Sync>,
48    state: Mutex<TimeSlice>,
49}
50
51impl SonyflakeGenerator {
52    /// Creates a Sonyflake-style generator with default layout and epoch.
53    ///
54    /// # Parameters
55    /// - `machine_id`: Machine identifier in `0..=65535`.
56    ///
57    /// # Returns
58    /// A configured generator.
59    ///
60    /// # Errors
61    /// Returns [`IdError::MachineIdOutOfRange`] when `machine_id` does not fit
62    /// in the default 16-bit machine field.
63    pub fn new(machine_id: u64) -> Result<Self, IdError> {
64        Self::with_epoch(
65            machine_id,
66            UNIX_EPOCH + Duration::from_millis(DEFAULT_START_MILLIS),
67        )
68    }
69
70    /// Creates a Sonyflake-style generator with default layout and explicit epoch.
71    ///
72    /// # Parameters
73    /// - `machine_id`: Machine identifier in `0..=65535`.
74    /// - `start_time`: Start time used as elapsed-time origin.
75    ///
76    /// # Returns
77    /// A configured generator using the system clock.
78    ///
79    /// # Errors
80    /// Returns the same errors as [`SonyflakeGenerator::with_options`].
81    pub fn with_epoch(machine_id: u64, start_time: SystemTime) -> Result<Self, IdError> {
82        Self::with_options(
83            machine_id,
84            DEFAULT_BITS_SEQUENCE,
85            DEFAULT_BITS_MACHINE,
86            Duration::from_nanos(DEFAULT_TIME_UNIT_NANOS as u64),
87            start_time,
88        )
89    }
90
91    /// Creates a Sonyflake-style generator with explicit layout.
92    ///
93    /// Passing `0` for either bit length selects the Sonyflake default for that
94    /// field.
95    ///
96    /// # Parameters
97    /// - `machine_id`: Machine identifier.
98    /// - `bits_sequence`: Sequence bit length, or `0` for default.
99    /// - `bits_machine_id`: Machine bit length, or `0` for default.
100    /// - `time_unit`: Time unit; must be at least one millisecond.
101    /// - `start_time`: Start time used as elapsed-time origin.
102    ///
103    /// # Returns
104    /// A configured generator using the system clock.
105    ///
106    /// # Errors
107    /// Returns [`IdError::InvalidBitLength`] for invalid bit allocation,
108    /// [`IdError::InvalidTimeUnit`] for sub-millisecond time units,
109    /// [`IdError::StartTimeAhead`] when `start_time` is in the future, or
110    /// [`IdError::MachineIdOutOfRange`] when `machine_id` does not fit.
111    pub fn with_options(
112        machine_id: u64,
113        bits_sequence: u8,
114        bits_machine_id: u8,
115        time_unit: Duration,
116        start_time: SystemTime,
117    ) -> Result<Self, IdError> {
118        Self::with_clock(
119            machine_id,
120            bits_sequence,
121            bits_machine_id,
122            time_unit,
123            start_time,
124            SystemTime::now,
125        )
126    }
127
128    /// Creates a Sonyflake-style generator with an explicit clock.
129    ///
130    /// # Parameters
131    /// - `machine_id`: Machine identifier.
132    /// - `bits_sequence`: Sequence bit length, or `0` for default.
133    /// - `bits_machine_id`: Machine bit length, or `0` for default.
134    /// - `time_unit`: Time unit; must be at least one millisecond.
135    /// - `start_time`: Start time used as elapsed-time origin.
136    /// - `clock`: Function returning the current time.
137    ///
138    /// # Returns
139    /// A configured generator.
140    ///
141    /// # Errors
142    /// Returns the same validation errors as [`SonyflakeGenerator::with_options`].
143    pub fn with_clock<F>(
144        machine_id: u64,
145        bits_sequence: u8,
146        bits_machine_id: u8,
147        time_unit: Duration,
148        start_time: SystemTime,
149        clock: F,
150    ) -> Result<Self, IdError>
151    where
152        F: Fn() -> SystemTime + Send + Sync + 'static,
153    {
154        let bits_sequence = Self::normalize_bits("sequence", bits_sequence, DEFAULT_BITS_SEQUENCE)?;
155        let bits_machine = Self::normalize_bits("machine", bits_machine_id, DEFAULT_BITS_MACHINE)?;
156        let bits_time = 63_u8
157            .checked_sub(bits_sequence)
158            .and_then(|value| value.checked_sub(bits_machine))
159            .ok_or(IdError::InvalidBitLength {
160                name: "time",
161                bits: 0,
162                reason: "63 - sequence bits - machine bits must be at least 32",
163            })?;
164        if bits_time < 32 {
165            return Err(IdError::InvalidBitLength {
166                name: "time",
167                bits: bits_time,
168                reason: "time bit length must be at least 32",
169            });
170        }
171
172        let nanos = time_unit.as_nanos();
173        if nanos < MIN_TIME_UNIT_NANOS {
174            return Err(IdError::InvalidTimeUnit {
175                nanos,
176                min_nanos: MIN_TIME_UNIT_NANOS,
177            });
178        }
179
180        if start_time > clock() {
181            return Err(IdError::StartTimeAhead);
182        }
183
184        let max_machine_id = (1_u64 << bits_machine) - 1;
185        if machine_id > max_machine_id {
186            return Err(IdError::MachineIdOutOfRange {
187                machine_id,
188                max: max_machine_id,
189            });
190        }
191
192        Ok(Self {
193            bits_time,
194            bits_sequence,
195            bits_machine,
196            time_unit,
197            start_time,
198            machine_id,
199            clock: Arc::new(clock),
200            state: Mutex::new(TimeSlice::with_sequence(0, (1_u64 << bits_sequence) - 1)),
201        })
202    }
203
204    /// Normalizes and validates a Sonyflake bit length.
205    ///
206    /// # Parameters
207    /// - `name`: Name of the setting for diagnostics.
208    /// - `bits`: Provided bit length.
209    /// - `default_bits`: Default bit length used when `bits` is zero.
210    ///
211    /// # Returns
212    /// Normalized bit length.
213    ///
214    /// # Errors
215    /// Returns [`IdError::InvalidBitLength`] when the normalized value is 31 or
216    /// greater.
217    fn normalize_bits(name: &'static str, bits: u8, default_bits: u8) -> Result<u8, IdError> {
218        let normalized = if bits == 0 { default_bits } else { bits };
219        if normalized >= 31 {
220            return Err(IdError::InvalidBitLength {
221                name,
222                bits: normalized,
223                reason: "bit length must be less than 31",
224            });
225        }
226        Ok(normalized)
227    }
228
229    /// Returns the number of time bits.
230    ///
231    /// # Returns
232    /// Time bit length.
233    pub const fn bits_time(&self) -> u8 {
234        self.bits_time
235    }
236
237    /// Returns the number of sequence bits.
238    ///
239    /// # Returns
240    /// Sequence bit length.
241    pub const fn bits_sequence(&self) -> u8 {
242        self.bits_sequence
243    }
244
245    /// Returns the number of machine bits.
246    ///
247    /// # Returns
248    /// Machine bit length.
249    pub const fn bits_machine(&self) -> u8 {
250        self.bits_machine
251    }
252
253    /// Returns the maximum representable elapsed time unit.
254    ///
255    /// # Returns
256    /// Maximum elapsed time value.
257    pub const fn max_elapsed_time(&self) -> u64 {
258        (1_u64 << self.bits_time) - 1
259    }
260
261    /// Returns the maximum representable sequence.
262    ///
263    /// # Returns
264    /// Maximum sequence number.
265    pub const fn max_sequence(&self) -> u64 {
266        (1_u64 << self.bits_sequence) - 1
267    }
268
269    /// Returns the maximum representable machine identifier.
270    ///
271    /// # Returns
272    /// Maximum machine identifier.
273    pub const fn max_machine_id(&self) -> u64 {
274        (1_u64 << self.bits_machine) - 1
275    }
276
277    /// Composes a Sonyflake-style ID from explicit parts.
278    ///
279    /// # Parameters
280    /// - `elapsed_time`: Time units elapsed since the start time.
281    /// - `sequence`: Sequence value inside the time unit.
282    /// - `machine_id`: Machine identifier.
283    ///
284    /// # Returns
285    /// Encoded ID.
286    ///
287    /// # Errors
288    /// Returns range errors when any part does not fit the configured layout.
289    pub fn compose(
290        &self,
291        elapsed_time: u64,
292        sequence: u64,
293        machine_id: u64,
294    ) -> Result<u64, IdError> {
295        if elapsed_time > self.max_elapsed_time() {
296            return Err(IdError::TimestampOverflow {
297                timestamp: elapsed_time,
298                max: self.max_elapsed_time(),
299            });
300        }
301        if sequence > self.max_sequence() {
302            return Err(IdError::SequenceOverflow {
303                sequence,
304                max: self.max_sequence(),
305            });
306        }
307        if machine_id > self.max_machine_id() {
308            return Err(IdError::MachineIdOutOfRange {
309                machine_id,
310                max: self.max_machine_id(),
311            });
312        }
313        Ok((elapsed_time << (self.bits_sequence + self.bits_machine))
314            | (sequence << self.bits_machine)
315            | machine_id)
316    }
317
318    /// Extracts elapsed time from a Sonyflake-style ID.
319    ///
320    /// # Parameters
321    /// - `id`: ID generated by this layout.
322    ///
323    /// # Returns
324    /// Elapsed time units since the start time.
325    pub fn extract_elapsed_time(&self, id: u64) -> u64 {
326        id >> (self.bits_sequence + self.bits_machine)
327    }
328
329    /// Extracts sequence from a Sonyflake-style ID.
330    ///
331    /// # Parameters
332    /// - `id`: ID generated by this layout.
333    ///
334    /// # Returns
335    /// Sequence number.
336    pub fn extract_sequence(&self, id: u64) -> u64 {
337        let mask = ((1_u64 << self.bits_sequence) - 1) << self.bits_machine;
338        (id & mask) >> self.bits_machine
339    }
340
341    /// Extracts machine ID from a Sonyflake-style ID.
342    ///
343    /// # Parameters
344    /// - `id`: ID generated by this layout.
345    ///
346    /// # Returns
347    /// Machine identifier.
348    pub fn extract_machine_id(&self, id: u64) -> u64 {
349        id & ((1_u64 << self.bits_machine) - 1)
350    }
351
352    /// Converts a time value into elapsed Sonyflake units.
353    ///
354    /// # Parameters
355    /// - `time`: Time to convert.
356    ///
357    /// # Returns
358    /// Elapsed time units since the start time.
359    ///
360    /// # Errors
361    /// Returns [`IdError::TimeBeforeEpoch`] when `time` is before `start_time`.
362    fn elapsed_time_for(&self, time: SystemTime) -> Result<u64, IdError> {
363        let elapsed = time
364            .duration_since(self.start_time)
365            .map_err(|_| IdError::TimeBeforeEpoch)?;
366        let elapsed_units = elapsed.as_nanos() / self.time_unit.as_nanos();
367        if elapsed_units > u128::from(self.max_elapsed_time()) {
368            return Err(IdError::TimestampOverflow {
369                timestamp: u64::try_from(elapsed_units).unwrap_or(u64::MAX),
370                max: self.max_elapsed_time(),
371            });
372        }
373        Ok(elapsed_units as u64)
374    }
375
376    /// Reads the current elapsed time from the configured clock.
377    ///
378    /// # Returns
379    /// Current elapsed time units.
380    ///
381    /// # Errors
382    /// Returns [`IdError::TimeBeforeEpoch`] when the clock is before start time.
383    fn current_elapsed_time(&self) -> Result<u64, IdError> {
384        self.elapsed_time_for((self.clock)())
385    }
386}
387
388impl IdGenerator<u64> for SonyflakeGenerator {
389    type Error = IdError;
390
391    /// Generates the next Sonyflake-style ID.
392    fn next_id(&self) -> Result<u64, Self::Error> {
393        let mut state = self
394            .state
395            .lock()
396            .expect("generator state mutex should not be poisoned");
397        let current = self.current_elapsed_time()?;
398
399        if state.timestamp < current {
400            state.timestamp = current;
401            state.sequence = 0;
402        } else {
403            state.sequence = (state.sequence + 1) & self.max_sequence();
404            if state.sequence == 0 {
405                state.timestamp += 1;
406                let overtime = state.timestamp.saturating_sub(current);
407                drop(state);
408                thread::sleep(Duration::from_nanos(
409                    (u128::from(overtime) * self.time_unit.as_nanos()) as u64,
410                ));
411                state = self
412                    .state
413                    .lock()
414                    .expect("generator state mutex should not be poisoned");
415            }
416        }
417
418        self.compose(state.timestamp, state.sequence, self.machine_id)
419    }
420}