ferroid 0.2.0

A flexible ID generator for producing unique, monotonic, and lexicographically sortable Snowflake-style IDs.
Documentation

ferroid

ferroid is a Rust crate for generating and parsing Snowflake-style unique IDs.

It supports pre-built layouts for platforms like Twitter, Discord, Instagram, and Mastodon. These IDs are 64-bit integers that encode timestamps, machine/shard IDs, and sequence numbers - making them lexicographically sortable, scalable, and ideal for distributed systems.

Features:

  • ๐Ÿ“Œ Bit-level layout compatibility with major Snowflake formats
  • ๐Ÿงฉ Pluggable time sources via the TimeSource trait
  • ๐Ÿงต Lock-based and lock-free thread-safe ID generation
  • ๐Ÿ“ Customizable layouts via the Snowflake trait
  • ๐Ÿ”ข Lexicographically sortable string encoding

๐Ÿ“ฆ Supported Layouts

Platform Timestamp Bits Machine ID Bits Sequence Bits Epoch
Twitter 41 10 12 2010-11-04 01:42:54.657
Discord 42 10 12 2015-01-01 00:00:00.000
Instagram 41 13 10 2011-01-01 00:00:00.000
Mastodon 48 0 16 1970-01-01 00:00:00.000

๐Ÿ”ง Generator Comparison

Generator Thread-Safe Lock-Free Throughput Use Case
BasicSnowflakeGenerator โŒ โŒ Highest Single-threaded, zero contention; ideal for sharded/core-local generators
LockSnowflakeGenerator โœ… โŒ Medium Multi-threaded workloads where fair access across threads is important
AtomicSnowflakeGenerator โœ… โœ… High Multi-threaded workloads where fair access is sacrificed for higher throughput

All generators produce monotonically increasing, time-ordered, and unique IDs.

๐Ÿš€ Usage

Generate an ID

Synchronous

Calling next_id() may yield Pending if the current sequence is exhausted. In that case, you can spin, yield, or sleep depending on your environment:

use ferroid::{MonotonicClock, TWITTER_EPOCH, BasicSnowflakeGenerator, SnowflakeTwitterId, IdGenStatus};

let clock = MonotonicClock::with_epoch(TWITTER_EPOCH);
let generator = BasicSnowflakeGenerator::new(0, clock);

let id: SnowflakeTwitterId = loop {
    match generator.next_id() {
        IdGenStatus::Ready { id } => break id,
        IdGenStatus::Pending { yield_for } => {
            println!("Exhausted; wait for: {}ms", yield_for);
            std::hint::spin_loop();
            // Use `std::hint::spin_loop()` for single-threaded or per-thread generators.
            // Use `std::thread::yield_now()` when sharing a generator across multiple threads.
            // Use `std::thread::sleep(Duration::from_millis(yield_for.to_u64().unwrap())` to sleep.
        }
    }
};

println!("Generated ID: {}", id);

Asynchronous

If you're in an async context (e.g., using Tokio), you can use the async-tokio feature and import the SnowflakeGeneratorAsyncExt trait to await a new ID:

use ferroid::{
    MonotonicClock, Result, MASTODON_EPOCH, AtomicSnowflakeGenerator, SnowflakeMastodonId,
    SnowflakeGeneratorAsyncExt, TokioSleep,
};

#[tokio::main]
async fn main() -> Result<()> {
    let clock = MonotonicClock::with_epoch(MASTODON_EPOCH);
    let generator =  AtomicSnowflakeGenerator::<SnowflakeMastodonId, _>::new(0, clock);

    // Generate a non-blocking ID that sleeps if the generator isn't ready.
    let id = generator.try_next_id_async::<TokioSleep>().await?;
    println!("Generated ID: {}", id);

    Ok(())
}

Custom Layouts

To define a custom Snowflake layout, implement Snowflake:

use core::fmt;
use ferroid::Snowflake;

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyCustomId {
    id: u64,
}

impl Snowflake for MyCustomId {
    // ...
}

impl fmt::Display for MyCustomId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.id)
    }
}

Behavior

  • If the clock advances: reset sequence to 0 โ†’ IdGenStatus::Ready
  • If the clock is unchanged: increment sequence โ†’ IdGenStatus::Ready
  • If the clock goes backward: return IdGenStatus::Pending
  • If the sequence overflows: return IdGenStatus::Pending

Serialize as padded string

Use .to_padded_string() or .encode() (enabled with base32 feature) for sortable representations:

use ferroid::{SnowflakeTwitterId, SnowflakeBase32Ext};

let id = SnowflakeTwitterId::from(123456, 1, 42);
println!("default: {id}");
// > default: 517811998762

println!("padded: {}", id.to_padded_string());
// > padded: 00000000517811998762

let encoded = id.encode();
println!("base32: {encoded}");
// > base32: 00000Y4G0082M

let decoded = SnowflakeTwitterId::decode(&encoded).expect("decode should succeed");
assert_eq!(id, decoded);

๐Ÿ“ˆ Benchmarks

ferroid ships with Criterion benchmarks to measure ID generation performance:

  • BasicSnowflakeGenerator: single-threaded generator
  • LockSnowflakeGenerator: mutex-based, thread-safe generator
  • AtomicSnowflakeGenerator: lock-free, thread-safe generator

Benchmark scenarios include:

  • Single-threaded with/without a real clock
  • Multi-threaded with/without a real clock

NOTE: Shared generators (like LockSnowflakeGenerator and AtomicSnowflakeGenerator) can slow down under high thread contention. This happens because threads must coordinate access - either through mutex locks or atomic compare-and-swap (CAS) loops - which introduces overhead.

For maximum throughput, avoid sharing. Instead, give each thread its own generator instance. This eliminates contention and allows every thread to issue IDs independently at full speed.

The thread-safe generators are primarily for convenience, or for use cases where ID generation is not expected to be the performance bottleneck.

To run:

cargo criterion --all-features

๐Ÿงช Testing

Run all tests with:

cargo test --all-features

๐Ÿ“„ License

Licensed under either of:

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.