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}