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}