apalis_core/task/
task_id.rs

1//! Defines the `TaskId` type and related functionality.
2//!
3//! `TaskId` is a wrapper around a generic identifier type, providing type safety and utility methods for task identification.
4//!
5use std::{
6    fmt::{Debug, Display},
7    hash::Hash,
8    str::FromStr,
9};
10
11use crate::{
12    task::{data::MissingDataError, Task},
13    task_fn::FromRequest,
14};
15
16pub use random_id::RandomId;
17
18/// A wrapper type that defines a task id.
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
21pub struct TaskId<IdType = RandomId>(IdType);
22
23impl<IdType> TaskId<IdType> {
24    /// Generate a new [`TaskId`]
25    pub fn new(id: IdType) -> Self {
26        Self(id)
27    }
28    /// Get the inner value
29    pub fn inner(&self) -> &IdType {
30        &self.0
31    }
32}
33
34/// Errors that can occur when parsing a `TaskId` from a string
35#[derive(Debug, thiserror::Error)]
36pub enum TaskIdError<E> {
37    /// Decoding error
38    #[error("could not decode task_id: `{0}`")]
39    Decode(E),
40}
41
42impl<IdType: FromStr> FromStr for TaskId<IdType> {
43    type Err = TaskIdError<IdType::Err>;
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        Ok(TaskId::new(
46            IdType::from_str(s).map_err(|e| TaskIdError::Decode(e))?,
47        ))
48    }
49}
50
51impl<IdType: FromStr> TryFrom<&'_ str> for TaskId<IdType> {
52    type Error = TaskIdError<IdType::Err>;
53
54    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
55        Self::from_str(value)
56    }
57}
58
59impl<IdType: Display> Display for TaskId<IdType> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        Display::fmt(&self.0, f)
62    }
63}
64
65impl<Args: Sync, Ctx: Sync, IdType: Sync + Send + Clone> FromRequest<Task<Args, Ctx, IdType>>
66    for TaskId<IdType>
67{
68    type Error = MissingDataError;
69    async fn from_request(task: &Task<Args, Ctx, IdType>) -> Result<Self, Self::Error> {
70        Ok(task
71            .parts
72            .task_id
73            .clone()
74            .ok_or(MissingDataError::NotFound(
75                std::any::type_name::<TaskId<IdType>>().to_string(),
76            ))?)
77    }
78}
79
80mod random_id {
81    use super::*;
82    use std::convert::Infallible;
83    use std::sync::atomic::{AtomicU64, Ordering};
84    use std::time::{SystemTime, UNIX_EPOCH};
85
86    const ALPHABET: &[u8] = b"abcdefghijkmnopqrstuvwxyz23456789-";
87    const BASE: u64 = 34;
88    const TIME_LEN: usize = 6;
89    const RANDOM_LEN: usize = 5;
90
91    /// A simple, unique, time-ordered ID (zero-deps).
92    ///
93    /// Consider using a ulid/uuid/nanoid in backend implementation
94    /// This is a placeholder and does not guarantee/tested as the other implementations
95    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96    #[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
97    pub struct RandomId(String);
98
99    impl FromStr for RandomId {
100        type Err = Infallible;
101        fn from_str(s: &str) -> Result<Self, Self::Err> {
102            Ok(RandomId(s.to_owned()))
103        }
104    }
105
106    impl TryFrom<&'_ str> for RandomId {
107        type Error = Infallible;
108
109        fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
110            Self::from_str(value)
111        }
112    }
113
114    impl Display for RandomId {
115        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116            Display::fmt(&self.0, f)
117        }
118    }
119
120    impl Default for RandomId {
121        fn default() -> Self {
122            RandomId(unique_id())
123        }
124    }
125
126    // Atomic counter to ensure uniqueness within same millisecond
127    static COUNTER: AtomicU64 = AtomicU64::new(0);
128
129    /// Converts a number to base-64 using the NanoID alphabet.
130    fn encode_base64(mut value: u64, length: usize) -> String {
131        let mut buf = vec![b'A'; length];
132        for i in (0..length).rev() {
133            buf[i] = ALPHABET[(value % BASE) as usize];
134            value /= BASE;
135        }
136        String::from_utf8(buf).unwrap()
137    }
138
139    /// Generates a unique, time-ordered NanoID-style string (zero-deps).
140    pub(super) fn unique_id() -> String {
141        let timestamp = current_time_millis();
142        let time_str = encode_base64(timestamp, TIME_LEN);
143
144        // Counter ensures uniqueness across fast calls
145        let count = COUNTER.fetch_add(1, Ordering::Relaxed);
146        let rand_part = encode_base64(xorshift64(timestamp ^ count), RANDOM_LEN);
147
148        format!("{time_str}{rand_part}")
149    }
150
151    /// Returns current time in milliseconds since UNIX epoch.
152    fn current_time_millis() -> u64 {
153        SystemTime::now()
154            .duration_since(UNIX_EPOCH)
155            .unwrap_or_default()
156            .as_millis() as u64
157    }
158
159    /// Simple xorshift PRNG
160    fn xorshift64(mut x: u64) -> u64 {
161        x ^= x << 13;
162        x ^= x >> 7;
163        x ^= x << 17;
164        x
165    }
166}