use crate::error::{JobError, JobResult};
use crate::types::{JobId, JobPriority, JobStatus, RetryPolicy};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub id: JobId,
pub name: String,
pub description: Option<String>,
pub cron_expression: String,
pub timezone: String,
pub queue: String,
pub payload: serde_json::Value,
pub status: JobStatus,
pub priority: JobPriority,
pub timeout: u64,
pub max_attempts: u32,
pub retry_policy: RetryPolicy,
pub metadata: HashMap<String, String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_run_time: Option<DateTime<Utc>>,
pub next_run_time: Option<DateTime<Utc>>,
pub run_count: u32,
pub success_count: u32,
pub failure_count: u32,
}
impl Job {
pub fn new(
id: JobId,
name: String,
cron_expression: String,
queue: String,
payload: serde_json::Value,
) -> Self {
let now = Utc::now();
Self {
id,
name,
description: None,
cron_expression,
timezone: "UTC".to_string(),
queue,
payload,
status: JobStatus::Scheduled,
priority: JobPriority::Normal,
timeout: 300, max_attempts: 5,
retry_policy: RetryPolicy::default(),
metadata: HashMap::new(),
created_at: now,
updated_at: now,
last_run_time: None,
next_run_time: None,
run_count: 0,
success_count: 0,
failure_count: 0,
}
}
pub fn is_ready_to_run(&self) -> bool {
if !self.is_scheduled() {
return false;
}
if let Some(next_run) = self.next_run_time {
Utc::now() >= next_run
} else {
false
}
}
pub fn is_scheduled(&self) -> bool {
self.status == JobStatus::Scheduled
}
pub fn is_running(&self) -> bool {
self.status == JobStatus::Running
}
pub fn can_retry(&self) -> bool {
self.status == JobStatus::Failed && self.failure_count < self.max_attempts
}
pub fn success_rate(&self) -> f64 {
if self.run_count == 0 {
0.0
} else {
(self.success_count as f64 / self.run_count as f64) * 100.0
}
}
pub fn mark_as_running(&mut self) -> JobResult<()> {
if self.status != JobStatus::Scheduled {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"running",
));
}
self.status = JobStatus::Running;
self.updated_at = Utc::now();
Ok(())
}
pub fn mark_as_completed(&mut self) -> JobResult<()> {
if self.status != JobStatus::Running {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"completed",
));
}
self.status = JobStatus::Completed;
self.last_run_time = Some(Utc::now());
self.updated_at = Utc::now();
self.run_count += 1;
self.success_count += 1;
Ok(())
}
pub fn mark_as_failed(&mut self) -> JobResult<()> {
if self.status != JobStatus::Running {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"failed",
));
}
self.status = JobStatus::Failed;
self.last_run_time = Some(Utc::now());
self.updated_at = Utc::now();
self.run_count += 1;
self.failure_count += 1;
Ok(())
}
pub fn reset_for_retry(&mut self) -> JobResult<()> {
if !matches!(self.status, JobStatus::Failed | JobStatus::Completed) {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"scheduled",
));
}
self.status = JobStatus::Scheduled;
self.updated_at = Utc::now();
Ok(())
}
pub fn cancel(&mut self) -> JobResult<()> {
if self.status == JobStatus::Completed || self.status == JobStatus::Cancelled {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"cancelled",
));
}
self.status = JobStatus::Cancelled;
self.updated_at = Utc::now();
Ok(())
}
pub fn pause(&mut self) -> JobResult<()> {
if self.status == JobStatus::Completed || self.status == JobStatus::Cancelled {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"paused",
));
}
self.status = JobStatus::Paused;
self.updated_at = Utc::now();
Ok(())
}
pub fn resume(&mut self) -> JobResult<()> {
if self.status != JobStatus::Paused {
return Err(JobError::invalid_status_transition(
&self.status.to_string(),
"scheduled",
));
}
self.status = JobStatus::Scheduled;
self.updated_at = Utc::now();
Ok(())
}
pub fn set_next_run_time(&mut self, next_run: DateTime<Utc>) {
self.next_run_time = Some(next_run);
self.updated_at = Utc::now();
}
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
self.updated_at = Utc::now();
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
pub fn validate(&self) -> JobResult<()> {
if self.cron_expression.is_empty() {
return Err(JobError::validation("Cron expression cannot be empty"));
}
let parts: Vec<&str> = self.cron_expression.split_whitespace().collect();
if parts.len() != 5 {
return Err(JobError::validation(
"Cron expression must have exactly 5 fields",
));
}
if self.queue.is_empty() {
return Err(JobError::validation("Queue name cannot be empty"));
}
if self.timeout == 0 {
return Err(JobError::validation("Timeout must be greater than 0"));
}
if self.max_attempts == 0 {
return Err(JobError::validation("Max attempts must be greater than 0"));
}
Ok(())
}
}
pub struct JobBuilder {
id: Option<JobId>,
name: String,
description: Option<String>,
cron_expression: String,
timezone: String,
queue: String,
payload: serde_json::Value,
priority: JobPriority,
timeout: u64,
max_attempts: u32,
retry_policy: RetryPolicy,
metadata: HashMap<String, String>,
}
impl JobBuilder {
pub fn new() -> Self {
Self {
id: None,
name: String::new(),
description: None,
cron_expression: String::new(),
timezone: "UTC".to_string(),
queue: String::new(),
payload: serde_json::Value::Null,
priority: JobPriority::Normal,
timeout: 300,
max_attempts: 5,
retry_policy: RetryPolicy::default(),
metadata: HashMap::new(),
}
}
pub fn id(mut self, id: impl Into<JobId>) -> Self {
self.id = Some(id.into());
self
}
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = name.into();
self
}
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
pub fn cron<S: Into<String>>(mut self, cron_expression: S) -> Self {
self.cron_expression = cron_expression.into();
self
}
pub fn timezone<S: Into<String>>(mut self, timezone: S) -> Self {
self.timezone = timezone.into();
self
}
pub fn queue<S: Into<String>>(mut self, queue: S) -> Self {
self.queue = queue.into();
self
}
pub fn payload<T: Into<serde_json::Value>>(mut self, payload: T) -> Self {
self.payload = payload.into();
self
}
pub fn priority(mut self, priority: JobPriority) -> Self {
self.priority = priority;
self
}
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
pub fn max_attempts(mut self, max_attempts: u32) -> Self {
self.max_attempts = max_attempts;
self
}
pub fn retry_policy(mut self, retry_policy: RetryPolicy) -> Self {
self.retry_policy = retry_policy;
self
}
pub fn metadata<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata.extend(metadata);
self
}
pub fn build(self) -> JobResult<Job> {
let id = self.id.unwrap_or_default();
let mut job = Job::new(id, self.name, self.cron_expression, self.queue, self.payload);
job.description = self.description;
job.timezone = self.timezone;
job.priority = self.priority;
job.timeout = self.timeout;
job.max_attempts = self.max_attempts;
job.retry_policy = self.retry_policy;
job.metadata = self.metadata;
job.validate()?;
Ok(job)
}
}
impl Default for JobBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_job_builder() {
let job = JobBuilder::new()
.name("Test Job")
.description("A test job")
.cron("0 12 * * *")
.queue("test_queue")
.payload(json!({"test": true}))
.priority(JobPriority::High)
.timeout(600)
.max_attempts(3)
.metadata("env", "test")
.build()
.unwrap();
assert_eq!(job.name, "Test Job");
assert_eq!(job.description, Some("A test job".to_string()));
assert_eq!(job.cron_expression, "0 12 * * *");
assert_eq!(job.queue, "test_queue");
assert_eq!(job.priority, JobPriority::High);
assert_eq!(job.timeout, 600);
assert_eq!(job.max_attempts, 3);
assert_eq!(job.get_metadata("env"), Some(&"test".to_string()));
}
#[test]
fn test_job_validation() {
let mut job = Job::new(
JobId::new(),
"Test".to_string(),
"invalid cron".to_string(),
"test".to_string(),
json!({}),
);
assert!(job.validate().is_err());
job.cron_expression = "0 12 * * *".to_string();
job.queue = "".to_string(); assert!(job.validate().is_err());
job.queue = "test_queue".to_string();
assert!(job.validate().is_ok());
}
#[test]
fn test_job_state_transitions() {
let mut job = Job::new(
JobId::new(),
"Test".to_string(),
"0 12 * * *".to_string(),
"test".to_string(),
json!({}),
);
assert!(job.mark_as_running().is_ok());
assert_eq!(job.status, JobStatus::Running);
assert!(job.mark_as_running().is_err());
assert!(job.mark_as_completed().is_ok());
assert_eq!(job.status, JobStatus::Completed);
assert_eq!(job.run_count, 1);
assert_eq!(job.success_count, 1);
assert!(job.mark_as_completed().is_err());
}
#[test]
fn test_job_retry_logic() {
let mut job = Job::new(
JobId::new(),
"Test".to_string(),
"0 12 * * *".to_string(),
"test".to_string(),
json!({}),
);
job.max_attempts = 3;
job.mark_as_running().unwrap();
job.mark_as_failed().unwrap();
assert!(job.can_retry());
job.reset_for_retry().unwrap();
job.mark_as_running().unwrap();
job.mark_as_failed().unwrap();
assert!(job.can_retry());
job.reset_for_retry().unwrap();
job.mark_as_running().unwrap();
job.mark_as_failed().unwrap();
assert!(!job.can_retry());
}
#[test]
fn test_success_rate() {
let mut job = Job::new(
JobId::new(),
"Test".to_string(),
"0 12 * * *".to_string(),
"test".to_string(),
json!({}),
);
assert_eq!(job.success_rate(), 0.0);
job.mark_as_running().unwrap();
job.mark_as_completed().unwrap();
assert_eq!(job.success_rate(), 100.0);
job.reset_for_retry().unwrap();
job.mark_as_running().unwrap();
job.mark_as_failed().unwrap();
assert_eq!(job.success_rate(), 50.0); }
}