use crate::{CronError, CronResult};
use chrono::{DateTime, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum MisfirePolicy {
#[default]
Skip,
FireOnce,
FireAll,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum RetryPolicy {
#[default]
None,
Fixed {
max_retries: usize,
interval: Duration,
},
Exponential {
max_retries: usize,
initial_interval: Duration,
max_interval: Duration,
},
}
#[derive(Debug, Clone)]
pub enum JobEvent {
Started { id: String, name: String },
Completed {
id: String,
name: String,
duration: Duration,
},
Failed {
id: String,
name: String,
error: String,
},
Retrying {
id: String,
name: String,
attempt: usize,
delay: Duration,
},
Misfired {
id: String,
name: String,
scheduled_time: DateTime<Utc>,
},
}
#[async_trait::async_trait]
pub trait JobEventListener: Send + Sync {
async fn on_event(&self, event: JobEvent);
}
pub trait MetricsExporter: Send + Sync {
fn record_start(&self, id: &str, name: &str);
fn record_completion(&self, id: &str, name: &str, duration: Duration);
fn record_failure(&self, id: &str, name: &str);
fn record_retry(&self, id: &str, name: &str);
fn record_misfire(&self, id: &str, name: &str);
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct JobState {
pub last_run: Option<DateTime<Utc>>,
pub last_success: Option<DateTime<Utc>>,
pub last_failure: Option<DateTime<Utc>>,
pub consecutive_failures: usize,
}
#[async_trait::async_trait]
pub trait JobStore: Send + Sync {
async fn save_state(&self, id: &str, state: &JobState) -> CronResult<()>;
async fn get_state(&self, id: &str) -> CronResult<Option<JobState>>;
}
#[derive(Default)]
pub struct InMemoryJobStore {
states: Arc<RwLock<HashMap<String, JobState>>>,
}
impl InMemoryJobStore {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait::async_trait]
impl JobStore for InMemoryJobStore {
async fn save_state(&self, id: &str, state: &JobState) -> CronResult<()> {
let mut states = self.states.write().await;
states.insert(id.to_string(), state.clone());
Ok(())
}
async fn get_state(&self, id: &str) -> CronResult<Option<JobState>> {
let states = self.states.read().await;
Ok(states.get(id).cloned())
}
}
#[derive(Debug, Clone)]
pub struct ValidatedSchedule(pub(crate) cron::Schedule);
impl ValidatedSchedule {
pub fn parse(expr: &str) -> CronResult<Self> {
let schedule = cron::Schedule::from_str(expr)
.map_err(|e| CronError::InvalidSchedule(format!("{}: {}", expr, e)))?;
Ok(Self(schedule))
}
pub fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>> {
let local_after = after.with_timezone(&tz);
self.0
.after(&local_after)
.next()
.map(|dt| dt.with_timezone(&Utc))
}
}
pub trait Schedule: Send + Sync {
fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>>;
}
impl Schedule for ValidatedSchedule {
fn next_after(&self, after: &DateTime<Utc>, tz: Tz) -> Option<DateTime<Utc>> {
self.next_after(after, tz)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobType {
Recurring,
Once,
}
#[async_trait::async_trait]
pub trait JobContract: Send + Sync {
async fn run(&self) -> CronResult<()>;
fn id(&self) -> Cow<'_, str>;
fn name(&self) -> Cow<'_, str>;
fn schedule(&self) -> &dyn Schedule;
fn job_type(&self) -> JobType {
JobType::Recurring
}
fn run_at(&self) -> Option<DateTime<Utc>> {
None
}
fn start_after(&self) -> Option<DateTime<Utc>> {
None
}
fn timezone(&self) -> Tz {
chrono_tz::UTC
}
fn description(&self) -> Option<Cow<'_, str>> {
None
}
fn timeout(&self) -> Option<Duration> {
None
}
fn priority(&self) -> i32 {
0
}
fn concurrency_limit(&self) -> Option<usize> {
None
}
fn misfire_policy(&self) -> MisfirePolicy {
MisfirePolicy::default()
}
fn retry_policy(&self) -> RetryPolicy {
RetryPolicy::default()
}
async fn on_start(&self) {}
async fn on_complete(&self) {}
async fn on_error(&self, _error: &CronError) {}
}