From 1a6c9a0bb85e2f13c027a21f527548c577d248d7 Mon Sep 17 00:00:00 2001 From: Michael van Niekerk Date: Sat, 21 Sep 2024 16:09:00 +0200 Subject: [PATCH 1/3] english-to-cron integration --- Cargo.toml | 2 ++ README.md | 58 +++++++++++++++++++++++++++++++++------------- examples/lib.rs | 25 +++++++++++++++++++- src/job/builder.rs | 16 +++++++++++++ src/job/mod.rs | 28 ++++++++++++++++------ 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 00ce6ff..9774402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ categories = ["date-and-time"] tokio = { version = "1", features = ["time", "rt", "sync"] } croner = "2.0.5" chrono = { version = "0.4", default-features = false } +english-to-cron = { version = "0.1", optional = true } uuid = { version = "1", features = ["v4"] } prost = { version = "0.13", optional = true } tracing = "0.1" @@ -58,6 +59,7 @@ prost-build = { version = "0.13", optional = true } signal = ["tokio/signal"] has_bytes = ["prost-build", "prost"] nats_storage = ["nats", "has_bytes"] +english = ["english-to-cron"] postgres_storage = ["tokio-postgres", "has_bytes"] postgres_native_tls = ["postgres_storage", "postgres-native-tls"] postgres_openssl = ["postgres_storage", "postgres-openssl"] diff --git a/README.md b/README.md index 52f2980..9fa524f 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,22 @@ async fn main() -> Result<(), JobSchedulerError> { })? ).await?; + // Needs the `english` feature enabled + sched.add( + Job::new_async("every 4 seconds", |uuid, mut l| { + Box::pin(async move { + println!("I run async every 4 seconds"); + + // Query the next execution time for this job + let next_tick = l.next_tick_for_job(uuid).await; + match next_tick { + Ok(Some(ts)) => println!("Next time for 4s job is {:?}", ts), + _ => println!("Could not get next tick for 4s job"), + } + }) + })? + ); + // Add one-shot job with given duration sched.add( Job::new_one_shot(Duration::from_secs(18), |_uuid, _l| { @@ -139,23 +155,25 @@ chrono-tz is not included into the dependencies, so you need to add it to your C would like to have easy creation of a `Timezone` struct. ```rust -let job = JobBuilder::new() -.with_timezone(chrono_tz::Africa::Johannesburg) -.with_cron_job_type() -.with_schedule("*/2 * * * *") -.unwrap() -.with_run_async(Box::new( | uuid, mut l| { -Box::pin(async move { -info ! ("JHB run async every 2 seconds id {:?}", uuid); -let next_tick = l.next_tick_for_job(uuid).await; -match next_tick { -Ok(Some(ts)) => info !("Next time for JHB 2s is {:?}", ts), -_ => warn !("Could not get next tick for 2s job"), +async fn tz_job() { + let job = JobBuilder::new() + .with_timezone(chrono_tz::Africa::Johannesburg) + .with_cron_job_type() + .with_schedule("*/2 * * * *") + .unwrap() + .with_run_async(Box::new(|uuid, mut l| { + Box::pin(async move { + info!("JHB run async every 2 seconds id {:?}", uuid); + let next_tick = l.next_tick_for_job(uuid).await; + match next_tick { + Ok(Some(ts)) => info!("Next time for JHB 2s is {:?}", ts), + _ => warn!("Could not get next tick for 2s job"), + } + }) + })) + .build() + .unwrap(); } -}) -})) -.build() -.unwrap(); ``` ## Similar Libraries @@ -191,6 +209,14 @@ Please see the [CONTRIBUTING](CONTRIBUTING.md) file for more information. ## Features +### english + +Since 0.13.0 + +Enables the schedule text to be interpreted in English. This is done using +the [english-to-cron](https://crates.io/crates/english-to-cron) crate. +For instance "every 15 seconds" will be converted in the background to "0/15 * * * * ? *". + ### has_bytes Since 0.7 diff --git a/examples/lib.rs b/examples/lib.rs index 0b5f70d..e856323 100644 --- a/examples/lib.rs +++ b/examples/lib.rs @@ -181,6 +181,28 @@ pub async fn run_example(sched: &mut JobScheduler) -> Result, JobSched let jhb_job_guid = jhb_job.guid(); sched.add(jhb_job).await.unwrap(); + #[cfg(feature = "english")] + let english_job = JobBuilder::new() + .with_timezone(Utc) + .with_cron_job_type() + .with_schedule("every 10 seconds") + .unwrap() + .with_run_async(Box::new(|uuid, mut l| { + Box::pin(async move { + info!("English parsed job every 10 seconds id {:?}", uuid); + let next_tick = l.next_tick_for_job(uuid).await; + match next_tick { + Ok(Some(ts)) => info!("Next time for English parsed job is is {:?}", ts), + _ => warn!("Could not get next tick for English parsed job"), + } + }) + })) + .build() + .unwrap(); + + let english_job_guid = english_job.guid(); + sched.add(english_job).await.unwrap(); + let start = sched.start().await; if let Err(e) = start { error!("Error starting scheduler {}", e); @@ -194,8 +216,9 @@ pub async fn run_example(sched: &mut JobScheduler) -> Result, JobSched jja_guid, utc_job_guid, jhb_job_guid, + english_job_guid, ]; - return Ok(ret); + Ok(ret) } pub async fn stop_example( diff --git a/src/job/builder.rs b/src/job/builder.rs index 336d201..9d0277a 100644 --- a/src/job/builder.rs +++ b/src/job/builder.rs @@ -101,6 +101,22 @@ impl JobBuilder { TS: ToString, { let schedule = schedule.to_string(); + #[cfg(feature = "english")] + let schedule = { + match Cron::new(&schedule).parse() { + Ok(_) => schedule, + Err(_) => match english_to_cron::str_cron_syntax(&schedule) { + Ok(english_to_cron) => { + if english_to_cron != schedule { + english_to_cron + } else { + schedule + } + } + Err(_) => schedule, + }, + } + }; let schedule = Cron::new(&schedule) .with_seconds_required() .with_dom_and_dow() diff --git a/src/job/mod.rs b/src/job/mod.rs index 8858bd4..67ba40f 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -179,12 +179,13 @@ impl JobLocked { /// sched.add(job) /// tokio::spawn(sched.start()); /// ``` - pub fn new_async(schedule: &str, run: T) -> Result + pub fn new_async(schedule: S, run: T) -> Result where T: 'static, T: FnMut(Uuid, JobsSchedulerLocked) -> Pin + Send>> + Send + Sync, + S: ToString, { Self::new_async_tz(schedule, Utc, run) } @@ -215,6 +216,22 @@ impl JobLocked { TZ: TimeZone, { let schedule = schedule.to_string(); + #[cfg(feature = "english")] + let schedule = { + match Cron::new(&schedule).parse() { + Ok(_) => schedule, + Err(_) => match english_to_cron::str_cron_syntax(&schedule) { + Ok(english_to_cron) => { + if english_to_cron != schedule { + english_to_cron + } else { + schedule + } + } + Err(_) => schedule, + }, + } + }; let time_offset_seconds = timezone .offset_from_utc_datetime(&Utc::now().naive_local()) .fix() @@ -315,7 +332,7 @@ impl JobLocked { /// tokio::spawn(sched.start()); /// ``` pub fn new_cron_job_async_tz( - schedule: &str, + schedule: S, timezone: TZ, run: T, ) -> Result @@ -488,10 +505,7 @@ impl JobLocked { /// sched.add(job) /// tokio::spawn(sched.start()); /// ``` - pub fn new_one_shot_at_instant( - instant: std::time::Instant, - run: T, - ) -> Result + pub fn new_one_shot_at_instant(instant: Instant, run: T) -> Result where T: 'static, T: FnMut(Uuid, JobsSchedulerLocked) + Send + Sync, @@ -515,7 +529,7 @@ impl JobLocked { /// tokio::spawn(sched.start()); /// ``` pub fn new_one_shot_at_instant_async( - instant: std::time::Instant, + instant: Instant, run: T, ) -> Result where From a1159c1bc9f777005d7bd9b85c9a00c5e8b924f6 Mon Sep 17 00:00:00 2001 From: Michael van Niekerk Date: Sat, 21 Sep 2024 16:20:44 +0200 Subject: [PATCH 2/3] Warning on changing schedule on builder --- examples/lib.rs | 42 +++++++++++++++++++++++------------------- src/job/builder.rs | 17 +++++++++++------ src/job/mod.rs | 26 +++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/examples/lib.rs b/examples/lib.rs index e856323..a322c57 100644 --- a/examples/lib.rs +++ b/examples/lib.rs @@ -182,26 +182,29 @@ pub async fn run_example(sched: &mut JobScheduler) -> Result, JobSched sched.add(jhb_job).await.unwrap(); #[cfg(feature = "english")] - let english_job = JobBuilder::new() - .with_timezone(Utc) - .with_cron_job_type() - .with_schedule("every 10 seconds") - .unwrap() - .with_run_async(Box::new(|uuid, mut l| { - Box::pin(async move { - info!("English parsed job every 10 seconds id {:?}", uuid); - let next_tick = l.next_tick_for_job(uuid).await; - match next_tick { - Ok(Some(ts)) => info!("Next time for English parsed job is is {:?}", ts), - _ => warn!("Could not get next tick for English parsed job"), - } - }) - })) - .build() - .unwrap(); + let english_job_guid = { + let english_job = JobBuilder::new() + .with_timezone(Utc) + .with_cron_job_type() + .with_schedule("every 10 seconds") + .unwrap() + .with_run_async(Box::new(|uuid, mut l| { + Box::pin(async move { + info!("English parsed job every 10 seconds id {:?}", uuid); + let next_tick = l.next_tick_for_job(uuid).await; + match next_tick { + Ok(Some(ts)) => info!("Next time for English parsed job is is {:?}", ts), + _ => warn!("Could not get next tick for English parsed job"), + } + }) + })) + .build() + .unwrap(); - let english_job_guid = english_job.guid(); - sched.add(english_job).await.unwrap(); + let english_job_guid = english_job.guid(); + sched.add(english_job).await.unwrap(); + english_job_guid + }; let start = sched.start().await; if let Err(e) = start { @@ -216,6 +219,7 @@ pub async fn run_example(sched: &mut JobScheduler) -> Result, JobSched jja_guid, utc_job_guid, jhb_job_guid, + #[cfg(feature = "english")] english_job_guid, ]; Ok(ret) diff --git a/src/job/builder.rs b/src/job/builder.rs index 9d0277a..c58d8d2 100644 --- a/src/job/builder.rs +++ b/src/job/builder.rs @@ -1,7 +1,11 @@ use crate::job::cron_job::CronJob; #[cfg(not(feature = "has_bytes"))] +use crate::job::job_data; +#[cfg(not(feature = "has_bytes"))] pub use crate::job::job_data::{JobStoredData, JobType, Uuid}; #[cfg(feature = "has_bytes")] +use crate::job::job_data_prost; +#[cfg(feature = "has_bytes")] pub use crate::job::job_data_prost::{JobStoredData, JobType, Uuid}; use crate::job::{nop, nop_async, JobLocked}; use crate::{JobSchedulerError, JobToRun, JobToRunAsync}; @@ -10,11 +14,7 @@ use core::time::Duration; use croner::Cron; use std::sync::{Arc, RwLock}; use std::time::Instant; - -#[cfg(not(feature = "has_bytes"))] -use crate::job::job_data; -#[cfg(feature = "has_bytes")] -use crate::job::job_data_prost; +use tracing::warn; use uuid::Uuid as UuidUuid; @@ -103,11 +103,16 @@ impl JobBuilder { let schedule = schedule.to_string(); #[cfg(feature = "english")] let schedule = { - match Cron::new(&schedule).parse() { + match Cron::new(&schedule) + .with_seconds_required() + .with_dom_and_dow() + .parse() + { Ok(_) => schedule, Err(_) => match english_to_cron::str_cron_syntax(&schedule) { Ok(english_to_cron) => { if english_to_cron != schedule { + warn!("Changing schedule [{schedule}] to [{english_to_cron}]"); english_to_cron } else { schedule diff --git a/src/job/mod.rs b/src/job/mod.rs index 67ba40f..3d98ac5 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -124,6 +124,26 @@ impl JobLocked { TZ: TimeZone, { let schedule = schedule.to_string(); + #[cfg(feature = "english")] + let schedule = { + match Cron::new(&schedule) + .with_seconds_required() + .with_dom_and_dow() + .parse() + { + Ok(_) => schedule, + Err(_) => match english_to_cron::str_cron_syntax(&schedule) { + Ok(english_to_cron) => { + if english_to_cron != schedule { + english_to_cron + } else { + schedule + } + } + Err(_) => schedule, + }, + } + }; let time_offset_seconds = timezone .offset_from_utc_datetime(&Utc::now().naive_local()) .fix() @@ -218,7 +238,11 @@ impl JobLocked { let schedule = schedule.to_string(); #[cfg(feature = "english")] let schedule = { - match Cron::new(&schedule).parse() { + match Cron::new(&schedule) + .with_seconds_required() + .with_dom_and_dow() + .parse() + { Ok(_) => schedule, Err(_) => match english_to_cron::str_cron_syntax(&schedule) { Ok(english_to_cron) => { From c7cf2a3e0201f759b13e33476965af7cf946bea8 Mon Sep 17 00:00:00 2001 From: Michael van Niekerk Date: Sun, 22 Sep 2024 11:15:22 +0200 Subject: [PATCH 3/3] Remove year field from english-to-cron output --- examples/lib.rs | 1 + src/job/builder.rs | 24 +------------ src/job/mod.rs | 86 ++++++++++++++++++++++------------------------ 3 files changed, 44 insertions(+), 67 deletions(-) diff --git a/examples/lib.rs b/examples/lib.rs index a322c57..3ca7463 100644 --- a/examples/lib.rs +++ b/examples/lib.rs @@ -186,6 +186,7 @@ pub async fn run_example(sched: &mut JobScheduler) -> Result, JobSched let english_job = JobBuilder::new() .with_timezone(Utc) .with_cron_job_type() + // .with_schedule("every 10 seconds") .with_schedule("every 10 seconds") .unwrap() .with_run_async(Box::new(|uuid, mut l| { diff --git a/src/job/builder.rs b/src/job/builder.rs index c58d8d2..b64d952 100644 --- a/src/job/builder.rs +++ b/src/job/builder.rs @@ -14,7 +14,6 @@ use core::time::Duration; use croner::Cron; use std::sync::{Arc, RwLock}; use std::time::Instant; -use tracing::warn; use uuid::Uuid as UuidUuid; @@ -100,28 +99,7 @@ impl JobBuilder { where TS: ToString, { - let schedule = schedule.to_string(); - #[cfg(feature = "english")] - let schedule = { - match Cron::new(&schedule) - .with_seconds_required() - .with_dom_and_dow() - .parse() - { - Ok(_) => schedule, - Err(_) => match english_to_cron::str_cron_syntax(&schedule) { - Ok(english_to_cron) => { - if english_to_cron != schedule { - warn!("Changing schedule [{schedule}] to [{english_to_cron}]"); - english_to_cron - } else { - schedule - } - } - Err(_) => schedule, - }, - } - }; + let schedule = JobLocked::schedule_to_cron(schedule)?; let schedule = Cron::new(&schedule) .with_seconds_required() .with_dom_and_dow() diff --git a/src/job/mod.rs b/src/job/mod.rs index 3d98ac5..c0c9719 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -29,6 +29,7 @@ mod runner; pub mod to_code; use crate::notification::{NotificationCreator, NotificationDeleter}; +use crate::JobSchedulerError::ParseSchedule; pub use builder::JobBuilder; pub use creator::JobCreator; pub use deleter::JobDeleter; @@ -123,27 +124,7 @@ impl JobLocked { S: ToString, TZ: TimeZone, { - let schedule = schedule.to_string(); - #[cfg(feature = "english")] - let schedule = { - match Cron::new(&schedule) - .with_seconds_required() - .with_dom_and_dow() - .parse() - { - Ok(_) => schedule, - Err(_) => match english_to_cron::str_cron_syntax(&schedule) { - Ok(english_to_cron) => { - if english_to_cron != schedule { - english_to_cron - } else { - schedule - } - } - Err(_) => schedule, - }, - } - }; + let schedule = Self::schedule_to_cron(schedule)?; let time_offset_seconds = timezone .offset_from_utc_datetime(&Utc::now().naive_local()) .fix() @@ -152,7 +133,7 @@ impl JobLocked { .with_seconds_required() .with_dom_and_dow() .parse() - .map_err(|_| JobSchedulerError::ParseSchedule)?; + .map_err(|_| ParseSchedule)?; let job_id = Uuid::new_v4(); Ok(Self(Arc::new(RwLock::new(Box::new(CronJob { data: JobStoredData { @@ -235,27 +216,7 @@ impl JobLocked { S: ToString, TZ: TimeZone, { - let schedule = schedule.to_string(); - #[cfg(feature = "english")] - let schedule = { - match Cron::new(&schedule) - .with_seconds_required() - .with_dom_and_dow() - .parse() - { - Ok(_) => schedule, - Err(_) => match english_to_cron::str_cron_syntax(&schedule) { - Ok(english_to_cron) => { - if english_to_cron != schedule { - english_to_cron - } else { - schedule - } - } - Err(_) => schedule, - }, - } - }; + let schedule = Self::schedule_to_cron(schedule)?; let time_offset_seconds = timezone .offset_from_utc_datetime(&Utc::now().naive_local()) .fix() @@ -264,7 +225,7 @@ impl JobLocked { .with_seconds_required() .with_dom_and_dow() .parse() - .map_err(|_| JobSchedulerError::ParseSchedule)?; + .map_err(|_| ParseSchedule)?; let job_id = Uuid::new_v4(); Ok(Self(Arc::new(RwLock::new(Box::new(CronJob { data: JobStoredData { @@ -923,4 +884,41 @@ impl JobLocked { _ => Err(JobSchedulerError::GetJobData), } } + + #[cfg(not(feature = "english"))] + pub fn schedule_to_cron(schedule: T) -> Result { + Ok(schedule.to_string()) + } + + #[cfg(feature = "english")] + pub fn schedule_to_cron(schedule: T) -> Result { + let schedule = schedule.to_string(); + match Cron::new(&schedule) + .with_seconds_required() + .with_dom_and_dow() + .parse() + { + Ok(_) => Ok(schedule), + Err(_) => match english_to_cron::str_cron_syntax(&schedule) { + Ok(english_to_cron) => { + if english_to_cron != schedule { + if english_to_cron == "0 * * * * ? *" { + Err(ParseSchedule) + } else { + // english-to-cron adds the year field which we can't put off (currently) + let cron = english_to_cron + .split(' ') + .take(6) + .collect::>() + .join(" "); + Ok(cron) + } + } else { + Ok(schedule) + } + } + Err(_) => Err(ParseSchedule), + }, + } + } }