1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8pub type JobId = Uuid;
10
11pub type JobData = serde_json::Value;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
16pub enum JobPriority {
17 Low = 0,
19 #[default]
21 Normal = 1,
22 High = 2,
24 Critical = 3,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum JobState {
31 Pending,
33 Processing,
35 Completed,
37 Failed,
39 Dead,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct JobStatus {
46 pub state: JobState,
48
49 pub progress: u8,
51
52 pub message: Option<String>,
54
55 pub error: Option<String>,
57
58 pub updated_at: DateTime<Utc>,
60}
61
62impl JobStatus {
63 pub fn pending() -> Self {
65 Self {
66 state: JobState::Pending,
67 progress: 0,
68 message: None,
69 error: None,
70 updated_at: Utc::now(),
71 }
72 }
73
74 pub fn processing() -> Self {
76 Self {
77 state: JobState::Processing,
78 progress: 0,
79 message: None,
80 error: None,
81 updated_at: Utc::now(),
82 }
83 }
84
85 pub fn completed() -> Self {
87 Self {
88 state: JobState::Completed,
89 progress: 100,
90 message: None,
91 error: None,
92 updated_at: Utc::now(),
93 }
94 }
95
96 pub fn failed(error: String) -> Self {
98 Self {
99 state: JobState::Failed,
100 progress: 0,
101 message: None,
102 error: Some(error),
103 updated_at: Utc::now(),
104 }
105 }
106
107 pub fn dead(error: String) -> Self {
109 Self {
110 state: JobState::Dead,
111 progress: 0,
112 message: None,
113 error: Some(error),
114 updated_at: Utc::now(),
115 }
116 }
117
118 pub fn with_progress(mut self, progress: u8) -> Self {
120 self.progress = progress.min(100);
121 self.updated_at = Utc::now();
122 self
123 }
124
125 pub fn with_message(mut self, message: impl Into<String>) -> Self {
127 self.message = Some(message.into());
128 self.updated_at = Utc::now();
129 self
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Job {
136 pub id: JobId,
138
139 pub job_type: String,
141
142 pub data: JobData,
144
145 pub priority: JobPriority,
147
148 pub status: JobStatus,
150
151 pub attempts: u32,
153
154 pub max_attempts: u32,
156
157 pub queue: String,
159
160 pub created_at: DateTime<Utc>,
162
163 pub scheduled_at: Option<DateTime<Utc>>,
165
166 pub started_at: Option<DateTime<Utc>>,
168
169 pub completed_at: Option<DateTime<Utc>>,
171
172 pub metadata: HashMap<String, String>,
174}
175
176impl Job {
177 pub fn new(queue: impl Into<String>, job_type: impl Into<String>, data: JobData) -> Self {
179 Self {
180 id: Uuid::new_v4(),
181 job_type: job_type.into(),
182 data,
183 priority: JobPriority::default(),
184 status: JobStatus::pending(),
185 attempts: 0,
186 max_attempts: 3,
187 queue: queue.into(),
188 created_at: Utc::now(),
189 scheduled_at: None,
190 started_at: None,
191 completed_at: None,
192 metadata: HashMap::new(),
193 }
194 }
195
196 pub fn with_priority(mut self, priority: JobPriority) -> Self {
198 self.priority = priority;
199 self
200 }
201
202 pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
204 self.max_attempts = max_attempts;
205 self
206 }
207
208 pub fn schedule_at(mut self, time: DateTime<Utc>) -> Self {
210 self.scheduled_at = Some(time);
211 self
212 }
213
214 pub fn schedule_after(mut self, duration: chrono::Duration) -> Self {
216 self.scheduled_at = Some(Utc::now() + duration);
217 self
218 }
219
220 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
222 self.metadata.insert(key.into(), value.into());
223 self
224 }
225
226 pub fn is_ready(&self) -> bool {
228 if let Some(scheduled_at) = self.scheduled_at {
229 Utc::now() >= scheduled_at
230 } else {
231 true
232 }
233 }
234
235 pub fn can_retry(&self) -> bool {
237 self.attempts < self.max_attempts
238 }
239
240 pub fn start_processing(&mut self) {
242 self.status = JobStatus::processing();
243 self.started_at = Some(Utc::now());
244 self.attempts += 1;
245 }
246
247 pub fn complete(&mut self) {
249 self.status = JobStatus::completed();
250 self.completed_at = Some(Utc::now());
251 }
252
253 pub fn fail(&mut self, error: String) {
255 if self.can_retry() {
256 self.status = JobStatus::failed(error);
257 } else {
258 self.status = JobStatus::dead(error);
259 self.completed_at = Some(Utc::now());
260 }
261 }
262
263 pub fn update_progress(&mut self, progress: u8, message: Option<String>) {
265 self.status.progress = progress.min(100);
266 self.status.message = message;
267 self.status.updated_at = Utc::now();
268 }
269
270 pub fn backoff_delay(&self) -> chrono::Duration {
272 let exponent = self.attempts.saturating_sub(1).min(20);
281 let seconds = 2_i64.saturating_pow(exponent);
282 chrono::Duration::seconds(seconds.min(3600)) }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_job_creation() {
292 let job = Job::new(
293 "default",
294 "send_email",
295 serde_json::json!({"to": "test@example.com"}),
296 );
297
298 assert_eq!(job.queue, "default");
299 assert_eq!(job.job_type, "send_email");
300 assert_eq!(job.attempts, 0);
301 assert_eq!(job.priority, JobPriority::Normal);
302 }
303
304 #[test]
305 fn test_job_builder() {
306 let job = Job::new("default", "task", serde_json::json!({}))
307 .with_priority(JobPriority::High)
308 .with_max_attempts(5)
309 .with_metadata("user_id", "123");
310
311 assert_eq!(job.priority, JobPriority::High);
312 assert_eq!(job.max_attempts, 5);
313 assert_eq!(job.metadata.get("user_id"), Some(&"123".to_string()));
314 }
315
316 #[test]
317 fn test_job_ready() {
318 let mut job = Job::new("default", "task", serde_json::json!({}));
319 assert!(job.is_ready());
320
321 job = job.schedule_at(Utc::now() + chrono::Duration::hours(1));
322 assert!(!job.is_ready());
323 }
324
325 #[test]
326 fn test_job_retry_logic() {
327 let mut job = Job::new("default", "task", serde_json::json!({}));
328 job.max_attempts = 3;
329
330 assert!(job.can_retry());
331
332 job.start_processing();
333 job.fail("Error 1".to_string());
334 assert!(job.can_retry());
335 assert_eq!(job.status.state, JobState::Failed);
336
337 job.start_processing();
338 job.fail("Error 2".to_string());
339 assert!(job.can_retry());
340
341 job.start_processing();
342 job.fail("Error 3".to_string());
343 assert!(!job.can_retry());
344 assert_eq!(job.status.state, JobState::Dead);
345 }
346
347 #[test]
348 fn test_backoff_delay() {
349 let mut job = Job::new("default", "task", serde_json::json!({}));
350
351 job.attempts = 1;
352 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(1));
353
354 job.attempts = 2;
355 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(2));
356
357 job.attempts = 3;
358 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(4));
359
360 job.attempts = 10;
361 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(512));
362 }
363
364 #[test]
365 fn test_backoff_delay_large_attempts_does_not_panic() {
366 let mut job = Job::new("default", "task", serde_json::json!({}));
371
372 job.attempts = 100;
373 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(3600));
374
375 job.attempts = u32::MAX;
376 assert_eq!(job.backoff_delay(), chrono::Duration::seconds(3600));
377 }
378
379 #[test]
380 fn test_job_priority_levels() {
381 let low =
382 Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Low);
383 let normal =
384 Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Normal);
385 let high =
386 Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::High);
387 let critical =
388 Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Critical);
389
390 assert_eq!(low.priority, JobPriority::Low);
391 assert_eq!(normal.priority, JobPriority::Normal);
392 assert_eq!(high.priority, JobPriority::High);
393 assert_eq!(critical.priority, JobPriority::Critical);
394 }
395
396 #[test]
397 fn test_job_metadata() {
398 let job = Job::new("default", "task", serde_json::json!({}))
399 .with_metadata("key1", "value1")
400 .with_metadata("key2", "value2");
401
402 assert_eq!(job.metadata.len(), 2);
403 assert_eq!(job.metadata.get("key1"), Some(&"value1".to_string()));
404 assert_eq!(job.metadata.get("key2"), Some(&"value2".to_string()));
405 }
406
407 #[test]
408 fn test_job_schedule_at() {
409 let future = Utc::now() + chrono::Duration::hours(2);
410 let job = Job::new("default", "task", serde_json::json!({})).schedule_at(future);
411
412 assert!(!job.is_ready());
413 assert!(job.scheduled_at.is_some());
414 }
415
416 #[test]
417 fn test_job_scheduled_at_in_future() {
418 let future = Utc::now() + chrono::Duration::minutes(30);
419 let job = Job::new("default", "task", serde_json::json!({})).schedule_at(future);
420
421 assert!(!job.is_ready());
422 assert!(job.scheduled_at.is_some());
423 }
424
425 #[test]
426 fn test_job_status_transitions() {
427 let mut job = Job::new("default", "task", serde_json::json!({}));
428
429 assert_eq!(job.status.state, JobState::Pending);
430
431 job.start_processing();
432 assert_eq!(job.status.state, JobState::Processing);
433
434 job.complete();
435 assert_eq!(job.status.state, JobState::Completed);
436 }
437
438 #[test]
439 fn test_job_failure_tracking() {
440 let mut job = Job::new("default", "task", serde_json::json!({}));
441
442 job.start_processing();
443 job.fail("First error".to_string());
444
445 assert_eq!(job.status.state, JobState::Failed);
446 assert_eq!(job.status.error, Some("First error".to_string()));
447 assert_eq!(job.attempts, 1);
448 }
449
450 #[test]
451 fn test_job_max_attempts() {
452 let job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(10);
453
454 assert_eq!(job.max_attempts, 10);
455 }
456
457 #[test]
458 fn test_job_default_max_attempts() {
459 let job = Job::new("default", "task", serde_json::json!({}));
460 assert_eq!(job.max_attempts, 3);
461 }
462
463 #[test]
464 fn test_job_can_retry_with_zero_max_attempts() {
465 let mut job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(0);
466
467 job.start_processing();
468 job.fail("Error".to_string());
469
470 assert!(!job.can_retry());
471 }
472
473 #[test]
474 fn test_job_id_uniqueness() {
475 let job1 = Job::new("default", "task", serde_json::json!({}));
476 let job2 = Job::new("default", "task", serde_json::json!({}));
477
478 assert_ne!(job1.id, job2.id);
479 }
480
481 #[test]
482 fn test_job_timestamps() {
483 let before = Utc::now();
484 let job = Job::new("default", "task", serde_json::json!({}));
485 let after = Utc::now();
486
487 assert!(job.created_at >= before);
488 assert!(job.created_at <= after);
489 }
490
491 #[test]
492 fn test_job_complete_sets_state() {
493 let mut job = Job::new("default", "task", serde_json::json!({}));
494
495 job.start_processing();
496 job.complete();
497
498 assert_eq!(job.status.state, JobState::Completed);
499 }
500
501 #[test]
502 fn test_job_ready_with_past_schedule() {
503 let past = Utc::now() - chrono::Duration::hours(1);
504 let job = Job::new("default", "task", serde_json::json!({})).schedule_at(past);
505
506 assert!(job.is_ready());
507 }
508
509 #[test]
510 fn test_job_serialization_data() {
511 let data = serde_json::json!({
512 "email": "test@example.com",
513 "subject": "Test",
514 "count": 42
515 });
516
517 let job = Job::new("default", "send_email", data.clone());
518 assert_eq!(job.data, data);
519 }
520
521 #[test]
522 fn test_job_priority_ordering() {
523 assert!(JobPriority::Low < JobPriority::Normal);
524 assert!(JobPriority::Normal < JobPriority::High);
525 assert!(JobPriority::High < JobPriority::Critical);
526 }
527
528 #[test]
529 fn test_backoff_delay_exponential_growth() {
530 let mut job = Job::new("default", "task", serde_json::json!({}));
531
532 let delays: Vec<i64> = (1..=5)
533 .map(|attempt| {
534 job.attempts = attempt;
535 job.backoff_delay().num_seconds()
536 })
537 .collect();
538
539 assert!(delays[0] < delays[1]);
541 assert!(delays[1] < delays[2]);
542 assert!(delays[2] < delays[3]);
543 assert!(delays[3] < delays[4]);
544 }
545
546 #[test]
547 fn test_job_state_dead_after_max_retries() {
548 let mut job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(2);
549
550 job.start_processing();
551 job.fail("Error 1".to_string());
552 assert_eq!(job.status.state, JobState::Failed);
553
554 job.start_processing();
555 job.fail("Error 2".to_string());
556 assert_eq!(job.status.state, JobState::Dead);
557 }
558
559 #[test]
560 fn test_job_metadata_overwrite() {
561 let job = Job::new("default", "task", serde_json::json!({}))
562 .with_metadata("key", "value1")
563 .with_metadata("key", "value2");
564
565 assert_eq!(job.metadata.get("key"), Some(&"value2".to_string()));
566 }
567}