use super::schedule::Schedule;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum JobStatus {
#[default]
Pending,
Running,
Completed,
Failed,
Cancelled,
Paused,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job<S: Schedule + Clone> {
pub name: String,
pub description: Option<String>,
pub status: JobStatus,
#[serde(skip)]
pub schedule: Option<S>,
pub run_count: u64,
pub last_run_at: Option<u64>,
pub last_run_duration_ms: Option<u64>,
pub last_error: Option<String>,
#[serde(default)]
pub metadata: HashMap<String, String>,
pub created_at: u64,
#[serde(default)]
pub tags: Vec<String>,
}
impl<S: Schedule + Clone> Job<S> {
pub fn new(name: impl Into<String>, schedule: S) -> Self {
Self {
name: name.into(),
description: None,
status: JobStatus::Pending,
schedule: Some(schedule),
run_count: 0,
last_run_at: None,
last_run_duration_ms: None,
last_error: None,
metadata: HashMap::new(),
created_at: now_millis(),
tags: Vec::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn should_run(&self) -> bool {
if self.status == JobStatus::Paused || self.status == JobStatus::Cancelled {
return false;
}
self.schedule
.as_ref()
.map(|s| s.should_run())
.unwrap_or(false)
}
pub fn next_run(&self) -> Option<u64> {
self.schedule.as_ref().and_then(|s| s.next_run())
}
pub fn is_exhausted(&self) -> bool {
self.schedule
.as_ref()
.map(|s| s.is_exhausted())
.unwrap_or(true)
}
pub fn mark_started(&mut self) {
self.status = JobStatus::Running;
}
pub fn mark_completed(&mut self, duration_ms: u64) {
self.status = JobStatus::Pending; self.run_count += 1;
self.last_run_at = Some(now_millis());
self.last_run_duration_ms = Some(duration_ms);
self.last_error = None;
if let Some(schedule) = &mut self.schedule {
schedule.advance();
}
if self.is_exhausted() {
self.status = JobStatus::Completed;
}
}
pub fn mark_failed(&mut self, error: impl Into<String>) {
self.status = JobStatus::Failed;
self.last_run_at = Some(now_millis());
self.last_error = Some(error.into());
if let Some(schedule) = &mut self.schedule {
schedule.advance();
}
}
pub fn pause(&mut self) {
self.status = JobStatus::Paused;
}
pub fn resume(&mut self) {
if self.status == JobStatus::Paused {
self.status = JobStatus::Pending;
}
}
pub fn cancel(&mut self) {
self.status = JobStatus::Cancelled;
}
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
pub struct JobBuilder<S: Schedule + Clone> {
name: String,
schedule: S,
description: Option<String>,
metadata: HashMap<String, String>,
tags: Vec<String>,
}
impl<S: Schedule + Clone> JobBuilder<S> {
pub fn new(name: impl Into<String>, schedule: S) -> Self {
Self {
name: name.into(),
schedule,
description: None,
metadata: HashMap::new(),
tags: Vec::new(),
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tags.extend(tags.into_iter().map(|t| t.into()));
self
}
pub fn build(self) -> Job<S> {
let mut job = Job::new(self.name, self.schedule);
job.description = self.description;
job.metadata = self.metadata;
job.tags = self.tags;
job
}
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::super::schedule::{IntervalSchedule, OnceSchedule};
use super::*;
use std::time::Duration;
#[test]
fn test_job_new() {
let schedule = OnceSchedule::now();
let job = Job::new("test_job", schedule);
assert_eq!(job.name, "test_job");
assert_eq!(job.status, JobStatus::Pending);
assert_eq!(job.run_count, 0);
}
#[test]
fn test_job_with_description() {
let schedule = OnceSchedule::now();
let job = Job::new("test_job", schedule).description("A test job");
assert_eq!(job.description, Some("A test job".to_string()));
}
#[test]
fn test_job_should_run() {
let schedule = OnceSchedule::now();
let job = Job::new("test", schedule);
assert!(job.should_run());
}
#[test]
fn test_job_paused_should_not_run() {
let schedule = OnceSchedule::now();
let mut job = Job::new("test", schedule);
job.pause();
assert!(!job.should_run());
}
#[test]
fn test_job_mark_completed() {
let schedule = IntervalSchedule::new(Duration::from_secs(60));
let mut job = Job::new("test", schedule);
job.mark_started();
assert_eq!(job.status, JobStatus::Running);
job.mark_completed(100);
assert_eq!(job.status, JobStatus::Pending);
assert_eq!(job.run_count, 1);
assert_eq!(job.last_run_duration_ms, Some(100));
}
#[test]
fn test_job_mark_failed() {
let schedule = OnceSchedule::now();
let mut job = Job::new("test", schedule);
job.mark_failed("Something went wrong");
assert_eq!(job.status, JobStatus::Failed);
assert_eq!(job.last_error, Some("Something went wrong".to_string()));
}
#[test]
fn test_job_exhausted() {
let schedule = OnceSchedule::now();
let mut job = Job::new("test", schedule);
job.mark_completed(10);
assert!(job.is_exhausted());
assert_eq!(job.status, JobStatus::Completed);
}
#[test]
fn test_job_pause_resume() {
let schedule = OnceSchedule::now();
let mut job = Job::new("test", schedule);
job.pause();
assert_eq!(job.status, JobStatus::Paused);
job.resume();
assert_eq!(job.status, JobStatus::Pending);
}
#[test]
fn test_job_cancel() {
let schedule = OnceSchedule::now();
let mut job = Job::new("test", schedule);
job.cancel();
assert_eq!(job.status, JobStatus::Cancelled);
assert!(!job.should_run());
}
#[test]
fn test_job_tags() {
let schedule = OnceSchedule::now();
let job = Job::new("test", schedule).tag("important").tag("daily");
assert!(job.has_tag("important"));
assert!(job.has_tag("daily"));
assert!(!job.has_tag("weekly"));
}
#[test]
fn test_job_builder() {
let schedule = OnceSchedule::now();
let job = JobBuilder::new("my_job", schedule)
.description("Test description")
.metadata("key", "value")
.tag("test")
.build();
assert_eq!(job.name, "my_job");
assert_eq!(job.description, Some("Test description".to_string()));
assert_eq!(job.metadata.get("key"), Some(&"value".to_string()));
assert!(job.has_tag("test"));
}
#[test]
fn test_job_builder_multiple_tags() {
let schedule = OnceSchedule::now();
let job = JobBuilder::new("job", schedule)
.tags(vec!["a", "b", "c"])
.build();
assert_eq!(job.tags.len(), 3);
}
}