<p align="center">
<b>π¬π§ <a href="README.md">English</a></b>
|
<b>π·πΊ <a href="README.ru.md">Π ΡΡΡΠΊΠΈΠΉ</a></b>
</p>
---
# beakid
[](https://crates.io/crates/beakid)
[](LICENSE)
Lock-free Snowflake-like 64-bit unique ID generator. Produces `i64` values compatible with PostgreSQL `BIGINT` columns. Built on **tokio** β `next_id()` is an async function that may call `tokio::time::sleep` when the clock drifts.
```toml
[dependencies]
beakid = "0.3"
```
## 1. Tokio dependency
The generator **requires tokio**. The public API is fully `async`:
| `next_id` / `next_id_with_timestamp` | both are `async fn` |
| Time-drift sleep | uses `tokio::time::sleep` |
| Runtime | caller provides the tokio runtime |
Add tokio to your project:
```toml
[dependencies]
tokio = { version = "1", features = ["time"] }
beakid = "0.3"
```
For tests you'll also need `rt` + `macros`:
```toml
[dev-dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time"] }
```
## 2. Usage
```rust
use std::time::{Duration, SystemTime};
use beakid::BeakIdGenerator;
#[tokio::main]
async fn main() {
let generator = BeakIdGenerator::new(
1, // worker_id (0..1023)
SystemTime::UNIX_EPOCH, // custom epoch
Duration::from_secs(1), // clock-drift window
);
let id = generator.next_id().await;
println!("{id}"); // 13510798882111488
println!("{:?}", id); // BeakId(13510798882111488)
println!("{}", id.raw()); // 13510798882111488
}
```
### Reusing a timestamp
If you already have a timestamp for another task, pass it to avoid an extra syscall:
```rust
let now_ms = /* timestamp obtained elsewhere */;
let id = generator.next_id_with_timestamp(now_ms).await;
```
The `now` parameter is **milliseconds since the generator's custom epoch**:
```rust
use std::time::{SystemTime, UNIX_EPOCH};
fn relative_now_ms(epoch: SystemTime) -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64
- epoch.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64
}
```
### Multi-instance setup
Use a different `worker_id` per process / pod:
```rust
let gen_a = BeakIdGenerator::new(1, epoch, window);
let gen_b = BeakIdGenerator::new(2, epoch, window);
let gen_c = BeakIdGenerator::new(3, epoch, window);
```
IDs from different generators are unique thanks to distinct `worker_id` bits, but they are **not strictly ordered across generators** β only within a single generator.
### Reading fields from an ID
```rust
let id = generator.next_id().await;
id.timestamp(); // 41-bit timestamp (ms since epoch)
id.sequence(); // 12-bit monotonic counter
id.worker_id(); // 10-bit instance identifier
id.raw(); // raw i64
```
## 3. ID structure
A 64-bit signed integer (`i64`), always non-negative:
```
ββββββββββββ¬βββββββββββββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ
β reserved β timestamp β sequence β worker_id β
β 1 bit β 41 bits β 12 bits β 10 bits β
ββββββββββββ΄βββββββββββββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββ
bit 63 bit 62 bit 22 bit 21 bit 10 bit 9 bit 0
```
| `reserved` | 1 (bit 63) | always `0` | Keeps IDs in the positive `i64` range |
| `timestamp` | 41 (bits 22β62) | 0 β¦ 2β΄ΒΉβ1 | Milliseconds since custom `epoch`. Covers ~69 years |
| `sequence` | 12 (bits 10β21) | 0 β¦ 4095 | Monotonic counter per millisecond |
| `worker_id` | 10 (bits 0β9) | 0 β¦ 1023 | Instance / pod identifier |
The `reserved` bit ensures all generated IDs satisfy `0 <= id < 2^63`, mapping cleanly to PostgreSQL `BIGINT`.
## 4. Generator internals
### Lock-free algorithm
The generator uses a single `AtomicI64` β no `Mutex`, no `RwLock`. The core operation is one `fetch_add(1, Relaxed)`:
```text
βββββββββββββββββββββ
β fetch_add(1) β β atomic, lock-free
β β old value = ID β
ββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββ
β extract timestampβ β non-atomic check
β from old value β
ββββββββββ¬βββββββββββ
β
βββββββββββββββ΄ββββββββββββββ
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ
β ts >= now ? β β now > ts ? β
β diff < window ? β β (clock lagging) β
ββββββ¬βββββββββ¬βββββ ββββββββββ¬βββββββββ
β β β
β OK β sleep β advance
β βΌ β βΌ
β βββββββββββ β βββββββββββββββββββββββ
β β return β β β CAS loop: β
β β old β β β set base = (now << 22) β
β βββββββββββ β β | (1 << 10) | worker β
β β βββββββββββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββ
β diff >= window β sleep(diff - window) β
β then return (no re-check) β
βββββββββββββββββββββββββββββββββββββββ
```
### Three states
| **In window** | `0 <= ts - now < window_size` | Return ID immediately |
| **Ahead** | `ts - now >= window_size` | Sleep until the real clock catches up, then return |
| **Behind** | `now > ts` | CAS-advance the atomic to the current timestamp. Another thread may have already done this β CAS loop handles contention |
### Key properties
- **Monotonic within a generator**: IDs always increase within one `BeakIdGenerator`.
- **Not ordered across generators**: two instances with different `worker_id` values may produce interleaved IDs.
- **Gaps are normal**: the atomic counter jumps forward on `advance`, skipping unused values. This is by design β uniqueness is guaranteed, density is not.
- **Thread-safe & lock-free**: multiple `tokio` tasks can call `next_id` concurrently on the same `&BeakIdGenerator`.
- **PostgreSQL-ready**: IDs are non-negative `i64` values that fit directly into `BIGINT` columns.
### Clock drift protection
When a generated ID carries a timestamp **ahead** of the real clock (e.g. after an NTP correction, or due to a burst that consumed future timestamps), the generator **sleeps** using `tokio::time::sleep` until the real time catches up. The `window_size` parameter controls how much drift is tolerated before sleeping. A typical value is 1β5 seconds.