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}