scru64/
lib.rs

1//! # SCRU64: Sortable, Clock-based, Realm-specifically Unique identifier
2//!
3//! SCRU64 ID offers compact, time-ordered unique identifiers generated by
4//! distributed nodes. SCRU64 has the following features:
5//!
6//! - 63-bit non-negative integer storable as signed/unsigned 64-bit integer
7//! - Sortable by generation time (as integer and as text)
8//! - 12-digit case-insensitive textual representation (Base36)
9//! - ~38-bit Unix epoch-based timestamp that ensures useful life until year 4261
10//! - Variable-length node/machine ID and counter fields that share 24 bits
11//!
12//! ```rust
13//! # unsafe { std::env::set_var("SCRU64_NODE_SPEC", "42/8") };
14//! // pass node ID through environment variable
15//! // (e.g., SCRU64_NODE_SPEC=42/8 command ...)
16//!
17//! // generate a new identifier object
18//! let x = scru64::new_sync();
19//! println!("{}", x); // e.g., "0u2r85hm2pt3"
20//! println!("{}", x.to_u64()); // as a 64-bit unsigned integer
21//!
22//! // generate a textual representation directly
23//! println!("{}", scru64::new_string_sync()); // e.g., "0u2r85hm2pt4"
24//! ```
25//!
26//! See [SCRU64 Specification] for details.
27//!
28//! SCRU64's uniqueness is realm-specific, i.e., dependent on the centralized
29//! assignment of node ID to each generator. If you need decentralized, globally
30//! unique time-ordered identifiers, consider [SCRU128].
31//!
32//! [SCRU64 Specification]: https://github.com/scru64/spec
33//! [SCRU128]: https://github.com/scru128/spec
34//!
35//! ## Crate features
36//!
37//! Default features:
38//!
39//! - `std` integrates the library with, among others, the system clock to draw
40//!   current timestamps. Without `std`, this crate provides limited functionality
41//!   available under `no_std` environments.
42//! - `global_gen` (implies `std`) enables the [`new()`], [`new_string()`], [`new_sync()`],
43//!   and [`new_string_sync()`] primary entry point functions as well as the
44//!   process-wide global generator under the hood.
45//!
46//! Optional features:
47//!
48//! - `serde` enables serialization/deserialization via serde.
49
50#![cfg_attr(not(feature = "std"), no_std)]
51#![cfg_attr(docsrs, feature(doc_cfg))]
52
53pub mod generator;
54pub mod id;
55
56pub use generator::Scru64Generator;
57pub use id::Scru64Id;
58
59#[cfg(feature = "global_gen")]
60#[cfg_attr(docsrs, doc(cfg(feature = "global_gen")))]
61pub use shortcut::{new, new_string, new_string_sync, new_sync};
62
63#[cfg(test)]
64mod test_cases;
65
66/// The total size in bits of the `node_id` and `counter` fields.
67const NODE_CTR_SIZE: u8 = 24;
68
69#[cfg(feature = "global_gen")]
70mod shortcut {
71    use std::{thread, time};
72
73    use crate::{generator::GlobalGenerator, Scru64Id};
74
75    const DELAY: time::Duration = time::Duration::from_millis(64);
76
77    /// Generates a new SCRU64 ID object using the global generator.
78    ///
79    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
80    /// for the next timestamp tick.
81    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
82    ///
83    /// # Panics
84    ///
85    /// Panics if the global generator is not properly configured.
86    pub async fn new() -> Scru64Id {
87        if let Some(value) = GlobalGenerator.generate() {
88            value
89        } else {
90            spawn_thread_or_block(new_sync).await
91        }
92    }
93
94    /// Generates a new SCRU64 ID encoded in the 12-digit canonical string representation using the
95    /// global generator.
96    ///
97    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
98    /// for the next timestamp tick.
99    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
100    ///
101    /// # Panics
102    ///
103    /// Panics if the global generator is not properly configured.
104    pub async fn new_string() -> String {
105        if let Some(value) = GlobalGenerator.generate() {
106            value.into()
107        } else {
108            spawn_thread_or_block(new_string_sync).await
109        }
110    }
111
112    /// Executes a task asynchronously in a separate thread, or falls back on a blocking operation
113    /// upon failure in creating a new thread.
114    #[cold]
115    async fn spawn_thread_or_block<T: Send + 'static>(f: fn() -> T) -> T {
116        match thread_async::run_with_builder(thread::Builder::new(), f) {
117            Ok((ftr, _)) => ftr.await,
118            Err(_) => f(),
119        }
120    }
121
122    /// Generates a new SCRU64 ID object using the global generator.
123    ///
124    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
125    /// for the next timestamp tick. It employs blocking sleep to wait; see [`new`] for the
126    /// non-blocking equivalent.
127    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
128    ///
129    /// # Panics
130    ///
131    /// Panics if the global generator is not properly configured.
132    pub fn new_sync() -> Scru64Id {
133        loop {
134            if let Some(value) = GlobalGenerator.generate() {
135                break value;
136            } else {
137                #[cfg(test)]
138                tests::SLEEP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
139
140                thread::sleep(DELAY);
141            }
142        }
143    }
144
145    /// Generates a new SCRU64 ID encoded in the 12-digit canonical string representation using the
146    /// global generator.
147    ///
148    /// This function usually returns a value immediately, but if not possible, it sleeps and waits
149    /// for the next timestamp tick. It employs blocking sleep to wait; see [`new_string`] for the
150    /// non-blocking equivalent.
151    #[doc = concat!("\n\n", include_str!("generator/doc_global_gen.md"), "\n\n")]
152    ///
153    /// # Panics
154    ///
155    /// Panics if the global generator is not properly configured.
156    pub fn new_string_sync() -> String {
157        new_sync().into()
158    }
159
160    #[cfg(test)]
161    mod tests {
162        use super::{new, new_string, new_string_sync, new_sync, GlobalGenerator};
163        use std::sync::atomic;
164
165        // (ticks per rollback_allowance) * (average expected capacity of 16-bit counter per tick)
166        // / (aggregate number of for-loops in tests) ~= 320k
167        const N: usize = (10_000 >> 8) * (1 << 15) / (4);
168
169        fn setup() {
170            let _ = GlobalGenerator.initialize("42/8".parse().unwrap());
171        }
172
173        pub static SLEEP_COUNT: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
174
175        fn teardown(name: &str) {
176            let c = SLEEP_COUNT.load(atomic::Ordering::SeqCst);
177            println!("finishing {}: global sleep count is now at {}", name, c);
178        }
179
180        /// Generates a ton of monotonically increasing IDs.
181        #[tokio::test]
182        async fn new_many() {
183            setup();
184
185            let mut prev = new().await;
186            for _ in 0..N {
187                let curr = new().await;
188                assert!(prev < curr);
189                prev = curr;
190            }
191
192            let mut prev = String::from(prev);
193            for _ in 0..N {
194                let curr = new_string().await;
195                assert!(prev < curr);
196                prev = curr;
197            }
198
199            teardown("new_many");
200        }
201
202        /// Generates a ton of monotonically increasing IDs.
203        #[test]
204        fn new_sync_many() {
205            setup();
206
207            let mut prev = new_sync();
208            for _ in 0..N {
209                let curr = new_sync();
210                assert!(prev < curr);
211                prev = curr;
212            }
213
214            let mut prev = String::from(prev);
215            for _ in 0..N {
216                let curr = new_string_sync();
217                assert!(prev < curr);
218                prev = curr;
219            }
220
221            teardown("new_sync_many");
222        }
223    }
224}