Skip to main content

apalis_core/task/
task_id.rs

1//! Defines the `TaskId` type and related functionality.
2//!
3//! `TaskId` is an identifier for a task, supporting a fixed set of common
4//! id representations (integer, string, and optionally UUID/ULID when the
5//! corresponding features are enabled).
6use std::{
7    fmt::{Debug, Display},
8    str::FromStr,
9};
10
11use crate::{
12    task::from_request::FromRequest,
13    task::{Task, data::MissingDataError},
14};
15
16pub use random_id::RandomId;
17
18/// A wrapper type that defines a task id.
19///
20/// Supports a fixed set of common identifier representations. `Uuid` and
21/// `Ulid` variants are only available when the corresponding crate features
22/// are enabled.
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
25#[cfg_attr(feature = "serde", serde(untagged))]
26#[non_exhaustive]
27pub enum TaskId {
28    /// An integer-based id
29    Int(u64),
30    /// A string-based id
31    String(String),
32    /// A UUID-based id
33    #[cfg(feature = "uuid")]
34    Uuid(uuid::Uuid),
35    /// A ULID-based id
36    #[cfg(feature = "ulid")]
37    Ulid(ulid::Ulid),
38}
39
40impl TaskId {
41    /// Construct a `TaskId` from an integer.
42    #[must_use]
43    pub fn from_int(id: u64) -> Self {
44        Self::Int(id)
45    }
46
47    /// Construct a `TaskId` from a string.
48    #[must_use]
49    pub fn from_string(id: impl Into<String>) -> Self {
50        Self::String(id.into())
51    }
52
53    /// Construct a `TaskId` from a UUID.
54    #[cfg(feature = "uuid")]
55    #[must_use]
56    pub fn from_uuid(id: uuid::Uuid) -> Self {
57        Self::Uuid(id)
58    }
59
60    /// Construct a `TaskId` from a ULID.
61    #[cfg(feature = "ulid")]
62    #[must_use]
63    pub fn from_ulid(id: ulid::Ulid) -> Self {
64        Self::Ulid(id)
65    }
66
67    /// Returns the inner value as an integer, if this is an `Int` variant.
68    #[must_use]
69    #[allow(clippy::match_wildcard_for_single_variants)]
70    pub fn as_int(&self) -> Option<u64> {
71        match self {
72            Self::Int(id) => Some(*id),
73            _ => None,
74        }
75    }
76
77    /// Returns the inner value as a string slice, if this is a `String` variant.
78    #[must_use]
79    #[allow(clippy::match_wildcard_for_single_variants)]
80    pub fn as_string(&self) -> Option<&str> {
81        match self {
82            Self::String(id) => Some(id.as_str()),
83            _ => None,
84        }
85    }
86
87    /// Returns the inner value as a `Uuid`, if this is a `Uuid` variant.
88    #[cfg(feature = "uuid")]
89    #[must_use]
90    pub fn as_uuid(&self) -> Option<uuid::Uuid> {
91        match self {
92            Self::Uuid(id) => Some(*id),
93            _ => None,
94        }
95    }
96
97    /// Returns the inner value as a `Ulid`, if this is a `Ulid` variant.
98    #[cfg(feature = "ulid")]
99    #[must_use]
100    pub fn as_ulid(&self) -> Option<ulid::Ulid> {
101        match self {
102            Self::Ulid(id) => Some(*id),
103            _ => None,
104        }
105    }
106}
107
108impl TaskId {
109    /// Generates a deterministic uuid based on the task id.
110    #[cfg(feature = "uuid")]
111    #[must_use]
112    pub fn to_uuid(&self) -> uuid::Uuid {
113        const NAMESPACE: uuid::Uuid = uuid::Uuid::from_u128(0x6ba7b8109dad11d180b400c04fd430c8);
114
115        match self {
116            Self::Int(id) => uuid::Uuid::new_v5(&NAMESPACE, &id.to_be_bytes()),
117            Self::String(id) => uuid::Uuid::new_v5(&NAMESPACE, id.as_bytes()),
118            #[cfg(feature = "uuid")]
119            Self::Uuid(id) => *id,
120            #[cfg(feature = "ulid")]
121            Self::Ulid(id) => (*id).into(),
122        }
123    }
124}
125
126/// Errors that can occur when parsing a `TaskId` from a string
127#[derive(Debug, thiserror::Error)]
128#[non_exhaustive]
129pub enum TaskIdError {
130    /// The string did not match any known `TaskId` representation
131    #[error("could not decode task_id: `{0}`")]
132    Decode(String),
133}
134
135impl FromStr for TaskId {
136    type Err = TaskIdError;
137    fn from_str(s: &str) -> Result<Self, Self::Err> {
138        #[cfg(feature = "uuid")]
139        if let Ok(id) = uuid::Uuid::from_str(s) {
140            return Ok(Self::Uuid(id));
141        }
142
143        #[cfg(feature = "ulid")]
144        if let Ok(id) = ulid::Ulid::from_str(s) {
145            return Ok(Self::Ulid(id));
146        }
147
148        if let Ok(id) = u64::from_str(s) {
149            return Ok(Self::Int(id));
150        }
151
152        if !s.is_empty() {
153            return Ok(Self::String(s.to_owned()));
154        }
155
156        Err(TaskIdError::Decode(s.to_owned()))
157    }
158}
159
160impl TryFrom<&'_ str> for TaskId {
161    type Error = TaskIdError;
162
163    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
164        Self::from_str(value)
165    }
166}
167
168impl Display for TaskId {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        match self {
171            Self::Int(id) => Display::fmt(id, f),
172            Self::String(id) => Display::fmt(id, f),
173            #[cfg(feature = "uuid")]
174            Self::Uuid(id) => Display::fmt(id, f),
175            #[cfg(feature = "ulid")]
176            Self::Ulid(id) => Display::fmt(id, f),
177        }
178    }
179}
180
181impl<Args: Sync> FromRequest<Task<Args>> for TaskId {
182    type Error = MissingDataError;
183    async fn from_request(req: &Task<Args>) -> Result<Self, Self::Error> {
184        req.task_id().cloned().ok_or(MissingDataError::NotFound(
185            std::any::type_name::<Self>().to_owned(),
186        ))
187    }
188}
189
190impl From<u64> for TaskId {
191    fn from(id: u64) -> Self {
192        Self::Int(id)
193    }
194}
195
196impl From<String> for TaskId {
197    fn from(id: String) -> Self {
198        Self::String(id)
199    }
200}
201
202#[cfg(feature = "uuid")]
203impl From<uuid::Uuid> for TaskId {
204    fn from(id: uuid::Uuid) -> Self {
205        Self::Uuid(id)
206    }
207}
208
209#[cfg(feature = "ulid")]
210impl From<ulid::Ulid> for TaskId {
211    fn from(id: ulid::Ulid) -> Self {
212        Self::Ulid(id)
213    }
214}
215
216/// Helper function to combine a list of ids into a single string
217///
218/// The output should be json compatible
219pub fn coalesce_ids<T: Display>(task_ids: impl IntoIterator<Item = T> + Send) -> String {
220    use std::fmt::Write;
221    let mut ids = String::from("[");
222
223    for (i, id) in task_ids.into_iter().enumerate() {
224        if i != 0 {
225            ids.push(',');
226        }
227        write!(&mut ids, "\"{id}\"").unwrap();
228    }
229
230    ids.push(']');
231    ids
232}
233/// Trait for generating unique IDs
234pub trait GenerateId {
235    /// Generate a new unique ID
236    fn generate() -> TaskId;
237}
238
239#[cfg(feature = "uuid")]
240impl GenerateId for uuid::Uuid {
241    fn generate() -> TaskId {
242        TaskId::Uuid(Self::new_v4())
243    }
244}
245
246#[cfg(feature = "ulid")]
247impl GenerateId for ulid::Ulid {
248    fn generate() -> TaskId {
249        TaskId::Ulid(Self::generate())
250    }
251}
252
253impl GenerateId for RandomId {
254    fn generate() -> TaskId {
255        TaskId::from_string(Self::default())
256    }
257}
258
259impl GenerateId for u64 {
260    fn generate() -> TaskId {
261        TaskId::Int(rand::random::<Self>())
262    }
263}
264
265mod random_id {
266    use super::*;
267    use std::convert::Infallible;
268    use std::sync::atomic::{AtomicU64, Ordering};
269    use std::time::{SystemTime, UNIX_EPOCH};
270
271    const ALPHABET: &[u8] = b"abcdefghijkmnopqrstuvwxyz23456789-";
272    const BASE: u64 = 34;
273    const TIME_LEN: usize = 6;
274    const RANDOM_LEN: usize = 5;
275
276    /// A simple, unique, time-ordered ID (zero-deps).
277    ///
278    /// Consider using a ulid/uuid/nanoid in backend implementation
279    /// This is a placeholder and does not guarantee/tested as the other implementations
280    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
281    #[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
282    pub struct RandomId(String);
283
284    impl From<RandomId> for String {
285        fn from(value: RandomId) -> Self {
286            value.0
287        }
288    }
289
290    impl FromStr for RandomId {
291        type Err = Infallible;
292        fn from_str(s: &str) -> Result<Self, Self::Err> {
293            Ok(Self(s.to_owned()))
294        }
295    }
296
297    #[allow(clippy::infallible_try_from)]
298    impl TryFrom<&'_ str> for RandomId {
299        type Error = Infallible;
300
301        fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
302            Self::from_str(value)
303        }
304    }
305
306    impl Display for RandomId {
307        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308            Display::fmt(&self.0, f)
309        }
310    }
311
312    impl Default for RandomId {
313        fn default() -> Self {
314            Self(unique_id())
315        }
316    }
317
318    // Atomic counter to ensure uniqueness within same millisecond
319    static COUNTER: AtomicU64 = AtomicU64::new(0);
320
321    /// Converts a number to base-64 using the NanoID alphabet.
322    fn encode_base64(mut value: u64, length: usize) -> String {
323        let mut buf = vec![b'A'; length];
324        for i in (0..length).rev() {
325            buf[i] = ALPHABET[(value % BASE) as usize];
326            value /= BASE;
327        }
328        String::from_utf8(buf).unwrap()
329    }
330
331    /// Generates a unique, time-ordered NanoID-style string.
332    pub(super) fn unique_id() -> String {
333        let timestamp = current_time_millis();
334        let time_str = encode_base64(timestamp, TIME_LEN);
335
336        // Counter ensures uniqueness across fast calls
337        let count = COUNTER.fetch_add(1, Ordering::Relaxed);
338        let rand_part = encode_base64(rand::random::<u64>(), RANDOM_LEN);
339
340        format!("{time_str}{rand_part}{count}")
341    }
342
343    /// Returns current time in milliseconds since UNIX epoch.
344    fn current_time_millis() -> u64 {
345        SystemTime::now()
346            .duration_since(UNIX_EPOCH)
347            .unwrap_or_default()
348            .as_millis() as u64
349    }
350}