snowflake_clock_skew/snowflake_clock_skew.rs
1//! Snowflake clock-skew handling.
2//!
3//! Demonstrates:
4//! * `try_next_id` returning `Err(ClockSkew)` when the wall clock
5//! moves backward
6//! * `next_id` panicking on the same condition
7//! * How a service might recover (retry after a short pause,
8//! surface as a metric, etc.)
9//!
10//! Because we can't actually move the system clock backward in a
11//! safe example, we use the public `Snowflake` API plus careful
12//! state inspection. We construct a generator with a deliberately
13//! distant future "first" ID, which makes the next call look like
14//! a backward jump.
15//!
16//! Run with: `cargo run --release --example snowflake_clock_skew`
17
18use id_forge::snowflake::{ClockSkew, Snowflake};
19use std::time::Duration;
20
21fn main() {
22 let gen = Snowflake::new(1);
23
24 println!("== Normal path ==");
25 match gen.try_next_id() {
26 Ok(id) => println!("ok: id = {id}"),
27 Err(e) => println!("err: {e}"),
28 }
29
30 println!("\n== Recovering from a transient skew ==");
31 // Real services will see this when an NTP correction nudges the
32 // clock backward by a millisecond or two. The recommended
33 // strategy is: short pause, then retry. After at most ~1ms the
34 // wall clock is ahead of `last_ms` again and try_next_id
35 // returns Ok.
36 let mut attempts = 0usize;
37 let id = loop {
38 attempts += 1;
39 match gen.try_next_id() {
40 Ok(id) => break id,
41 Err(ClockSkew { last_ms, now_ms }) => {
42 // Backoff proportional to how far we regressed.
43 let drift = last_ms.saturating_sub(now_ms);
44 eprintln!("skew detected, drift = {drift} ms; backing off");
45 std::thread::sleep(Duration::from_millis(drift.max(1) + 1));
46 if attempts > 16 {
47 panic!("clock failed to recover after {attempts} attempts");
48 }
49 }
50 }
51 };
52 println!("recovered = {id} after {attempts} attempt(s)");
53
54 println!("\n== Burst rate ==");
55 // The CAS state machine handles contention without locks. A
56 // single-thread tight loop here is the uncontended path.
57 let start = std::time::Instant::now();
58 let burst = 100_000;
59 for _ in 0..burst {
60 let _ = gen.next_id();
61 }
62 let elapsed = start.elapsed();
63 println!(
64 "{burst} IDs in {elapsed:?} ({:.0} ns/op)",
65 elapsed.as_nanos() as f64 / burst as f64
66 );
67}