Skip to main content

scheduler/
scheduler.rs

1use std::time::Duration;
2
3use time::DateTime;
4
5use crate::error::SchedulerError;
6
7pub enum Scheduler {
8    /// Set to execute on set time periods
9    ///
10    /// Boxed: a [`cron::Schedule`] is ~248 bytes, and boxing keeps the enum
11    /// small (in particular the elements of [`Scheduler::Multi`]).
12    Cron(Box<cron::Schedule>),
13
14    /// Set to execute exactly `duration` away from the previous execution.
15    /// If
16    Interval {
17        interval_duration: Duration,
18        execute_at_startup: bool,
19    },
20
21    /// Multi shceduler: the execution is trigger where at least one of the schedulers in matched
22    Multi(Vec<Scheduler>),
23
24    /// Set to execute to never
25    Never,
26}
27
28impl Scheduler {
29    pub fn from(schedule: &[&dyn TryToScheduler]) -> Result<Scheduler, SchedulerError> {
30        schedule.to_scheduler()
31    }
32
33    // Determine the next time we should execute (from a reference point)
34    pub fn next(&mut self, after: &DateTime) -> Option<DateTime> {
35        match *self {
36            Scheduler::Cron(ref cs) => cs.after(after).next(),
37
38            Scheduler::Interval {
39                ref interval_duration,
40                ref mut execute_at_startup,
41            } => {
42                if *execute_at_startup {
43                    *execute_at_startup = false;
44                    Some(*after)
45                } else {
46                    after.checked_add(*interval_duration).ok()
47                }
48            }
49
50            Scheduler::Multi(ref mut schedulers) => {
51                let mut result = None;
52                for scheduler in schedulers {
53                    if let Some(local_next) = scheduler.next(after) {
54                        result = match result {
55                            Some(current_next) => {
56                                if local_next < current_next {
57                                    Some(local_next)
58                                } else {
59                                    Some(current_next)
60                                }
61                            }
62                            None => Some(local_next),
63                        }
64                    }
65                }
66                result
67            }
68
69            Scheduler::Never => None,
70        }
71    }
72}
73
74pub trait TryToScheduler {
75    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError>;
76}
77
78impl TryToScheduler for Vec<String> {
79    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
80        let refs: Vec<&str> = self.iter().map(|s| s.as_ref()).collect();
81        refs.to_scheduler()
82    }
83}
84
85impl TryToScheduler for Vec<&str> {
86    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
87        match self.len() {
88            0 => Ok(Scheduler::Never),
89            1 => self[0].to_scheduler(),
90            _ => {
91                let mut result = vec![];
92                for scheduler in self {
93                    result.push(scheduler.to_scheduler()?);
94                }
95                Ok(Scheduler::Multi(result))
96            }
97        }
98    }
99}
100
101impl TryToScheduler for Vec<&dyn TryToScheduler> {
102    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
103        (&self[..]).to_scheduler()
104    }
105}
106
107impl TryToScheduler for &[&dyn TryToScheduler] {
108    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
109        match self.len() {
110            0 => Ok(Scheduler::Never),
111            1 => self[0].to_scheduler(),
112            _ => {
113                let mut result = vec![];
114                for scheduler in *self {
115                    result.push(scheduler.to_scheduler()?);
116                }
117                Ok(Scheduler::Multi(result))
118            }
119        }
120    }
121}
122
123impl<'a> TryToScheduler for &'a str {
124    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
125        Ok(Scheduler::Cron(Box::new(self.parse().map_err(|err| {
126            SchedulerError::ScheduleDefinitionError {
127                message: format!("Cannot create schedule for [{self}]. Err: {err:?}"),
128            }
129        })?)))
130    }
131}
132
133impl TryToScheduler for String {
134    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
135        self.as_str().to_scheduler()
136    }
137}
138
139impl TryToScheduler for Duration {
140    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
141        Ok(Scheduler::Interval {
142            interval_duration: *self,
143            execute_at_startup: false,
144        })
145    }
146}
147
148impl TryToScheduler for (Duration, bool) {
149    fn to_scheduler(&self) -> Result<Scheduler, SchedulerError> {
150        Ok(Scheduler::Interval {
151            interval_duration: self.0,
152            execute_at_startup: self.1,
153        })
154    }
155}
156
157impl From<Vec<Scheduler>> for Scheduler {
158    fn from(val: Vec<Scheduler>) -> Self {
159        Scheduler::Multi(val)
160    }
161}
162
163#[cfg(test)]
164pub mod test {
165
166    use time::TimeZone;
167
168    use super::*;
169
170    #[test]
171    fn never_should_not_schedule() {
172        let mut schedule = Scheduler::Never;
173        assert_eq!(None, schedule.next(&DateTime::now_in(TimeZone::UTC)))
174    }
175
176    #[test]
177    fn interval_should_schedule_plus_duration() {
178        let now = DateTime::now_in(TimeZone::UTC);
179        let secs = 10;
180        let mut schedule = Duration::new(secs, 0).to_scheduler().unwrap();
181
182        let next = schedule.next(&now).unwrap();
183
184        assert!(next.unix() >= now.unix() + (secs as i64));
185    }
186
187    #[test]
188    fn interval_should_schedule_at_startup() {
189        let now = DateTime::now_in(TimeZone::UTC);
190        let secs = 10;
191        let mut schedule = (Duration::new(secs, 0), true).to_scheduler().unwrap();
192
193        let first = schedule.next(&now).unwrap();
194        assert_eq!(now.unix(), first.unix());
195
196        let next = schedule.next(&now).unwrap();
197        assert!(next.unix() >= now.unix() + (secs as i64));
198    }
199
200    #[test]
201    fn should_build_an_interval_schedule_from_duration() {
202        let schedule = Duration::new(1, 1).to_scheduler().unwrap();
203        match schedule {
204            Scheduler::Interval {
205                ..
206            } => (),
207            _ => panic!(),
208        }
209    }
210
211    #[test]
212    fn should_build_a_periodic_schedule_from_str() {
213        let schedule = "* * * * * *".to_scheduler().unwrap();
214        match schedule {
215            Scheduler::Cron(_) => (),
216            _ => panic!(),
217        }
218    }
219
220    #[test]
221    fn should_build_a_multi_scheduler_from_empty_array() {
222        let schedule = Scheduler::from(&[]).unwrap();
223        match schedule {
224            Scheduler::Never => (),
225            _ => panic!(),
226        }
227    }
228
229    #[test]
230    fn should_build_a_multi_scheduler_from_single_entry_array() {
231        let schedule = Scheduler::from(&[&vec!["* * * * * *"]]).unwrap();
232        match schedule {
233            Scheduler::Cron(_) => (),
234            _ => panic!(),
235        }
236    }
237
238    #[test]
239    fn should_build_a_multi_scheduler_from_array() {
240        let schedule = Scheduler::from(&[&"* * * * * *", &Duration::from_secs(9)]).unwrap();
241        match schedule {
242            Scheduler::Multi(inner) => {
243                match inner[0] {
244                    Scheduler::Cron(_) => (),
245                    _ => panic!(),
246                };
247                match inner[1] {
248                    Scheduler::Interval {
249                        ..
250                    } => (),
251                    _ => panic!(),
252                };
253            }
254            _ => panic!(),
255        }
256    }
257
258    #[test]
259    fn cron_should_be_time_zone_aware_with_utc() {
260        let mut schedule = "* 11 10 * * *".to_scheduler().unwrap();
261        let date = DateTime::from_parts(2010, 1, 1, 10, 10, 0, 0, TimeZone::UTC).unwrap();
262
263        let expected_utc = DateTime::from_parts(2010, 1, 1, 10, 11, 0, 0, TimeZone::UTC).unwrap();
264
265        let next = schedule.next(&date).unwrap();
266
267        assert_eq!(next, expected_utc);
268    }
269
270    // #[test]
271    // fn cron_should_be_time_zone_aware_with_custom_time_zone() {
272    //     let mut schedule = "* 11 10 * * *".to_scheduler().unwrap();
273
274    //     let date = Utc.with_ymd_and_hms(2010, 1, 1, 10, 10, 0).unwrap();
275    //     let expected_utc = Utc.with_ymd_and_hms(2010, 1, 2, 9, 11, 0).unwrap();
276
277    //     let next = schedule.next(&date).unwrap();
278
279    //     assert_eq!(next.with_timezone(&Utc), expected_utc);
280    // }
281
282    #[test]
283    fn multi_should_return_first_possible_next_execution() {
284        let mut schedule = Scheduler::from(&[&"* 10 10 * * *", &"* 20 20 * * *"]).unwrap();
285
286        {
287            let date = DateTime::from_parts(2010, 1, 1, 10, 8, 0, 0, TimeZone::UTC).unwrap();
288            let expected = DateTime::from_parts(2010, 1, 1, 10, 10, 0, 0, TimeZone::UTC).unwrap();
289
290            let next = schedule.next(&date).unwrap();
291            assert_eq!(next, expected);
292        }
293
294        {
295            let date = DateTime::from_parts(2010, 1, 1, 11, 8, 0, 0, TimeZone::UTC).unwrap();
296            let expected = DateTime::from_parts(2010, 1, 1, 20, 20, 0, 0, TimeZone::UTC).unwrap();
297
298            let next = schedule.next(&date).unwrap();
299            assert_eq!(next, expected);
300        }
301
302        {
303            let date = DateTime::from_parts(2010, 1, 1, 22, 8, 0, 0, TimeZone::UTC).unwrap();
304            let expected = DateTime::from_parts(2010, 1, 2, 10, 10, 0, 0, TimeZone::UTC).unwrap();
305
306            let next = schedule.next(&date).unwrap();
307            assert_eq!(next, expected);
308        }
309    }
310}