From 14496141e2f6b4643de0a66b6723e1a18bb0fbe5 Mon Sep 17 00:00:00 2001 From: Nyvil Date: Mon, 2 Jun 2025 20:45:38 +0200 Subject: [PATCH 1/3] feat: Add clock drift tolerance handling --- src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2329774..8640a90 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{fmt, thread}; - +use std::thread::sleep; use rand::Rng; /// The default epoch used **with milliseconds**, which is the 1st of January 2015 at 12:00:00 AM GMT. @@ -16,6 +16,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. @@ -479,6 +482,11 @@ fn generate_on_node_and_worker( )); } + if generate_at + CLOCK_DRIFT_TOLERANCE_MS < now { + let delta = now - generate_at; + sleep(Duration::from_millis(delta + CLOCK_DRIFT_TOLERANCE_MS)) + } + let mut increment = worker.increment.lock().unwrap(); if *increment >= MAX_12_BITS { *increment = 0 From 688416d1d124f14b1baf1878585e864658b2f9e4 Mon Sep 17 00:00:00 2001 From: Nyvil Date: Mon, 2 Jun 2025 21:03:32 +0200 Subject: [PATCH 2/3] make consistent with go --- src/lib.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8640a90..c090f1f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,6 +241,7 @@ pub struct Worker { pub sequence: u64, /// The incremented number of the worker, used for the sequence. increment: Arc>, + last_timestamp: u64, } /// The default implementation of a worker. @@ -256,6 +257,7 @@ impl Worker { node_id, sequence: 0, increment: Arc::new(Mutex::new(0)), + last_timestamp: 0, } } @@ -320,7 +322,7 @@ pub fn bulk_generate(settings: BulkGeneratorSettings) -> Result, let mut spaceflakes = Vec::::new(); for i in 1..=settings.amount { if i % ((MAX_12_BITS * MAX_5_BITS * MAX_5_BITS) as usize) == 0 { - thread::sleep(Duration::from_millis(1)); + sleep(Duration::from_millis(1)); let mut new_node = Node::new(1); let mut new_worker = new_node.new_worker(); new_worker.base_epoch = settings.base_epoch; @@ -450,7 +452,7 @@ pub fn decompose_binary(spaceflake_id: u64, base_epoch: u64) -> HashMap, ) -> Result { let now = SystemTime::now() @@ -482,11 +484,28 @@ fn generate_on_node_and_worker( )); } - if generate_at + CLOCK_DRIFT_TOLERANCE_MS < now { - let delta = now - generate_at; - sleep(Duration::from_millis(delta + CLOCK_DRIFT_TOLERANCE_MS)) + 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)); + } + + 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 From a382ea221b0cab9209ad03b174d893a3ee6e19f9 Mon Sep 17 00:00:00 2001 From: Krypton Date: Mon, 2 Jun 2025 21:21:38 +0200 Subject: [PATCH 3/3] chore: Small changes & add tests back --- .github/FUNDING.yml | 2 ++ .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++++----- .github/workflows/publish.yml | 9 +++------ CONTRIBUTING.md | 21 +++----------------- Cargo.toml | 6 +++--- src/lib.rs | 21 +++++++------------- tests/test_lib.rs | 15 -------------- 7 files changed, 50 insertions(+), 61 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6d9ee9e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [kkrypt0nn] +custom: ["https://buymeacoffee.com/kkrypt0nn"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16f2aec..fe9c1f5 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 042b33f..f292a97 100755 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: spaceflake.rs CD (Publish) +name: Spaceflake Rust CD (Publish) on: push: @@ -6,15 +6,12 @@ on: - "*" 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 }} \ No newline at end of file + run: cargo publish --verbose --all-features --token ${{ secrets.CARGO_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc4631b..5b240c5 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. \ No newline at end of file +Your submissions are understood to be under the same [MIT License](./LICENSE.md) that covers the project. diff --git a/Cargo.toml b/Cargo.toml index 119f318..71921d6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/lib.rs b/src/lib.rs index c090f1f..593e3b0 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,10 @@ #![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 std::thread::sleep; -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; @@ -241,6 +240,7 @@ pub struct Worker { pub sequence: u64, /// The incremented number of the worker, used for the sequence. increment: Arc>, + /// The timestamp of the most recently generated Spaceflake, used to prevent clock drifting. last_timestamp: u64, } @@ -322,7 +322,7 @@ pub fn bulk_generate(settings: BulkGeneratorSettings) -> Result, let mut spaceflakes = Vec::::new(); for i in 1..=settings.amount { if i % ((MAX_12_BITS * MAX_5_BITS * MAX_5_BITS) as usize) == 0 { - sleep(Duration::from_millis(1)); + thread::sleep(Duration::from_millis(1)); let mut new_node = Node::new(1); let mut new_worker = new_node.new_worker(); new_worker.base_epoch = settings.base_epoch; @@ -399,7 +399,7 @@ impl Default for GeneratorSettings { pub fn generate(settings: GeneratorSettings) -> Result { 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; } @@ -412,7 +412,7 @@ pub fn generate(settings: GeneratorSettings) -> Result { pub fn generate_at(settings: GeneratorSettings, at: u64) -> Result { 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; } @@ -485,35 +485,28 @@ 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)); } - - sleep(Duration::from_millis(delta + 1)); + 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); diff --git a/tests/test_lib.rs b/tests/test_lib.rs index 4a339cb..7f48cbd 100755 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -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 = HashMap::new();