# ferroid
[`ferroid`](https://github.com/s0l0ist/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
| 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
| `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:
```rust
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](https://tokio.rs/)), you can
use the `async-tokio` feature and import the `SnowflakeGeneratorAsyncExt` trait
to await a new ID:
```rust
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`:
```rust
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:
```rust
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:
```sh
cargo criterion --all-features
```
## ๐งช Testing
Run all tests with:
```sh
cargo test --all-features
```
## ๐ License
Licensed under either of:
- [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
([LICENSE-APACHE](LICENSE-APACHE))
- [MIT License](https://opensource.org/licenses/MIT)
([LICENSE-MIT](LICENSE-MIT))
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.