Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: [kkrypt0nn]
custom: ["https://buymeacoffee.com/kkrypt0nn"]
37 changes: 32 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
name: spaceflake.rs CI (Lint & Test)
name: Spaceflake Rust CI (Lint & Test)

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
lint-test:
name: Lint & Test
rustfmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Run rustfmt
run: cargo fmt --all -- --check --verbose
clippy:
name: clippy
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Run Clippy
run: cargo clippy --all-targets --all-features
test:
name: Test
runs-on: ${{ matrix.os }}
needs: [rustfmt, clippy]
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
toolchain:
- "stable"
- "nightly"
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install nightly toolchain
if: matrix.toolchain == 'nightly'
run: rustup toolchain install nightly
- name: Build
run: cargo build --all --no-default-features --all-features
run: cargo +${{ matrix.toolchain }} build --all --no-default-features --all-features
- name: Test
run: cargo test --all --no-default-features --all-features
run: cargo +${{ matrix.toolchain }} test --all --no-default-features --all-features
9 changes: 3 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
name: spaceflake.rs CD (Publish)
name: Spaceflake Rust CD (Publish)

on:
push:
tags:
- "*"
workflow_dispatch:

permissions:
contents: read

jobs:
test:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Publish
run: cargo publish --verbose --all-features --token ${{ secrets.CARGO_TOKEN }}
run: cargo publish --verbose --all-features --token ${{ secrets.CARGO_TOKEN }}
21 changes: 3 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,11 @@ important side, this includes:

Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:

1. Fork the repo and create your branch from `main`.
1. Fork the repo and create your branch from `main`
2. Keep consistency with the current state of the codebase
3. Format the code of the files properly.
3. Format the code of the files properly
4. Issue that pull request!

## Commit messages guidelines

This project uses [`Conventional Commits 1.0.0`](https://conventionalcommits.org/en/v1.0.0/) hence your commit messages
**must** follow the same convention or your contributions will be ignored, refused or assigned to another user or
maintainer.

It would be more than welcome to keep your contributions as a single commit rather than, for examples, 20 `"fix: Stuff"`
commits in-between. You may use multiple commits if you believe the changes made in these commits have nothing, or close
to nothing, in common - feel free to ask a maintainer on whether it should be a single commit or not.

## Create a GitHub Issue and **then** a pull request

Start contributing by first opening a new issue. Once that is done, you can create a pull request for the issue if you
already have a fix for it.

## License

Your submissions are understood to be under the same [MIT License](LICENSE.md) that covers the project.
Your submissions are understood to be under the same [MIT License](./LICENSE.md) that covers the project.
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[package]
name = "spaceflake"
description = "⛄ A distributed generator to create unique IDs with ease in Rust; inspired by Twitter's Snowflake"
version = "1.1.2"
edition = "2021"
version = "1.2.0"
edition = "2024"
license-file = "LICENSE.md"
readme = "README.md"

[dependencies]
rand = "0.8.5"
rand = "0.9.1"
34 changes: 27 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
#![allow(clippy::needless_doctest_main)]

use rand::Rng;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{fmt, thread};

use rand::Rng;

/// The default epoch used **with milliseconds**, which is the 1st of January 2015 at 12:00:00 AM GMT.
pub const EPOCH: u64 = 1420070400000;

Expand All @@ -16,6 +15,9 @@ const MAX_5_BITS: u64 = 31;
/// The maximum number that can be set with 12 bits.
const MAX_12_BITS: u64 = 4095;

/// The maximum amount of milliseconds for clock drift tolerance.
const CLOCK_DRIFT_TOLERANCE_MS: u64 = 10;

/// A Spaceflake is the internal name for a Snowflake ID.
///
/// Apart from being a crystal of snow, a snowflake is a form of unique identifier which is being used in distributed computing. It has specific parts and is 64 bits long in binary.
Expand Down Expand Up @@ -238,6 +240,8 @@ pub struct Worker {
pub sequence: u64,
/// The incremented number of the worker, used for the sequence.
increment: Arc<Mutex<u64>>,
/// The timestamp of the most recently generated Spaceflake, used to prevent clock drifting.
last_timestamp: u64,
}

/// The default implementation of a worker.
Expand All @@ -253,6 +257,7 @@ impl Worker {
node_id,
sequence: 0,
increment: Arc::new(Mutex::new(0)),
last_timestamp: 0,
}
}

Expand Down Expand Up @@ -394,7 +399,7 @@ impl Default for GeneratorSettings {
pub fn generate(settings: GeneratorSettings) -> Result<Spaceflake, String> {
let mut worker = Worker::new(settings.worker_id, settings.node_id);
if settings.sequence == 0 {
worker.sequence = rand::thread_rng().gen_range(1..=MAX_12_BITS);
worker.sequence = rand::rng().random_range(1..=MAX_12_BITS);
} else {
worker.sequence = settings.sequence;
}
Expand All @@ -407,7 +412,7 @@ pub fn generate(settings: GeneratorSettings) -> Result<Spaceflake, String> {
pub fn generate_at(settings: GeneratorSettings, at: u64) -> Result<Spaceflake, String> {
let mut worker = Worker::new(settings.worker_id, settings.node_id);
if settings.sequence == 0 {
worker.sequence = rand::thread_rng().gen_range(1..=MAX_12_BITS);
worker.sequence = rand::rng().random_range(1..=MAX_12_BITS);
} else {
worker.sequence = settings.sequence;
}
Expand Down Expand Up @@ -447,7 +452,7 @@ pub fn decompose_binary(spaceflake_id: u64, base_epoch: u64) -> HashMap<String,
/// Generates a Spaceflake for a given worker and node ID.
fn generate_on_node_and_worker(
node_id: u64,
worker: Worker,
mut worker: Worker,
at: Option<u64>,
) -> Result<Spaceflake, String> {
let now = SystemTime::now()
Expand Down Expand Up @@ -479,14 +484,29 @@ fn generate_on_node_and_worker(
));
}

let mut milliseconds = generate_at - worker.base_epoch;

if milliseconds < worker.last_timestamp {
let delta = worker.last_timestamp - milliseconds;
if delta >= CLOCK_DRIFT_TOLERANCE_MS {
return Err(format!("clock moved backwards by {}ms", delta));
}
thread::sleep(Duration::from_millis(delta + 1));

let now_after_sleep = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards?")
.as_millis() as u64;
milliseconds = now_after_sleep - worker.base_epoch;
}
worker.last_timestamp = milliseconds;

let mut increment = worker.increment.lock().unwrap();
if *increment >= MAX_12_BITS {
*increment = 0
}
*increment += 1;

let milliseconds = generate_at - worker.base_epoch;

let base = pad_left(decimal_binary(milliseconds), 41);
let node_id = pad_left(decimal_binary(node_id), 5);
let worker_id = pad_left(decimal_binary(worker.id), 5);
Expand Down
15 changes: 0 additions & 15 deletions tests/test_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,6 @@ mod tests {
}
}

#[test]
fn same_timestamp_different_base_epoch() {
let mut node = spaceflake::Node::new(1);
let mut worker = node.new_worker();
let sf1 = worker.generate().expect("Failed generating the Spaceflake");
worker.base_epoch = 1672531200000; // Sunday, January 1, 2023 12:00:00 AM GMT
let sf2 = worker.generate().expect("Failed generating the Spaceflake");
// Thanks Windows
if (sf1.time() > sf2.time() + 5) || (sf1.time() < sf2.time() - 5) {
panic!(
"Timestamps of the generated Spaceflakes are not the same, or at least not close"
)
}
}

#[test]
fn generate_unique() {
let mut spaceflakes: HashMap<String, Spaceflake> = HashMap::new();
Expand Down