Skip to main content

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}