Skip to main content

id_forge/
snowflake.rs

1//! # Snowflake ID generation
2//!
3//! 64-bit IDs in the Twitter Snowflake layout: a 41-bit millisecond
4//! timestamp offset from a configurable epoch, a 10-bit worker ID,
5//! and a 12-bit per-millisecond sequence.
6//!
7//! ```text
8//!  63                   22         12         0
9//!  +---------------------+----------+----------+
10//!  | 41 bits ms offset   | 10 bits  | 12 bits  |
11//!  | (since epoch_ms)    | worker   | sequence |
12//!  +---------------------+----------+----------+
13//! ```
14//!
15//! IDs are strictly monotonic within a single worker. When 4096 IDs
16//! are minted in the same millisecond the generator blocks (microsleep
17//! loop) until the wall clock advances. When the wall clock moves
18//! backward, [`Snowflake::try_next_id`] returns
19//! `Err(`[`ClockSkew`]`)` and [`Snowflake::next_id`] panics — the spec
20//! forbids issuing IDs whose timestamps could collide with previously
21//! issued ones.
22//!
23//! ```
24//! use id_forge::snowflake::Snowflake;
25//!
26//! let gen = Snowflake::new(1);
27//! let a = gen.next_id();
28//! let b = gen.next_id();
29//! assert!(b > a);
30//! let (ts_offset, worker, seq) = Snowflake::parts(b);
31//! assert_eq!(worker, 1);
32//! assert!(ts_offset > 0);
33//! assert!(seq <= 4095);
34//! ```
35
36use core::fmt;
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::time::{Duration, SystemTime, UNIX_EPOCH};
39
40/// Default epoch (2026-01-01T00:00:00Z in milliseconds since UNIX epoch).
41pub const DEFAULT_EPOCH_MS: u64 = 1_767_225_600_000;
42
43/// Number of bits assigned to the sequence field in an ID.
44pub const SEQUENCE_BITS: u32 = 12;
45/// Number of bits assigned to the worker field in an ID.
46pub const WORKER_BITS: u32 = 10;
47/// Number of bits assigned to the timestamp offset in an ID.
48pub const TIMESTAMP_BITS: u32 = 41;
49
50const SEQUENCE_MASK: u64 = (1 << SEQUENCE_BITS) - 1;
51const WORKER_MASK: u64 = (1 << WORKER_BITS) - 1;
52const TIMESTAMP_MASK: u64 = (1 << TIMESTAMP_BITS) - 1;
53
54const WORKER_SHIFT: u32 = SEQUENCE_BITS;
55const TIMESTAMP_SHIFT: u32 = SEQUENCE_BITS + WORKER_BITS;
56
57// Packed state layout: bits 0..13 hold the next sequence to assign
58// (0..=4096; the 4096 sentinel means "this millisecond is exhausted"),
59// bits 13..54 hold the last-seen millisecond offset (41 bits).
60const STATE_SEQ_BITS: u32 = 13;
61const STATE_SEQ_MASK: u64 = (1 << STATE_SEQ_BITS) - 1;
62const STATE_SEQ_EXHAUSTED: u64 = SEQUENCE_MASK + 1; // 4096
63
64/// Snowflake ID generator.
65///
66/// Holds the worker ID, the epoch, and the packed `(last_ms, next_seq)`
67/// state used to mint monotonic IDs lock-free.
68///
69/// # Example
70///
71/// ```
72/// use id_forge::snowflake::Snowflake;
73///
74/// let gen = Snowflake::new(7);
75/// let id = gen.next_id();
76/// assert_eq!(Snowflake::parts(id).1, 7);
77/// ```
78#[derive(Debug)]
79pub struct Snowflake {
80    worker_id: u16,
81    epoch_ms: u64,
82    state: AtomicU64,
83}
84
85impl Snowflake {
86    /// Build a new generator with the given worker ID (0-1023) and
87    /// the default 2026-01-01 epoch.
88    ///
89    /// Worker IDs above 1023 are silently clamped to 10 bits.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use id_forge::snowflake::Snowflake;
95    ///
96    /// let gen = Snowflake::new(42);
97    /// assert_eq!(gen.worker_id(), 42);
98    /// ```
99    pub const fn new(worker_id: u16) -> Self {
100        Self::with_epoch(worker_id, DEFAULT_EPOCH_MS)
101    }
102
103    /// Build a new generator with a custom epoch (milliseconds since
104    /// the UNIX epoch).
105    ///
106    /// # Example
107    ///
108    /// ```
109    /// use id_forge::snowflake::Snowflake;
110    ///
111    /// // Twitter's original Snowflake epoch.
112    /// let gen = Snowflake::with_epoch(1, 1_288_834_974_657);
113    /// assert_eq!(gen.epoch_ms(), 1_288_834_974_657);
114    /// ```
115    pub const fn with_epoch(worker_id: u16, epoch_ms: u64) -> Self {
116        Self {
117            worker_id: (worker_id as u64 & WORKER_MASK) as u16,
118            epoch_ms,
119            state: AtomicU64::new(0),
120        }
121    }
122
123    /// The worker ID this generator was built with, clamped to 10 bits.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use id_forge::snowflake::Snowflake;
129    ///
130    /// assert_eq!(Snowflake::new(42).worker_id(), 42);
131    /// assert_eq!(Snowflake::new(0xffff).worker_id(), 0x3ff);  // clamped
132    /// ```
133    pub const fn worker_id(&self) -> u16 {
134        self.worker_id
135    }
136
137    /// The epoch this generator subtracts from the wall clock.
138    ///
139    /// # Example
140    ///
141    /// ```
142    /// use id_forge::snowflake::{Snowflake, DEFAULT_EPOCH_MS};
143    ///
144    /// assert_eq!(Snowflake::new(1).epoch_ms(), DEFAULT_EPOCH_MS);
145    /// ```
146    pub const fn epoch_ms(&self) -> u64 {
147        self.epoch_ms
148    }
149
150    /// Generate the next ID, returning `Err` if the wall clock has
151    /// moved backward since the previous call.
152    ///
153    /// When 4096 IDs have been issued in the same millisecond, this
154    /// method blocks in a microsecond sleep loop until the wall clock
155    /// advances. It does **not** spin on a busy loop.
156    ///
157    /// # Example
158    ///
159    /// ```
160    /// use id_forge::snowflake::Snowflake;
161    ///
162    /// let gen = Snowflake::new(1);
163    /// let id = gen.try_next_id().expect("clock should not run backward");
164    /// assert!(id > 0);
165    /// ```
166    pub fn try_next_id(&self) -> Result<u64, ClockSkew> {
167        loop {
168            let cur = self.state.load(Ordering::Acquire);
169            let last_ms = cur >> STATE_SEQ_BITS;
170            let next_seq = cur & STATE_SEQ_MASK;
171
172            let now = current_offset_ms(self.epoch_ms);
173            if now < last_ms {
174                return Err(ClockSkew {
175                    last_ms,
176                    now_ms: now,
177                });
178            }
179
180            let (use_ms, assigned, new_next_seq) = if now == last_ms {
181                if next_seq >= STATE_SEQ_EXHAUSTED {
182                    sleep_until_after(self.epoch_ms, last_ms);
183                    continue;
184                }
185                (last_ms, next_seq, next_seq + 1)
186            } else {
187                (now, 0u64, 1u64)
188            };
189
190            let new_state = (use_ms << STATE_SEQ_BITS) | new_next_seq;
191            if self
192                .state
193                .compare_exchange(cur, new_state, Ordering::AcqRel, Ordering::Acquire)
194                .is_ok()
195            {
196                let id = (use_ms << TIMESTAMP_SHIFT)
197                    | ((self.worker_id as u64) << WORKER_SHIFT)
198                    | assigned;
199                return Ok(id);
200            }
201        }
202    }
203
204    /// Generate the next ID. Panics if the wall clock has moved
205    /// backward since the previous call.
206    ///
207    /// Use [`Snowflake::try_next_id`] when callers need to recover
208    /// from clock skew (e.g. to surface it as a service-level error
209    /// rather than crash the process).
210    ///
211    /// # Panics
212    ///
213    /// Panics with a [`ClockSkew`] description if the wall clock has
214    /// regressed below the most recently issued ID's timestamp.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// use id_forge::snowflake::Snowflake;
220    ///
221    /// let gen = Snowflake::new(1);
222    /// let id = gen.next_id();
223    /// assert!(id > 0);
224    /// ```
225    pub fn next_id(&self) -> u64 {
226        match self.try_next_id() {
227            Ok(id) => id,
228            Err(e) => panic!("snowflake: clock moved backward ({e})"),
229        }
230    }
231
232    /// Decompose an ID minted by any Snowflake generator into its
233    /// `(timestamp_offset_ms, worker_id, sequence)` parts.
234    ///
235    /// The first element is the millisecond offset from whatever epoch
236    /// the originating generator was built with. To recover the
237    /// wall-clock millisecond, add the generator's [`epoch_ms`](Self::epoch_ms).
238    ///
239    /// # Example
240    ///
241    /// ```
242    /// use id_forge::snowflake::Snowflake;
243    ///
244    /// let gen = Snowflake::new(7);
245    /// let id = gen.next_id();
246    /// let (ts_offset, worker, seq) = Snowflake::parts(id);
247    /// assert_eq!(worker, 7);
248    /// assert!(seq <= 4095);
249    /// let wall_ms = ts_offset + gen.epoch_ms();
250    /// assert!(wall_ms > gen.epoch_ms());
251    /// ```
252    pub const fn parts(id: u64) -> (u64, u16, u16) {
253        let timestamp_offset = (id >> TIMESTAMP_SHIFT) & TIMESTAMP_MASK;
254        let worker = ((id >> WORKER_SHIFT) & WORKER_MASK) as u16;
255        let sequence = (id & SEQUENCE_MASK) as u16;
256        (timestamp_offset, worker, sequence)
257    }
258}
259
260/// Error returned by [`Snowflake::try_next_id`] when the system clock
261/// has moved backward since the most recent ID was issued.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct ClockSkew {
264    /// The most recent millisecond offset (since the epoch) at which
265    /// the generator successfully issued an ID.
266    pub last_ms: u64,
267    /// The current millisecond offset reported by the wall clock,
268    /// which is strictly less than `last_ms`.
269    pub now_ms: u64,
270}
271
272impl fmt::Display for ClockSkew {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        write!(
275            f,
276            "clock moved backward: last issued at offset {} ms, now at offset {} ms",
277            self.last_ms, self.now_ms
278        )
279    }
280}
281
282impl std::error::Error for ClockSkew {}
283
284fn current_offset_ms(epoch_ms: u64) -> u64 {
285    let now = SystemTime::now()
286        .duration_since(UNIX_EPOCH)
287        .map(|d| d.as_millis() as u64)
288        .unwrap_or(0);
289    now.saturating_sub(epoch_ms) & TIMESTAMP_MASK
290}
291
292fn sleep_until_after(epoch_ms: u64, last_ms: u64) {
293    loop {
294        if current_offset_ms(epoch_ms) > last_ms {
295            return;
296        }
297        std::thread::sleep(Duration::from_micros(100));
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::collections::HashSet;
305    use std::sync::atomic::Ordering;
306    use std::sync::Arc;
307    use std::thread;
308
309    #[test]
310    fn next_id_produces_value() {
311        let gen = Snowflake::new(1);
312        assert!(gen.next_id() > 0);
313    }
314
315    #[test]
316    fn worker_id_clamped() {
317        let gen = Snowflake::new(0xffff);
318        assert_eq!(gen.worker_id(), 0x3ff);
319        let id = gen.next_id();
320        assert_eq!(Snowflake::parts(id).1, 0x3ff);
321    }
322
323    #[test]
324    fn worker_field_extracts() {
325        let gen = Snowflake::new(42);
326        let id = gen.next_id();
327        let (_, worker, _) = Snowflake::parts(id);
328        assert_eq!(worker, 42);
329    }
330
331    #[test]
332    fn monotonic_in_burst() {
333        let gen = Snowflake::new(1);
334        let mut prev = gen.next_id();
335        for _ in 0..10_000 {
336            let cur = gen.next_id();
337            assert!(cur > prev, "expected {cur} > {prev}");
338            prev = cur;
339        }
340    }
341
342    #[test]
343    fn all_unique_in_burst() {
344        let gen = Snowflake::new(1);
345        let mut set = HashSet::new();
346        for _ in 0..50_000 {
347            let id = gen.next_id();
348            assert!(set.insert(id));
349        }
350    }
351
352    #[test]
353    fn parts_round_trip() {
354        let gen = Snowflake::with_epoch(7, DEFAULT_EPOCH_MS);
355        let id = gen.next_id();
356        let (ts, worker, seq) = Snowflake::parts(id);
357        assert_eq!(worker, 7);
358        let reassembled = (ts << TIMESTAMP_SHIFT) | ((worker as u64) << WORKER_SHIFT) | seq as u64;
359        assert_eq!(reassembled, id);
360    }
361
362    #[test]
363    fn sequence_resets_each_ms() {
364        let gen = Snowflake::new(1);
365        let _ = gen.next_id();
366        thread::sleep(Duration::from_millis(3));
367        let id_after_sleep = gen.next_id();
368        let (_, _, seq) = Snowflake::parts(id_after_sleep);
369        assert_eq!(seq, 0, "first ID of a fresh ms must have sequence 0");
370    }
371
372    #[test]
373    fn sequence_exhaustion_blocks_until_next_ms() {
374        // Pre-load state to simulate an exhausted millisecond.
375        let gen = Snowflake::new(1);
376        let now = current_offset_ms(gen.epoch_ms);
377        let exhausted_state = (now << STATE_SEQ_BITS) | STATE_SEQ_EXHAUSTED;
378        gen.state.store(exhausted_state, Ordering::Release);
379
380        let start = SystemTime::now();
381        let id = gen.next_id();
382        let elapsed = SystemTime::now().duration_since(start).unwrap();
383
384        let (ts, _, seq) = Snowflake::parts(id);
385        assert!(ts > now, "new ID must be in a later millisecond");
386        assert_eq!(seq, 0);
387        assert!(
388            elapsed < Duration::from_millis(50),
389            "block should be roughly one ms, got {elapsed:?}"
390        );
391    }
392
393    #[test]
394    fn clock_skew_reported_via_result() {
395        let gen = Snowflake::new(1);
396        // Force the state to claim we've already issued an ID
397        // far in the future relative to the wall clock.
398        let future_ms = current_offset_ms(gen.epoch_ms) + 5_000;
399        gen.state
400            .store(future_ms << STATE_SEQ_BITS, Ordering::Release);
401
402        match gen.try_next_id() {
403            Err(ClockSkew { last_ms, now_ms }) => {
404                assert_eq!(last_ms, future_ms);
405                assert!(now_ms < last_ms);
406            }
407            Ok(id) => panic!("expected ClockSkew, got id {id}"),
408        }
409    }
410
411    #[test]
412    #[should_panic(expected = "clock moved backward")]
413    fn next_id_panics_on_clock_skew() {
414        let gen = Snowflake::new(1);
415        let future_ms = current_offset_ms(gen.epoch_ms) + 5_000;
416        gen.state
417            .store(future_ms << STATE_SEQ_BITS, Ordering::Release);
418        let _ = gen.next_id();
419    }
420
421    #[test]
422    fn multi_thread_all_unique() {
423        let gen = Arc::new(Snowflake::new(3));
424        let mut handles = Vec::new();
425        for _ in 0..8 {
426            let g = Arc::clone(&gen);
427            handles.push(thread::spawn(move || {
428                let mut local = Vec::with_capacity(2000);
429                for _ in 0..2000 {
430                    local.push(g.next_id());
431                }
432                local
433            }));
434        }
435        let mut all = HashSet::new();
436        for h in handles {
437            for id in h.join().unwrap() {
438                assert!(all.insert(id), "duplicate id under thread contention");
439            }
440        }
441        assert_eq!(all.len(), 8 * 2000);
442    }
443
444    #[test]
445    fn custom_epoch_round_trip() {
446        let epoch = 1_700_000_000_000_u64;
447        let gen = Snowflake::with_epoch(9, epoch);
448        let id = gen.next_id();
449        let (ts_offset, worker, _) = Snowflake::parts(id);
450        assert_eq!(worker, 9);
451        assert_eq!(gen.epoch_ms(), epoch);
452        let wall = ts_offset + epoch;
453        assert!(wall > epoch);
454    }
455
456    #[test]
457    fn parts_extracts_each_field() {
458        // Construct an ID with known fields and decompose it.
459        let ts: u64 = 12_345;
460        let worker: u64 = 700;
461        let seq: u64 = 4000;
462        let id = (ts << TIMESTAMP_SHIFT) | (worker << WORKER_SHIFT) | seq;
463        let (got_ts, got_w, got_s) = Snowflake::parts(id);
464        assert_eq!(got_ts, ts);
465        assert_eq!(got_w as u64, worker);
466        assert_eq!(got_s as u64, seq);
467    }
468}