ferroid/generator/ulid/lock_mono.rs
1use crate::{
2 rand::RandSource, Error, IdGenStatus, Mutex, Result, TimeSource, UlidGenerator, UlidId,
3};
4use alloc::sync::Arc;
5use core::cmp::Ordering;
6#[cfg(feature = "tracing")]
7use tracing::instrument;
8
9/// A lock-based *monotonic* ULID-style ID generator suitable for multi-threaded
10/// environments.
11///
12/// This generator wraps the Ulid state in an [`Arc<Mutex<_>>`], allowing safe
13/// shared use across threads.
14///
15/// ## Features
16/// - ✅ Thread-safe
17/// - ✅ Probabilistically unique (no coordination required)
18/// - ✅ Time-ordered (monotonically increasing per millisecond)
19///
20/// ## Recommended When
21/// - You're in a multi-threaded environment
22/// - You need require monotonically increasing IDs (ID generated within the
23/// same millisecond increment a sequence counter)
24/// - Your target doesn't support atomics.
25///
26/// ## See Also
27/// - [`BasicUlidGenerator`]
28/// - [`BasicMonoUlidGenerator`]
29/// - [`AtomicMonoUlidGenerator`]
30///
31/// [`BasicUlidGenerator`]: crate::BasicUlidGenerator
32/// [`BasicMonoUlidGenerator`]: crate::BasicMonoUlidGenerator
33/// [`AtomicMonoUlidGenerator`]: crate::AtomicMonoUlidGenerator
34pub struct LockMonoUlidGenerator<ID, T, R>
35where
36 ID: UlidId,
37 T: TimeSource<ID::Ty>,
38 R: RandSource<ID::Ty>,
39{
40 #[cfg(feature = "cache-padded")]
41 state: Arc<crossbeam_utils::CachePadded<Mutex<ID>>>,
42 #[cfg(not(feature = "cache-padded"))]
43 state: Arc<Mutex<ID>>,
44 time: T,
45 rng: R,
46}
47
48impl<ID, T, R> LockMonoUlidGenerator<ID, T, R>
49where
50 ID: UlidId,
51 T: TimeSource<ID::Ty>,
52 R: RandSource<ID::Ty>,
53{
54 /// Creates a new [`LockMonoUlidGenerator`] with the provided time source and
55 /// RNG.
56 ///
57 /// # Parameters
58 /// - `time`: A [`TimeSource`] used to retrieve the current timestamp
59 /// - `rng`: A [`RandSource`] used to generate random bits
60 ///
61 /// # Returns
62 /// A ready-to-use ULID generator suitable for producing unique, sortable
63 /// IDs.
64 ///
65 /// # Example
66 /// ```
67 /// use ferroid::{LockMonoUlidGenerator, IdGenStatus, ULID, MonotonicClock, ThreadRandom};
68 ///
69 /// let generator = LockMonoUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
70 ///
71 /// let id: ULID = loop {
72 /// match generator.next_id() {
73 /// IdGenStatus::Ready { id } => break id,
74 /// IdGenStatus::Pending { .. } => core::hint::spin_loop(),
75 /// }
76 /// };
77 /// ```
78 ///
79 /// [`TimeSource`]: crate::TimeSource
80 /// [`RandSource`]: crate::RandSource
81 pub fn new(time: T, rng: R) -> Self {
82 Self::from_components(ID::ZERO, ID::ZERO, time, rng)
83 }
84
85 /// Creates a new ID generator from explicit component values.
86 ///
87 /// This constructor is primarily useful for advanced use cases such as
88 /// restoring state from persistent storage or controlling the starting
89 /// point of the generator manually.
90 ///
91 /// # Parameters
92 /// - `timestamp`: The initial timestamp component (usually in milliseconds)
93 /// - `machine_id`: The machine or worker identifier
94 /// - `sequence`: The initial sequence number
95 /// - `time`: A [`TimeSource`] implementation used to fetch the current
96 /// time
97 ///
98 /// # Returns
99 /// A new generator instance preloaded with the given state.
100 ///
101 /// # ⚠️ Note
102 /// In typical use cases, you should prefer [`Self::new`] to let the
103 /// generator initialize itself from the current time.
104 pub fn from_components(timestamp: ID::Ty, random: ID::Ty, time: T, rng: R) -> Self {
105 let id = ID::from_components(timestamp, random);
106 Self {
107 #[cfg(feature = "cache-padded")]
108 state: Arc::new(crossbeam_utils::CachePadded::new(Mutex::new(id))),
109 #[cfg(not(feature = "cache-padded"))]
110 state: Arc::new(Mutex::new(id)),
111 time,
112 rng,
113 }
114 }
115
116 /// Generates a new ULID.
117 ///
118 /// Internally calls [`Self::try_next_id`] and unwraps the result. This
119 /// method will panic on error, so prefer the fallible version if you want
120 /// explicit control over error handling.
121 ///
122 /// # Panics
123 /// Panics if the lock is poisoned. For explicitly fallible behavior, use
124 /// [`Self::try_next_id`] instead.
125 ///
126 /// # Example
127 /// ```
128 /// use ferroid::{LockMonoUlidGenerator, IdGenStatus, ULID, MonotonicClock, ThreadRandom};
129 ///
130 /// let generator = LockMonoUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
131 ///
132 /// let id: ULID = loop {
133 /// match generator.next_id() {
134 /// IdGenStatus::Ready { id } => break id,
135 /// IdGenStatus::Pending { .. } => std::thread::yield_now(),
136 /// }
137 /// };
138 /// ```
139 pub fn next_id(&self) -> IdGenStatus<ID> {
140 self.try_next_id().unwrap()
141 }
142
143 /// Attempts to generate a new ULID with fallible error handling.
144 ///
145 /// Combines the current timestamp with a freshly generated random value to
146 /// produce a unique identifier. Returns [`IdGenStatus::Ready`] on success.
147 ///
148 /// # Returns
149 /// - `Ok(IdGenStatus::Ready { id })`: A new ID is available
150 /// - `Ok(IdGenStatus::Pending { yield_for })`: The time to wait (in
151 /// milliseconds) before trying again
152 /// - `Err(e)`: the lock was poisoned
153 ///
154 /// # Errors
155 /// - Returns an error if the underlying lock has been poisoned.
156 ///
157 /// # Example
158 /// ```
159 /// use ferroid::{LockMonoUlidGenerator, IdGenStatus, ULID, ToU64, MonotonicClock, ThreadRandom};
160 ///
161 /// let generator = LockMonoUlidGenerator::new(MonotonicClock::default(), ThreadRandom::default());
162 ///
163 /// // Attempt to generate a new ID
164 /// let id: ULID = loop {
165 /// match generator.try_next_id() {
166 /// Ok(IdGenStatus::Ready { id }) => break id,
167 /// Ok(IdGenStatus::Pending { yield_for }) => {
168 /// std::thread::sleep(core::time::Duration::from_millis(yield_for.to_u64()));
169 /// }
170 /// Err(e) => panic!("Generator error: {}", e),
171 /// }
172 /// };
173 /// ```
174 #[cfg_attr(feature = "tracing", instrument(level = "trace", skip(self)))]
175 pub fn try_next_id(&self) -> Result<IdGenStatus<ID>, Error<core::convert::Infallible>> {
176 let now = self.time.current_millis();
177
178 #[cfg(feature = "parking-lot")]
179 let mut id = self.state.lock();
180 #[cfg(not(feature = "parking-lot"))]
181 let mut id = self.state.lock()?;
182
183 let current_ts = id.timestamp();
184
185 match now.cmp(¤t_ts) {
186 Ordering::Equal => {
187 if id.has_random_room() {
188 *id = id.increment_random();
189 Ok(IdGenStatus::Ready { id: *id })
190 } else {
191 Ok(IdGenStatus::Pending { yield_for: ID::ONE })
192 }
193 }
194 Ordering::Greater => {
195 let rand = self.rng.rand();
196 *id = id.rollover_to_timestamp(now, rand);
197 Ok(IdGenStatus::Ready { id: *id })
198 }
199 Ordering::Less => Ok(Self::cold_clock_behind(now, current_ts)),
200 }
201 }
202
203 #[cold]
204 #[inline(never)]
205 fn cold_clock_behind(now: ID::Ty, current_ts: ID::Ty) -> IdGenStatus<ID> {
206 let yield_for = current_ts - now;
207 debug_assert!(yield_for >= ID::ZERO);
208 IdGenStatus::Pending { yield_for }
209 }
210}
211
212impl<ID, T, R> UlidGenerator<ID, T, R> for LockMonoUlidGenerator<ID, T, R>
213where
214 ID: UlidId,
215 T: TimeSource<ID::Ty>,
216 R: RandSource<ID::Ty>,
217{
218 type Err = Error;
219
220 fn new(time: T, rng: R) -> Self {
221 Self::new(time, rng)
222 }
223
224 fn next_id(&self) -> IdGenStatus<ID> {
225 self.next_id()
226 }
227
228 fn try_next_id(&self) -> Result<IdGenStatus<ID>, Self::Err> {
229 self.try_next_id()
230 }
231}