use std::time::Duration;
use anyhow::bail;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::pretty_duration::PrettyDuration;
use super::{EntityDescriptorConst, JobDefinition};
pub type CronJobId = Uuid;
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
pub struct CronJobSpecV1 {
pub schedule: String,
pub max_schedule_drift: Option<PrettyDuration>,
#[serde(flatten)]
pub job: JobDefinition,
}
impl CronJobSpecV1 {
pub fn parse_schedule(&self) -> Result<CronSchedule, CronTabParseError> {
self.schedule.parse()
}
}
impl EntityDescriptorConst for CronJobSpecV1 {
const NAMESPACE: &'static str = "wasmer.io";
const NAME: &'static str = "CronJob";
const VERSION: &'static str = "v1-alpha1";
const KIND: &'static str = "wasmer.io/CronJob.v1-alpha1";
type Spec = Self;
type State = ();
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum CronSchedule {
Interval(std::time::Duration),
CronTab(CronTab),
}
impl CronSchedule {
pub fn next(
&self,
last: Option<time::OffsetDateTime>,
drift: Option<Duration>,
) -> Result<OffsetDateTime, anyhow::Error> {
match self {
CronSchedule::Interval(duration) => {
if let Some(last) = last {
Ok(last + *duration)
} else {
Ok(OffsetDateTime::now_utc())
}
}
CronSchedule::CronTab(c) => c.next(last, drift),
}
}
pub fn max_timewindow(&self) -> Duration {
match self {
CronSchedule::Interval(duration) => *duration,
CronSchedule::CronTab(c) => c.max_timewindow(),
}
}
}
impl std::fmt::Display for CronSchedule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CronSchedule::Interval(d) => write!(f, "{}", PrettyDuration::from(*d)),
CronSchedule::CronTab(c) => c.fmt(f),
}
}
}
impl std::str::FromStr for CronSchedule {
type Err = CronTabParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<crate::pretty_duration::PrettyDuration>() {
Ok(d) => Ok(Self::Interval(d.0)),
Err(_) => match s.parse::<CronTab>() {
Ok(c) => Ok(Self::CronTab(c)),
Err(_) => Err(CronTabParseError::new(
s,
"invalid cron schedule - expected either an interval like '1m10s' or a valid crontab".to_string(),
)),
},
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
enum CronTabValue {
All,
Value(u8),
}
impl std::fmt::Display for CronTabValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CronTabValue::All => write!(f, "*"),
CronTabValue::Value(x) => write!(f, "{x}"),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct CronTab {
minute: CronTabValue,
hour: CronTabValue,
day_of_month: CronTabValue,
month: CronTabValue,
day_of_week: CronTabValue,
}
impl CronTab {
pub fn next(
&self,
_last: Option<time::OffsetDateTime>,
_drift: Option<Duration>,
) -> Result<OffsetDateTime, anyhow::Error> {
let now = OffsetDateTime::now_utc();
let mut target =
now.replace_nanosecond(0)? + std::time::Duration::from_secs(60 - now.second() as u64);
match self.minute {
CronTabValue::All => {}
CronTabValue::Value(val) => {
if target.minute() < val {
let diff = 60 - val - target.minute();
target += Duration::from_secs(diff as u64 * 60);
} else {
target = target.replace_minute(val)?;
}
}
}
match self.hour {
CronTabValue::All => {}
CronTabValue::Value(hour) => {
if target.hour() < hour {
let diff = 24 - hour - target.hour();
target += Duration::from_secs(diff as u64 * 60 * 60);
} else {
target = target.replace_hour(hour)?;
}
}
}
match self.month {
CronTabValue::All => {}
CronTabValue::Value(month) => {
let cur_month: u8 = target.month().into();
if month < cur_month {
let diff = 12 - cur_month - month;
target += Duration::from_secs(diff as u64 * 60 * 60 * 24 * 30);
} else {
target = target.replace_month(month.try_into()?)?;
}
}
}
match self.day_of_week {
CronTabValue::All => {}
CronTabValue::Value(_dm) => {
bail!("day of week schedule not supported yet");
}
}
Ok(target)
}
pub fn max_timewindow(&self) -> Duration {
Duration::from_secs(60 * 5)
}
}
impl std::str::FromStr for CronTab {
type Err = CronTabParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split_whitespace();
let part = parts
.next()
.ok_or_else(|| CronTabParseError::new(s, "missing minute specifier"))?;
let minute = match part {
"*" => CronTabValue::All,
x => {
let x = x.parse::<u8>().map_err(|err| {
CronTabParseError::new(
s,
format!("invalid minute specifier '{x}' - expected * or [0-59]: '{err}'"),
)
})?;
if x > 59 {
return Err(CronTabParseError::new(s, format!("invalid minute specifier '{x}': expected * or a number between 0 and 59")));
}
CronTabValue::Value(x)
}
};
let part = parts
.next()
.ok_or_else(|| CronTabParseError::new(s, "missing hour specifier"))?;
let hour = match part {
"*" => CronTabValue::All,
x => {
let x = x.parse::<u8>().map_err(|err| {
CronTabParseError::new(
s,
format!("invalid hour specifier '{x}' - expected * or [0-23]: '{err}'"),
)
})?;
if x > 23 {
return Err(CronTabParseError::new(
s,
format!(
"invalid hour specifier '{x}': expected * or a number between 0 and 23"
),
));
}
CronTabValue::Value(x)
}
};
let part = parts
.next()
.ok_or_else(|| CronTabParseError::new(s, "missing day of month specifier"))?;
let day_of_month = match part {
"*" => CronTabValue::All,
x => {
let x = x.parse::<u8>().map_err(|err| {
CronTabParseError::new(
s,
format!(
"invalid day of month specifier '{x}' - expected * or [1-31]: '{err}'",
),
)
})?;
if x < 1 || x > 31 {
return Err(CronTabParseError::new(
s,
format!(
"invalid day of month specifier '{x}': expected * or a number between 1 and 31",
),
));
}
CronTabValue::Value(x)
}
};
let part = parts
.next()
.ok_or_else(|| CronTabParseError::new(s, "missing month specifier"))?;
let month = match part {
"*" => CronTabValue::All,
x => {
let x = x.parse::<u8>().map_err(|err| {
CronTabParseError::new(
s,
format!("invalid month specifier '{x}' - expected * or [1-12]: '{err}'"),
)
})?;
if x < 1 || x > 12 {
return Err(CronTabParseError::new(
s,
format!(
"invalid month specifier '{x}': expected * or a number between 1 and 12",
),
));
}
CronTabValue::Value(x)
}
};
let part = parts
.next()
.ok_or_else(|| CronTabParseError::new(s, "missing day of week specifier"))?;
let day_of_week = match part {
"*" => CronTabValue::All,
x => {
let x = x.parse::<u8>().map_err(|err| {
CronTabParseError::new(
s,
format!(
"invalid day of week specifier '{x}' - expected * or [0-6]: '{err}'",
),
)
})?;
if x > 6 {
return Err(CronTabParseError::new(
s,
format!(
"invalid day of week specifier '{x}': expected * or a number between 0 and 6",
),
));
}
CronTabValue::Value(x)
}
};
Ok(Self {
minute,
hour,
day_of_month,
month,
day_of_week,
})
}
}
impl std::fmt::Display for CronTab {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{minute} {hour} {day_of_month} {month} {day_of_week}",
minute = self.minute,
hour = self.hour,
day_of_month = self.day_of_month,
month = self.month,
day_of_week = self.day_of_week,
)
}
}
#[derive(Debug)]
pub struct CronTabParseError {
error: String,
value: String,
}
impl CronTabParseError {
pub fn new(tab: impl Into<String>, error: impl Into<String>) -> Self {
Self {
value: tab.into(),
error: error.into(),
}
}
}
impl std::fmt::Display for CronTabParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid cron tab '{}': {}", self.value, self.error,)
}
}
impl std::error::Error for CronTabParseError {}
#[cfg(test)]
mod tests {
use std::{str::FromStr, time::Duration};
use super::*;
#[test]
fn test_parse_schedule() {
assert_eq!(
CronSchedule::from_str("1m").unwrap(),
CronSchedule::Interval(Duration::from_secs(60)),
);
assert_eq!(
CronSchedule::from_str("* * * * *").unwrap(),
CronSchedule::CronTab(CronTab {
minute: CronTabValue::All,
hour: CronTabValue::All,
day_of_month: CronTabValue::All,
month: CronTabValue::All,
day_of_week: CronTabValue::All,
})
);
}
#[test]
fn test_parse_crontab() {
assert_eq!(
CronTab::from_str("* * * * *").unwrap(),
CronTab {
minute: CronTabValue::All,
hour: CronTabValue::All,
day_of_month: CronTabValue::All,
month: CronTabValue::All,
day_of_week: CronTabValue::All,
},
);
assert_eq!(
CronTab::from_str("1 1 1 1 1").unwrap(),
CronTab {
minute: CronTabValue::Value(1),
hour: CronTabValue::Value(1),
day_of_month: CronTabValue::Value(1),
month: CronTabValue::Value(1),
day_of_week: CronTabValue::Value(1),
},
);
}
}