rcron/
lib.rs

1//! # rcron
2//!
3//! a simple cron-like job scheduling library for Rust.
4//!
5//! ## Usage
6//!
7//! Be sure to add the rcron crate to your `Cargo.toml`:
8//!
9//! ```toml
10//! [dependencies]
11//! rcron = "~1.2"
12//! ```
13//!
14//! Creating a schedule for a job is done using the `FromStr` impl for the
15//! `Schedule` type of the [cron](https://github.com/zslayton/cron) library.
16//!
17//! The scheduling format is as follows:
18//!
19//! ```text
20//! sec   min   hour   day of month   month   day of week   year
21//! *     *     *      *              *       *             *
22//! ```
23//!
24//! Note that the year may be omitted.
25//!
26//! Comma separated values such as `5,8,10` represent more than one time
27//! value. So for example, a schedule of `0 2,14,26 * * * *` would execute
28//! on the 2nd, 14th, and 26th minute of every hour.
29//!
30//! Ranges can be specified with a dash. A schedule of `0 0 * 5-10 * *`
31//! would execute once per hour but only on day 5 through 10 of the month.
32//!
33//! Day of the week can be specified as an abbreviation or the full name.
34//! A schedule of `0 0 6 * * Sun,Sat` would execute at 6am on Sunday and
35//! Saturday.
36//!
37//! A simple usage example:
38//!
39//! ```rust
40//! extern crate rcron;
41//! use rcron::{JobScheduler, Job};
42//! use std::time::Duration;
43//!
44//! fn main() {
45//!     let mut sched = JobScheduler::new();
46//!
47//!     sched.add(Job::new("1/10 * * * * *".parse().unwrap(), || {
48//!         println!("exec task every 10 seconds!");
49//!     }));
50//!
51//!     sched.add(Job::new("1/5 * * * * *".parse().unwrap(), || {
52//!         println!("exec task every 5 seconds!");
53//!     }));
54//!
55//!     loop {
56//!         sched.tick();
57//!
58//!         std::thread::sleep(Duration::from_millis(500));
59//!     }
60//! }
61//! ```
62
63use chrono::{DateTime, Duration, Local};
64pub use cron::Schedule;
65pub use uuid::Uuid;
66
67/// A scheduled `Job`.
68pub struct Job<'a> {
69    schedule: Schedule,
70    run: Box<dyn (FnMut() -> ()) + 'a>,
71    last_tick: Option<DateTime<Local>>,
72    limit_missed_runs: usize,
73    job_id: Uuid,
74}
75
76impl<'a> Job<'a> {
77    /// Create a new job.
78    ///
79    /// ```rust,ignore
80    /// // Run at second 0 of the 15th minute of the 6th, 8th, and 10th hour
81    /// // of any day in March and June that is a Friday of the year 2017.
82    /// let s: Schedule = "0 15 6,8,10 * Mar,Jun Fri 2017".into().unwrap();
83    /// Job::new(s, || println!("I have a complex schedule...") );
84    /// ```
85    pub fn new<T>(schedule: Schedule, run: T) -> Job<'a>
86    where
87        T: 'a,
88        T: FnMut() -> (),
89    {
90        Job {
91            schedule,
92            run: Box::new(run),
93            last_tick: None,
94            limit_missed_runs: 1,
95            job_id: Uuid::new_v4(),
96        }
97    }
98
99    fn tick(&mut self) {
100        let now = Local::now();
101        if self.last_tick.is_none() {
102            self.last_tick = Some(now);
103            return;
104        }
105
106        if self.limit_missed_runs > 0 {
107            self.schedule
108                .after(&self.last_tick.unwrap())
109                .take(self.limit_missed_runs)
110                .take_while(|&event| event <= now)
111                .for_each(|_| (self.run)());
112        } else {
113            self.schedule
114                .after(&self.last_tick.unwrap())
115                .take_while(|&event| event <= now)
116                .for_each(|_| (self.run)());
117        }
118
119        self.last_tick = Some(now);
120    }
121
122    /// Set the limit for missed jobs in the case of delayed runs. Setting to 0 means unlimited.
123    ///
124    /// ```rust,ignore
125    /// let mut job = Job::new("0/1 * * * * *".parse().unwrap(), || {
126    ///     println!("I get executed every 1 seconds!");
127    /// });
128    /// job.limit_missed_runs(99);
129    /// ```
130    pub fn limit_missed_runs(&mut self, limit: usize) {
131        self.limit_missed_runs = limit;
132    }
133
134    /// Set last tick to force re-running of missed runs.
135    ///
136    /// ```rust,ignore
137    /// let mut job = Job::new("0/1 * * * * *".parse().unwrap(), || {
138    ///     println!("I get executed every 1 seconds!");
139    /// });
140    /// job.last_tick(Some(Local::now()));
141    /// ```
142    pub fn last_tick(&mut self, last_tick: Option<DateTime<Local>>) {
143        self.last_tick = last_tick;
144    }
145}
146
147#[derive(Default)]
148/// The JobScheduler of rcron contains and executes the scheduled jobs.
149pub struct JobScheduler<'a> {
150    jobs: Vec<Job<'a>>,
151}
152
153impl<'a> JobScheduler<'a> {
154    /// Create a new `JobScheduler`.
155    pub fn new() -> JobScheduler<'a> {
156        JobScheduler { jobs: Vec::new() }
157    }
158
159    /// Add a job to the `JobScheduler`
160    ///
161    /// ```rust,ignore
162    /// let mut sched = JobScheduler::new();
163    /// sched.add(Job::new("1/10 * * * * *".parse().unwrap(), || {
164    ///     println!("I get executed every 10 seconds!");
165    /// }));
166    /// ```
167    pub fn add(&mut self, job: Job<'a>) -> Uuid {
168        let job_id = job.job_id;
169        self.jobs.push(job);
170
171        job_id
172    }
173
174    /// Remove a job from the `JobScheduler`
175    ///
176    /// ```rust,ignore
177    /// let mut sched = JobScheduler::new();
178    /// let job_id = sched.add(Job::new("1/10 * * * * *".parse().unwrap(), || {
179    ///     println!("I get executed every 10 seconds!");
180    /// }));
181    /// sched.remove(job_id);
182    /// ```
183    pub fn remove(&mut self, job_id: Uuid) -> bool {
184        let mut found_index = None;
185        for (i, job) in self.jobs.iter().enumerate() {
186            if job.job_id == job_id {
187                found_index = Some(i);
188                break;
189            }
190        }
191
192        if found_index.is_some() {
193            self.jobs.remove(found_index.unwrap());
194        }
195
196        found_index.is_some()
197    }
198
199    /// The `tick` method increments time for the JobScheduler and executes
200    /// any pending jobs. It is recommended to sleep for at least 500
201    /// milliseconds between invocations of this method.
202    ///
203    /// ```rust,ignore
204    /// loop {
205    ///     sched.tick();
206    ///     std::thread::sleep(Duration::from_millis(500));
207    /// }
208    /// ```
209    pub fn tick(&mut self) {
210        for job in &mut self.jobs {
211            job.tick();
212        }
213    }
214
215    /// The `time_till_next_job` method returns the duration till the next job
216    /// is supposed to run. This can be used to sleep until then without waking
217    /// up at a fixed interval.AsMut
218    ///
219    /// ```rust, ignore
220    /// loop {
221    ///     sched.tick();
222    ///     std::thread::sleep(sched.time_till_next_job());
223    /// }
224    /// ```
225    pub fn time_till_next_job(&self) -> std::time::Duration {
226        if self.jobs.is_empty() {
227            // Take a guess if there are no jobs.
228            return std::time::Duration::from_millis(500);
229        }
230
231        let mut duration = Duration::zero();
232        let now = Local::now();
233        for job in self.jobs.iter() {
234            for event in job.schedule.upcoming(Local).take(1) {
235                let d = event - now;
236                if duration.is_zero() || d < duration {
237                    duration = d;
238                }
239            }
240        }
241        duration.to_std().unwrap()
242    }
243}
244
245#[cfg(test)]
246mod test {
247    #[test]
248    fn it_works() {
249        println!("rcron");
250    }
251
252    #[test]
253    fn test_run() {
254        use super::Job;
255        use super::JobScheduler;
256        use std::time::Duration;
257        let mut sched = JobScheduler::new();
258        sched.add(Job::new("1/2 * * * * *".parse().unwrap(), || {
259            println!("I get executed every 2 seconds!");
260        }));
261
262        loop {
263            sched.tick();
264
265            std::thread::sleep(Duration::from_millis(500));
266        }
267    }
268}