scru64 2.0.1

SCRU64: Sortable, Clock-based, Realm-specifically Unique identifier
Documentation
//! # SCRU64: Sortable, Clock-based, Realm-specifically Unique identifier
//!
//! SCRU64 ID offers compact, time-ordered unique identifiers generated by
//! distributed nodes. SCRU64 has the following features:
//!
//! - 63-bit non-negative integer storable as signed/unsigned 64-bit integer
//! - Sortable by generation time (as integer and as text)
//! - 12-digit case-insensitive textual representation (Base36)
//! - ~38-bit Unix epoch-based timestamp that ensures useful life until year 4261
//! - Variable-length node/machine ID and counter fields that share 24 bits
//!
//! ```rust
//! # unsafe { std::env::set_var("SCRU64_NODE_SPEC", "42/8") };
//! // pass node ID through environment variable
//! // (e.g., SCRU64_NODE_SPEC=42/8 command ...)
//!
//! // generate a new identifier object
//! let x = scru64::new_sync();
//! println!("{}", x); // e.g., "0u2r85hm2pt3"
//! println!("{}", x.to_u64()); // as a 64-bit unsigned integer
//!
//! // generate a textual representation directly
//! println!("{}", scru64::new_string_sync()); // e.g., "0u2r85hm2pt4"
//! ```
//!
//! See [SCRU64 Specification] for details.
//!
//! SCRU64's uniqueness is realm-specific, i.e., dependent on the centralized
//! assignment of node ID to each generator. If you need decentralized, globally
//! unique time-ordered identifiers, consider [SCRU128].
//!
//! [SCRU64 Specification]: https://github.com/scru64/spec
//! [SCRU128]: https://github.com/scru128/spec
//!
//! ## Crate features
//!
//! Default features:
//!
//! - `std` integrates the library with, among others, the system clock to draw
//!   current timestamps. Without `std`, this crate provides limited functionality
//!   available under `no_std` environments.
//! - `global_gen` (implies `std`) enables the [`new()`], [`new_string()`], [`new_sync()`],
//!   and [`new_string_sync()`] primary entry point functions as well as the
//!   process-wide global generator under the hood.
//!
//! Optional features:
//!
//! - `serde` enables serialization/deserialization via serde.

#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]

pub mod generator;
pub mod id;

pub use generator::Scru64Generator;
pub use id::Scru64Id;

#[cfg(feature = "global_gen")]
#[cfg_attr(docsrs, doc(cfg(feature = "global_gen")))]
pub use shortcut::{new, new_string, new_string_sync, new_sync};

#[cfg(test)]
mod test_cases;

/// The total size in bits of the `node_id` and `counter` fields.
const NODE_CTR_SIZE: u8 = 24;

#[cfg(feature = "global_gen")]
mod shortcut {
    use std::{thread, time};

    use crate::{generator::GlobalGenerator, Scru64Id};

    const DELAY: time::Duration = time::Duration::from_millis(64);

    /// Generates a new SCRU64 ID object using the global generator.
    ///
    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
    /// for the next timestamp tick.
    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
    ///
    /// # Panics
    ///
    /// Panics if the global generator is not properly configured.
    pub async fn new() -> Scru64Id {
        if let Some(value) = GlobalGenerator.generate() {
            value
        } else {
            spawn_thread_or_block(new_sync).await
        }
    }

    /// Generates a new SCRU64 ID encoded in the 12-digit canonical string representation using the
    /// global generator.
    ///
    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
    /// for the next timestamp tick.
    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
    ///
    /// # Panics
    ///
    /// Panics if the global generator is not properly configured.
    pub async fn new_string() -> String {
        if let Some(value) = GlobalGenerator.generate() {
            value.into()
        } else {
            spawn_thread_or_block(new_string_sync).await
        }
    }

    /// Executes a task asynchronously in a separate thread, or falls back on a blocking operation
    /// upon failure in creating a new thread.
    #[cold]
    async fn spawn_thread_or_block<T: Send + 'static>(f: fn() -> T) -> T {
        match thread_async::run_with_builder(thread::Builder::new(), f) {
            Ok((ftr, _)) => ftr.await,
            Err(_) => f(),
        }
    }

    /// Generates a new SCRU64 ID object using the global generator.
    ///
    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
    /// for the next timestamp tick. It employs blocking sleep to wait; see [`new`] for the
    /// non-blocking equivalent.
    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
    ///
    /// # Panics
    ///
    /// Panics if the global generator is not properly configured.
    pub fn new_sync() -> Scru64Id {
        loop {
            if let Some(value) = GlobalGenerator.generate() {
                break value;
            } else {
                #[cfg(test)]
                tests::SLEEP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);

                thread::sleep(DELAY);
            }
        }
    }

    /// Generates a new SCRU64 ID encoded in the 12-digit canonical string representation using the
    /// global generator.
    ///
    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
    /// for the next timestamp tick. It employs blocking sleep to wait; see [`new_string`] for the
    /// non-blocking equivalent.
    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
    ///
    /// # Panics
    ///
    /// Panics if the global generator is not properly configured.
    pub fn new_string_sync() -> String {
        new_sync().into()
    }

    #[cfg(test)]
    mod tests {
        use super::{new, new_string, new_string_sync, new_sync, GlobalGenerator};
        use std::sync::atomic;

        // (ticks per rollback_allowance) * (average expected capacity of 16-bit counter per tick)
        // / (aggregate number of for-loops in tests) ~= 320k
        const N: usize = (10_000 >> 8) * (1 << 15) / (4);

        fn setup() {
            let _ = GlobalGenerator.initialize("42/8".parse().unwrap());
        }

        pub static SLEEP_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0);

        fn teardown(name: &str) {
            let c = SLEEP_COUNT.load(atomic::Ordering::SeqCst);
            println!("finishing {}: global sleep count is now at {}", name, c);
        }

        /// Generates a ton of monotonically increasing IDs.
        #[tokio::test]
        async fn new_many() {
            setup();

            let mut prev = new().await;
            for _ in 0..N {
                let curr = new().await;
                assert!(prev < curr);
                prev = curr;
            }

            let mut prev = String::from(prev);
            for _ in 0..N {
                let curr = new_string().await;
                assert!(prev < curr);
                prev = curr;
            }

            teardown("new_many");
        }

        /// Generates a ton of monotonically increasing IDs.
        #[test]
        fn new_sync_many() {
            setup();

            let mut prev = new_sync();
            for _ in 0..N {
                let curr = new_sync();
                assert!(prev < curr);
                prev = curr;
            }

            let mut prev = String::from(prev);
            for _ in 0..N {
                let curr = new_string_sync();
                assert!(prev < curr);
                prev = curr;
            }

            teardown("new_sync_many");
        }
    }
}