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(&current_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}