acton_htmx/jobs/
job.rs

1//! Core job trait and types.
2
3use super::JobResult;
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::time::Duration;
8use uuid::Uuid;
9
10/// Unique identifier for a job.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct JobId(Uuid);
13
14impl JobId {
15    /// Create a new random job ID.
16    #[must_use]
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20
21    /// Get the underlying UUID.
22    #[must_use]
23    pub const fn as_uuid(&self) -> &Uuid {
24        &self.0
25    }
26}
27
28impl Default for JobId {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl fmt::Display for JobId {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{}", self.0)
37    }
38}
39
40impl From<Uuid> for JobId {
41    fn from(uuid: Uuid) -> Self {
42        Self(uuid)
43    }
44}
45
46impl From<JobId> for Uuid {
47    fn from(id: JobId) -> Self {
48        id.0
49    }
50}
51
52/// A background job that can be executed asynchronously.
53///
54/// # Type Parameters
55///
56/// Jobs are generic over their result type, which must be `Send + Sync`.
57///
58/// # Example
59///
60/// ```rust
61/// use acton_htmx::jobs::{Job, JobContext, JobResult};
62/// use async_trait::async_trait;
63/// use serde::{Deserialize, Serialize};
64///
65/// #[derive(Debug, Clone, Serialize, Deserialize)]
66/// pub struct SendEmailJob {
67///     to: String,
68///     subject: String,
69///     body: String,
70/// }
71///
72/// #[async_trait]
73/// impl Job for SendEmailJob {
74///     type Result = ();
75///
76///     async fn execute(&self, ctx: &JobContext) -> JobResult<Self::Result> {
77///         // Send email using ctx.email_client
78///         println!("Sending email to: {}", self.to);
79///         Ok(())
80///     }
81///
82///     fn max_retries(&self) -> u32 {
83///         5 // Retry up to 5 times for transient failures
84///     }
85///
86///     fn timeout(&self) -> Duration {
87///         Duration::from_secs(30) // 30 second timeout
88///     }
89/// }
90/// ```
91// Note: Job trait cannot use #[automock] due to associated type `Result`
92// without default value. Use manual mocks or concrete test implementations instead.
93#[async_trait]
94pub trait Job: Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static {
95    /// The result type returned by this job.
96    type Result: Send + Sync;
97
98    /// Execute the job.
99    ///
100    /// Jobs receive a [`JobContext`](crate::jobs::JobContext) providing access to:
101    /// - Email sender for sending transactional emails
102    /// - Database pool for queries
103    /// - File storage for file operations
104    /// - Redis pool for caching (optional, feature-gated)
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the job execution fails. The job will be retried
109    /// according to `max_retries()` with exponential backoff.
110    async fn execute(&self, ctx: &super::JobContext) -> JobResult<Self::Result>;
111
112    /// Maximum number of retry attempts.
113    ///
114    /// Default: 3 retries
115    fn max_retries(&self) -> u32 {
116        3
117    }
118
119    /// Timeout for job execution.
120    ///
121    /// If the job takes longer than this duration, it will be cancelled
122    /// and marked as failed.
123    ///
124    /// Default: 5 minutes
125    fn timeout(&self) -> Duration {
126        Duration::from_secs(300)
127    }
128
129    /// Priority for job execution (higher = more important).
130    ///
131    /// Jobs with higher priority will be executed before lower priority jobs
132    /// when multiple jobs are queued.
133    ///
134    /// Default: 0 (normal priority)
135    fn priority(&self) -> i32 {
136        0
137    }
138
139    /// Job type name for logging and debugging.
140    ///
141    /// Default: Returns the type name
142    fn job_type(&self) -> &'static str {
143        std::any::type_name::<Self>()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_job_id_creation() {
153        let id1 = JobId::new();
154        let id2 = JobId::new();
155        assert_ne!(id1, id2);
156    }
157
158    #[test]
159    fn test_job_id_display() {
160        let id = JobId::new();
161        let display = format!("{id}");
162        assert!(!display.is_empty());
163        assert!(Uuid::parse_str(&display).is_ok());
164    }
165
166    #[test]
167    fn test_job_id_uuid_conversion() {
168        let uuid = Uuid::new_v4();
169        let job_id = JobId::from(uuid);
170        let converted: Uuid = job_id.into();
171        assert_eq!(uuid, converted);
172    }
173}