Skip to main content

zino_core/schedule/
async_job.rs

1//! Scheduler for sync and async cron jobs.
2
3use super::{AsyncScheduler, DEFAULT_TICK_INTERVAL, JobContext};
4use crate::{BoxFuture, Uuid, datetime::DateTime, extension::TomlTableExt};
5use chrono::Local;
6use cron::Schedule;
7use std::{io, str::FromStr, time::Duration};
8use toml::Table;
9
10/// A function pointer of the async cron job.
11pub type AsyncCronJob = for<'a> fn(ctx: &'a mut JobContext) -> BoxFuture<'a>;
12
13/// An async schedulable job.
14pub struct AsyncJob {
15    /// Job context.
16    context: JobContext,
17    /// Cron expression parser.
18    schedule: Schedule,
19    /// Cron job to run.
20    run: AsyncCronJob,
21}
22
23impl AsyncJob {
24    /// Creates a new instance.
25    ///
26    /// # Panics
27    ///
28    /// Panics if the cron expression is invalid.
29    #[inline]
30    pub fn new(cron_expr: &str, exec: AsyncCronJob) -> Self {
31        let schedule = Schedule::from_str(cron_expr)
32            .unwrap_or_else(|err| panic!("invalid cron expression `{cron_expr}`: {err}"));
33        let mut context = JobContext::new();
34        context.set_source(cron_expr);
35        Self {
36            context,
37            schedule,
38            run: exec,
39        }
40    }
41
42    /// Creates a new instance with the configuration.
43    ///
44    /// # Panics
45    ///
46    /// Panics if the `cron` expression is invalid.
47    pub fn with_config(config: &Table, exec: AsyncCronJob) -> Self {
48        let cron_expr = config.get_str("cron").unwrap_or_default();
49        let schedule = Schedule::from_str(cron_expr)
50            .unwrap_or_else(|err| panic!("invalid cron expression `{cron_expr}`: {err}"));
51        let mut context = JobContext::new();
52        if let Some(disabled) = config.get_bool("disable") {
53            context.set_disabled_status(disabled);
54        }
55        if let Some(immediate) = config.get_bool("immediate") {
56            context.set_immediate_mode(immediate);
57        }
58        if let Some(ticks) = config
59            .get_bool("once")
60            .and_then(|b| b.then_some(1))
61            .or_else(|| config.get_usize("max-ticks"))
62        {
63            context.set_remaining_ticks(ticks);
64        }
65        Self {
66            context,
67            schedule,
68            run: exec,
69        }
70    }
71
72    /// Sets the job name.
73    #[inline]
74    pub fn name(mut self, name: &'static str) -> Self {
75        self.context.set_name(name);
76        self
77    }
78
79    /// Sets the initial job data.
80    #[inline]
81    pub fn data<T: Send + 'static>(mut self, data: T) -> Self {
82        self.context.set_data(data);
83        self
84    }
85
86    /// Sets the number of maximum ticks.
87    #[inline]
88    pub fn max_ticks(mut self, ticks: usize) -> Self {
89        self.context.set_remaining_ticks(ticks);
90        self
91    }
92
93    /// Sets the number of maximum ticks as `1` to ensure that the job can only be executed once.
94    #[inline]
95    pub fn once(mut self) -> Self {
96        self.context.set_remaining_ticks(1);
97        self
98    }
99
100    /// Enables the flag to indicate whether the job is disabled.
101    #[inline]
102    pub fn disable(mut self, disabled: bool) -> Self {
103        self.context.set_disabled_status(disabled);
104        self
105    }
106
107    /// Enables the flag to indicate whether the job is executed immediately.
108    #[inline]
109    pub fn immediate(mut self, immediate: bool) -> Self {
110        self.context.set_immediate_mode(immediate);
111        self
112    }
113
114    /// Pauses the job by setting the `disabled` flag to `true`.
115    #[inline]
116    pub fn pause(&mut self) {
117        self.context.set_disabled_status(true);
118    }
119
120    /// Resumes the job by setting the `disabled` flag to `false`.
121    #[inline]
122    pub fn resume(&mut self) {
123        self.context.set_disabled_status(false);
124    }
125
126    /// Executes the missed runs asynchronously.
127    pub async fn tick(&mut self) {
128        let now = Local::now();
129        let upcoming = self.upcoming();
130        let ctx = &mut self.context;
131        let run = self.run;
132        if ctx.is_immediate() && !ctx.is_disabled() && !ctx.is_fused() {
133            ctx.start();
134            ctx.set_next_tick(upcoming);
135            run(ctx).await;
136            ctx.finish();
137        } else if let Some(last_tick) = ctx.last_tick().map(|dt| dt.into()) {
138            for event in self.schedule.after(&last_tick) {
139                if event > now || ctx.is_fused() {
140                    break;
141                }
142                if !ctx.is_disabled() {
143                    ctx.start();
144                    ctx.set_next_tick(upcoming);
145                    run(ctx).await;
146                    ctx.finish();
147                }
148            }
149        } else {
150            ctx.set_last_tick(now.into());
151        }
152    }
153
154    /// Executes the job manually.
155    pub async fn execute(&mut self) {
156        let upcoming = self.upcoming();
157        let ctx = &mut self.context;
158        let run = self.run;
159        ctx.start();
160        ctx.set_next_tick(upcoming);
161        run(ctx).await;
162        ctx.finish();
163    }
164
165    /// Returns a reference to the job context.
166    #[inline]
167    pub fn context(&self) -> &JobContext {
168        &self.context
169    }
170
171    /// Returns a mutable reference to the job context.
172    #[inline]
173    pub fn context_mut(&mut self) -> &mut JobContext {
174        &mut self.context
175    }
176
177    /// Returns the date-time for upcoming runs.
178    #[inline]
179    pub fn upcoming(&self) -> Option<DateTime> {
180        self.schedule.upcoming(Local).next().map(|dt| dt.into())
181    }
182}
183
184/// A type contains and executes the async scheduled jobs.
185#[derive(Default)]
186pub struct AsyncJobScheduler {
187    /// A list of async jobs.
188    jobs: Vec<AsyncJob>,
189}
190
191impl AsyncJobScheduler {
192    /// Creates a new instance.
193    #[inline]
194    pub fn new() -> Self {
195        Self { jobs: Vec::new() }
196    }
197
198    /// Adds an async job to the scheduler and returns the job ID.
199    #[inline]
200    pub fn add(&mut self, job: AsyncJob) -> Uuid {
201        let job_id = job.context().job_id();
202        self.jobs.push(job);
203        job_id
204    }
205
206    /// Removes an async job by ID from the scheduler.
207    pub fn remove(&mut self, job_id: Uuid) -> bool {
208        let position = self
209            .jobs
210            .iter()
211            .position(|job| job.context().job_id() == job_id);
212        if let Some(index) = position {
213            self.jobs.remove(index);
214            true
215        } else {
216            false
217        }
218    }
219
220    /// Returns a reference to the job with the ID.
221    #[inline]
222    pub fn get(&self, job_id: Uuid) -> Option<&AsyncJob> {
223        self.jobs
224            .iter()
225            .find(|job| job.context().job_id() == job_id)
226    }
227
228    /// Returns a mutable reference to the job with the ID.
229    #[inline]
230    pub fn get_mut(&mut self, job_id: Uuid) -> Option<&mut AsyncJob> {
231        self.jobs
232            .iter_mut()
233            .find(|job| job.context().job_id() == job_id)
234    }
235
236    /// Returns the duration till the next job is supposed to run.
237    pub fn time_till_next_job(&self) -> Duration {
238        if self.jobs.is_empty() {
239            DEFAULT_TICK_INTERVAL
240        } else {
241            let mut duration = Duration::ZERO;
242            let now = Local::now();
243            for job in self.jobs.iter() {
244                if let Some(interval) = job
245                    .context()
246                    .next_tick()
247                    .and_then(|dt| dt.span_after_now())
248                    .filter(|interval| duration.is_zero() || interval < &duration)
249                {
250                    duration = interval;
251                }
252                for event in job.schedule.after(&now).take(1) {
253                    let interval = event - now;
254                    if let Ok(interval) = interval.to_std()
255                        && (duration.is_zero() || interval < duration)
256                    {
257                        duration = interval;
258                    }
259                }
260            }
261            duration.max(DEFAULT_TICK_INTERVAL)
262        }
263    }
264
265    /// Increments time for the scheduler and executes any pending jobs asynchronously.
266    /// It is recommended to sleep for at least 500 milliseconds between invocations of this method.
267    pub async fn tick(&mut self) {
268        let mut fused_jobs = Vec::new();
269        for job in &mut self.jobs {
270            job.tick().await;
271
272            let ctx = job.context();
273            if ctx.is_fused() {
274                fused_jobs.push(ctx.job_id());
275            }
276        }
277        for job_id in fused_jobs {
278            self.remove(job_id);
279        }
280    }
281
282    /// Executes all the jobs manually.
283    #[inline]
284    pub async fn execute(&mut self) {
285        for job in &mut self.jobs {
286            job.execute().await;
287        }
288    }
289}
290
291impl AsyncScheduler for AsyncJobScheduler {
292    #[inline]
293    fn is_ready(&self) -> bool {
294        !self.jobs.is_empty()
295    }
296
297    #[inline]
298    fn is_blocking(&self) -> bool {
299        false
300    }
301
302    #[inline]
303    fn time_till_next_job(&self) -> Option<Duration> {
304        Some(self.time_till_next_job())
305    }
306
307    #[inline]
308    async fn tick(&mut self) {
309        self.tick().await;
310    }
311
312    #[inline]
313    async fn run(self) -> io::Result<()> {
314        Ok(())
315    }
316}