ferroid/generator/lock.rs
1use crate::{IdGenStatus, Result, Snowflake, TimeSource};
2use std::{
3 cmp::Ordering,
4 sync::{Arc, Mutex},
5};
6#[cfg(feature = "tracing")]
7use tracing::instrument;
8
9/// A lock-based Snowflake ID generator suitable for multi-threaded
10/// environments.
11///
12/// This generator wraps the Snowflake state in an [`Arc<Mutex<_>>`], allowing
13/// safe shared use across threads.
14///
15/// ## Features
16///
17/// - ✅ Thread-safe
18/// - ✅ Safely implement any [`Snowflake`] layout
19///
20/// ## Recommended When
21/// - You're in a multi-threaded environment
22/// - Fair access across threads is important
23///
24/// ## See Also
25/// - [`BasicSnowflakeGenerator`]
26/// - [`AtomicSnowflakeGenerator`]
27///
28/// [`BasicSnowflakeGenerator`]: crate::BasicSnowflakeGenerator
29/// [`AtomicSnowflakeGenerator`]: crate::AtomicSnowflakeGenerator
30pub struct LockSnowflakeGenerator<ID, T>
31where
32 ID: Snowflake,
33 T: TimeSource<ID::Ty>,
34{
35 state: Arc<Mutex<ID>>,
36 time: T,
37}
38
39impl<ID, T> LockSnowflakeGenerator<ID, T>
40where
41 ID: Snowflake,
42 T: TimeSource<ID::Ty>,
43{
44 /// Creates a new [`LockSnowflakeGenerator`] initialized with the current
45 /// time and a given machine ID.
46 ///
47 /// This constructor sets the initial timestamp and sequence to zero, and
48 /// uses the provided `clock` to fetch the current time during ID
49 /// generation. It is the recommended way to create a new atomic generator
50 /// for typical use cases.
51 ///
52 /// # Parameters
53 ///
54 /// - `machine_id`: A unique identifier for the node or instance generating
55 /// IDs. This value will be encoded into every generated ID.
56 /// - `clock`: A [`TimeSource`] implementation (e.g., [`MonotonicClock`])
57 /// that determines how timestamps are generated.
58 ///
59 /// # Returns
60 ///
61 /// A new [`LockSnowflakeGenerator`] ready to produce unique, time-ordered
62 /// IDs.
63 ///
64 /// # Example
65 ///
66 /// ```
67 /// use ferroid::{LockSnowflakeGenerator, SnowflakeTwitterId, MonotonicClock};
68 ///
69 /// let generator = LockSnowflakeGenerator::<SnowflakeTwitterId, _>::new(0, MonotonicClock::default());
70 /// let id = generator.next_id();
71 /// ```
72 ///
73 /// [`TimeSource`]: crate::TimeSource
74 /// [`MonotonicClock`]: crate::MonotonicClock
75 pub fn new(machine_id: ID::Ty, clock: T) -> Self {
76 Self::from_components(ID::ZERO, machine_id, ID::ZERO, clock)
77 }
78
79 /// Creates a new ID generator from explicit component values.
80 ///
81 /// This constructor is primarily useful for advanced use cases such as
82 /// restoring state from persistent storage or controlling the starting
83 /// point of the generator manually.
84 ///
85 /// # Parameters
86 /// - `timestamp`: The initial timestamp component (usually in milliseconds)
87 /// - `machine_id`: The machine or worker identifier
88 /// - `sequence`: The initial sequence number
89 /// - `clock`: A [`TimeSource`] implementation used to fetch the current
90 /// time
91 ///
92 /// # Returns
93 /// A new generator instance preloaded with the given state.
94 ///
95 /// # Note
96 /// In typical use cases, you should prefer [`Self::new`] to let the
97 /// generator initialize itself from the current time.
98 pub fn from_components(
99 timestamp: ID::Ty,
100 machine_id: ID::Ty,
101 sequence: ID::Ty,
102 clock: T,
103 ) -> Self {
104 let id = ID::from_components(timestamp, machine_id, sequence);
105 Self {
106 state: Arc::new(Mutex::new(id)),
107 time: clock,
108 }
109 }
110
111 /// Attempts to generate the next available ID.
112 ///
113 /// Returns a new, time-ordered, unique ID if generation succeeds. If the
114 /// generator is temporarily exhausted (e.g., the sequence is full and the
115 /// clock has not advanced), it returns [`IdGenStatus::Pending`].
116 ///
117 /// # Panics
118 /// Panics if the lock is poisoned. For explicitly fallible behavior, use
119 /// [`Self::try_next_id`] instead.
120 ///
121 /// # Example
122 /// ```
123 /// use ferroid::{LockSnowflakeGenerator, SnowflakeTwitterId, IdGenStatus, MonotonicClock, TimeSource};
124 ///
125 /// // Create a clock and a generator with machine_id = 0
126 /// let clock = MonotonicClock::default();
127 /// let generator = LockSnowflakeGenerator::<SnowflakeTwitterId, _>::new(0, clock);
128 ///
129 /// // Attempt to generate a new ID
130 /// match generator.next_id() {
131 /// IdGenStatus::Ready { id } => {
132 /// println!("ID: {}", id);
133 /// assert_eq!(id.machine_id(), 0);
134 /// }
135 /// IdGenStatus::Pending { yield_for } => {
136 /// println!("Exhausted; wait for: {}ms", yield_for);
137 /// }
138 /// }
139 /// ```
140 pub fn next_id(&self) -> IdGenStatus<ID> {
141 self.try_next_id().unwrap()
142 }
143
144 /// A fallible version of [`Self::next_id`] that returns a [`Result`].
145 ///
146 /// This method attempts to generate the next ID based on the current time
147 /// and internal state. If successful, it returns [`IdGenStatus::Ready`]
148 /// with a newly generated ID. If the generator is temporarily exhausted, it
149 /// returns [`IdGenStatus::Pending`]. If an internal failure occurs (e.g., a
150 /// time source or lock error), it returns an error.
151 ///
152 /// # Returns
153 /// - `Ok(IdGenStatus::Ready { id })`: A new ID is available
154 /// - `Ok(IdGenStatus::Pending { yield_for })`: The time to wait (in
155 /// milliseconds) before trying again
156 /// - `Err(e)`: A recoverable error occurred (e.g., time source failure)
157 ///
158 /// # Example
159 /// ```
160 /// use ferroid::{LockSnowflakeGenerator, SnowflakeTwitterId, IdGenStatus, MonotonicClock, TimeSource};
161 ///
162 /// // Create a clock and a generator with machine_id = 0
163 /// let clock = MonotonicClock::default();
164 /// let generator = LockSnowflakeGenerator::<SnowflakeTwitterId, _>::new(0, clock);
165 ///
166 /// // Attempt to generate a new ID
167 /// match generator.try_next_id() {
168 /// Ok(IdGenStatus::Ready { id }) => {
169 /// println!("ID: {}", id);
170 /// assert_eq!(id.machine_id(), 0);
171 /// }
172 /// Ok(IdGenStatus::Pending { yield_for }) => {
173 /// println!("Exhausted; wait for: {}ms", yield_for);
174 /// }
175 /// Err(err) => eprintln!("Generator error: {}", err),
176 /// }
177 /// ```
178 #[cfg_attr(feature = "tracing", instrument(level = "trace", skip(self)))]
179 pub fn try_next_id(&self) -> Result<IdGenStatus<ID>> {
180 let now = self.time.current_millis();
181 let mut id = self.state.lock()?;
182 let current_ts = id.timestamp();
183
184 let status = match now.cmp(¤t_ts) {
185 Ordering::Less => {
186 let yield_for = current_ts - now;
187 debug_assert!(yield_for >= ID::ZERO);
188 IdGenStatus::Pending { yield_for }
189 }
190 Ordering::Greater => {
191 *id = id.rollover_to_timestamp(now);
192 IdGenStatus::Ready { id: *id }
193 }
194 Ordering::Equal => {
195 if id.has_sequence_room() {
196 *id = id.increment_sequence();
197 IdGenStatus::Ready { id: *id }
198 } else {
199 IdGenStatus::Pending { yield_for: ID::ONE }
200 }
201 }
202 };
203
204 Ok(status)
205 }
206}