use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
pub type JobId = Uuid;
pub type JobData = serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub enum JobPriority {
Low = 0,
#[default]
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobState {
Pending,
Processing,
Completed,
Failed,
Dead,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStatus {
pub state: JobState,
pub progress: u8,
pub message: Option<String>,
pub error: Option<String>,
pub updated_at: DateTime<Utc>,
}
impl JobStatus {
pub fn pending() -> Self {
Self {
state: JobState::Pending,
progress: 0,
message: None,
error: None,
updated_at: Utc::now(),
}
}
pub fn processing() -> Self {
Self {
state: JobState::Processing,
progress: 0,
message: None,
error: None,
updated_at: Utc::now(),
}
}
pub fn completed() -> Self {
Self {
state: JobState::Completed,
progress: 100,
message: None,
error: None,
updated_at: Utc::now(),
}
}
pub fn failed(error: String) -> Self {
Self {
state: JobState::Failed,
progress: 0,
message: None,
error: Some(error),
updated_at: Utc::now(),
}
}
pub fn dead(error: String) -> Self {
Self {
state: JobState::Dead,
progress: 0,
message: None,
error: Some(error),
updated_at: Utc::now(),
}
}
pub fn with_progress(mut self, progress: u8) -> Self {
self.progress = progress.min(100);
self.updated_at = Utc::now();
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self.updated_at = Utc::now();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub id: JobId,
pub job_type: String,
pub data: JobData,
pub priority: JobPriority,
pub status: JobStatus,
pub attempts: u32,
pub max_attempts: u32,
pub queue: String,
pub created_at: DateTime<Utc>,
pub scheduled_at: Option<DateTime<Utc>>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub metadata: HashMap<String, String>,
}
impl Job {
pub fn new(queue: impl Into<String>, job_type: impl Into<String>, data: JobData) -> Self {
Self {
id: Uuid::new_v4(),
job_type: job_type.into(),
data,
priority: JobPriority::default(),
status: JobStatus::pending(),
attempts: 0,
max_attempts: 3,
queue: queue.into(),
created_at: Utc::now(),
scheduled_at: None,
started_at: None,
completed_at: None,
metadata: HashMap::new(),
}
}
pub fn with_priority(mut self, priority: JobPriority) -> Self {
self.priority = priority;
self
}
pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
self.max_attempts = max_attempts;
self
}
pub fn schedule_at(mut self, time: DateTime<Utc>) -> Self {
self.scheduled_at = Some(time);
self
}
pub fn schedule_after(mut self, duration: chrono::Duration) -> Self {
self.scheduled_at = Some(Utc::now() + duration);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_ready(&self) -> bool {
if let Some(scheduled_at) = self.scheduled_at {
Utc::now() >= scheduled_at
} else {
true
}
}
pub fn can_retry(&self) -> bool {
self.attempts < self.max_attempts
}
pub fn start_processing(&mut self) {
self.status = JobStatus::processing();
self.started_at = Some(Utc::now());
self.attempts += 1;
}
pub fn complete(&mut self) {
self.status = JobStatus::completed();
self.completed_at = Some(Utc::now());
}
pub fn fail(&mut self, error: String) {
if self.can_retry() {
self.status = JobStatus::failed(error);
} else {
self.status = JobStatus::dead(error);
self.completed_at = Some(Utc::now());
}
}
pub fn update_progress(&mut self, progress: u8, message: Option<String>) {
self.status.progress = progress.min(100);
self.status.message = message;
self.status.updated_at = Utc::now();
}
pub fn backoff_delay(&self) -> chrono::Duration {
let exponent = self.attempts.saturating_sub(1).min(20);
let seconds = 2_i64.saturating_pow(exponent);
chrono::Duration::seconds(seconds.min(3600)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_creation() {
let job = Job::new(
"default",
"send_email",
serde_json::json!({"to": "test@example.com"}),
);
assert_eq!(job.queue, "default");
assert_eq!(job.job_type, "send_email");
assert_eq!(job.attempts, 0);
assert_eq!(job.priority, JobPriority::Normal);
}
#[test]
fn test_job_builder() {
let job = Job::new("default", "task", serde_json::json!({}))
.with_priority(JobPriority::High)
.with_max_attempts(5)
.with_metadata("user_id", "123");
assert_eq!(job.priority, JobPriority::High);
assert_eq!(job.max_attempts, 5);
assert_eq!(job.metadata.get("user_id"), Some(&"123".to_string()));
}
#[test]
fn test_job_ready() {
let mut job = Job::new("default", "task", serde_json::json!({}));
assert!(job.is_ready());
job = job.schedule_at(Utc::now() + chrono::Duration::hours(1));
assert!(!job.is_ready());
}
#[test]
fn test_job_retry_logic() {
let mut job = Job::new("default", "task", serde_json::json!({}));
job.max_attempts = 3;
assert!(job.can_retry());
job.start_processing();
job.fail("Error 1".to_string());
assert!(job.can_retry());
assert_eq!(job.status.state, JobState::Failed);
job.start_processing();
job.fail("Error 2".to_string());
assert!(job.can_retry());
job.start_processing();
job.fail("Error 3".to_string());
assert!(!job.can_retry());
assert_eq!(job.status.state, JobState::Dead);
}
#[test]
fn test_backoff_delay() {
let mut job = Job::new("default", "task", serde_json::json!({}));
job.attempts = 1;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(1));
job.attempts = 2;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(2));
job.attempts = 3;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(4));
job.attempts = 10;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(512));
}
#[test]
fn test_backoff_delay_large_attempts_does_not_panic() {
let mut job = Job::new("default", "task", serde_json::json!({}));
job.attempts = 100;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(3600));
job.attempts = u32::MAX;
assert_eq!(job.backoff_delay(), chrono::Duration::seconds(3600));
}
#[test]
fn test_job_priority_levels() {
let low =
Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Low);
let normal =
Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Normal);
let high =
Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::High);
let critical =
Job::new("default", "task", serde_json::json!({})).with_priority(JobPriority::Critical);
assert_eq!(low.priority, JobPriority::Low);
assert_eq!(normal.priority, JobPriority::Normal);
assert_eq!(high.priority, JobPriority::High);
assert_eq!(critical.priority, JobPriority::Critical);
}
#[test]
fn test_job_metadata() {
let job = Job::new("default", "task", serde_json::json!({}))
.with_metadata("key1", "value1")
.with_metadata("key2", "value2");
assert_eq!(job.metadata.len(), 2);
assert_eq!(job.metadata.get("key1"), Some(&"value1".to_string()));
assert_eq!(job.metadata.get("key2"), Some(&"value2".to_string()));
}
#[test]
fn test_job_schedule_at() {
let future = Utc::now() + chrono::Duration::hours(2);
let job = Job::new("default", "task", serde_json::json!({})).schedule_at(future);
assert!(!job.is_ready());
assert!(job.scheduled_at.is_some());
}
#[test]
fn test_job_scheduled_at_in_future() {
let future = Utc::now() + chrono::Duration::minutes(30);
let job = Job::new("default", "task", serde_json::json!({})).schedule_at(future);
assert!(!job.is_ready());
assert!(job.scheduled_at.is_some());
}
#[test]
fn test_job_status_transitions() {
let mut job = Job::new("default", "task", serde_json::json!({}));
assert_eq!(job.status.state, JobState::Pending);
job.start_processing();
assert_eq!(job.status.state, JobState::Processing);
job.complete();
assert_eq!(job.status.state, JobState::Completed);
}
#[test]
fn test_job_failure_tracking() {
let mut job = Job::new("default", "task", serde_json::json!({}));
job.start_processing();
job.fail("First error".to_string());
assert_eq!(job.status.state, JobState::Failed);
assert_eq!(job.status.error, Some("First error".to_string()));
assert_eq!(job.attempts, 1);
}
#[test]
fn test_job_max_attempts() {
let job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(10);
assert_eq!(job.max_attempts, 10);
}
#[test]
fn test_job_default_max_attempts() {
let job = Job::new("default", "task", serde_json::json!({}));
assert_eq!(job.max_attempts, 3);
}
#[test]
fn test_job_can_retry_with_zero_max_attempts() {
let mut job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(0);
job.start_processing();
job.fail("Error".to_string());
assert!(!job.can_retry());
}
#[test]
fn test_job_id_uniqueness() {
let job1 = Job::new("default", "task", serde_json::json!({}));
let job2 = Job::new("default", "task", serde_json::json!({}));
assert_ne!(job1.id, job2.id);
}
#[test]
fn test_job_timestamps() {
let before = Utc::now();
let job = Job::new("default", "task", serde_json::json!({}));
let after = Utc::now();
assert!(job.created_at >= before);
assert!(job.created_at <= after);
}
#[test]
fn test_job_complete_sets_state() {
let mut job = Job::new("default", "task", serde_json::json!({}));
job.start_processing();
job.complete();
assert_eq!(job.status.state, JobState::Completed);
}
#[test]
fn test_job_ready_with_past_schedule() {
let past = Utc::now() - chrono::Duration::hours(1);
let job = Job::new("default", "task", serde_json::json!({})).schedule_at(past);
assert!(job.is_ready());
}
#[test]
fn test_job_serialization_data() {
let data = serde_json::json!({
"email": "test@example.com",
"subject": "Test",
"count": 42
});
let job = Job::new("default", "send_email", data.clone());
assert_eq!(job.data, data);
}
#[test]
fn test_job_priority_ordering() {
assert!(JobPriority::Low < JobPriority::Normal);
assert!(JobPriority::Normal < JobPriority::High);
assert!(JobPriority::High < JobPriority::Critical);
}
#[test]
fn test_backoff_delay_exponential_growth() {
let mut job = Job::new("default", "task", serde_json::json!({}));
let delays: Vec<i64> = (1..=5)
.map(|attempt| {
job.attempts = attempt;
job.backoff_delay().num_seconds()
})
.collect();
assert!(delays[0] < delays[1]);
assert!(delays[1] < delays[2]);
assert!(delays[2] < delays[3]);
assert!(delays[3] < delays[4]);
}
#[test]
fn test_job_state_dead_after_max_retries() {
let mut job = Job::new("default", "task", serde_json::json!({})).with_max_attempts(2);
job.start_processing();
job.fail("Error 1".to_string());
assert_eq!(job.status.state, JobState::Failed);
job.start_processing();
job.fail("Error 2".to_string());
assert_eq!(job.status.state, JobState::Dead);
}
#[test]
fn test_job_metadata_overwrite() {
let job = Job::new("default", "task", serde_json::json!({}))
.with_metadata("key", "value1")
.with_metadata("key", "value2");
assert_eq!(job.metadata.get("key"), Some(&"value2".to_string()));
}
}