Skip to main content

scheduler/
lib.rs

1use std::{
2    sync::{
3        Arc,
4        atomic::{AtomicBool, AtomicU64, Ordering},
5    },
6    time::Duration,
7};
8
9use time::{DateTime, TimeZone};
10use tokio::{sync::RwLock, task::JoinHandle};
11#[cfg(feature = "tracing")]
12use tracing::*;
13
14use crate::{
15    error::SchedulerError,
16    job::{Job, JobScheduler},
17    scheduler::{Scheduler, TryToScheduler},
18};
19
20pub mod error;
21pub mod job;
22pub mod scheduler;
23
24#[derive(Clone)]
25pub struct JobExecutor {
26    executor: Arc<JobExecutorInternal>,
27}
28
29struct JobExecutorInternal {
30    sleep_between_checks_ms: AtomicU64,
31    running: AtomicBool,
32    jobs: RwLock<Vec<Arc<JobScheduler>>>,
33}
34
35impl JobExecutorInternal {
36    /*
37        /// Returns true if the JobExecutor contains no jobs.
38        pub async fn is_empty(&self) -> bool {
39            let jobs = self.jobs.read().await;
40            jobs.is_empty()
41        }
42
43        /// Returns the number of jobs in the JobExecutor.
44        pub async fn len(&self) -> usize {
45            let jobs = self.jobs.read().await;
46            jobs.len()
47        }
48
49        /// Clear the JobExecutor, removing all jobs.
50        pub async fn clear(&mut self) {
51            let mut jobs = self.jobs.write().await;
52            jobs.clear()
53        }
54
55        /// Returns true if there is at least one job pending.
56        pub async fn is_pending_job(&self) -> bool {
57            let jobs = self.jobs.read().await;
58            for job_scheduler in jobs.iter() {
59                if job_scheduler.is_pending().await {
60                    return true;
61                }
62            }
63            false
64        }
65    */
66    /// Returns true if the Job Executor is running
67    fn is_running(&self) -> bool {
68        self.running.load(Ordering::SeqCst)
69    }
70
71    /// Returns true if there is at least one job running.
72    async fn is_running_job(&self) -> bool {
73        let jobs = self.jobs.read().await;
74        for job_scheduler in jobs.iter() {
75            if job_scheduler.job.is_running().await {
76                return true;
77            }
78        }
79        false
80    }
81
82    /// Run pending jobs in the JobExecutor.
83    // `duration` and `err` are only read by the `tracing` macros below.
84    #[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
85    async fn run_pending_jobs(&self) {
86        #[cfg(feature = "tracing")]
87        trace!("Check pending jobs");
88        let jobs = self.jobs.read().await;
89        for job_scheduler in jobs.iter() {
90            //println!("check JOB IS PENDING: {}", job.is_pending());
91            if job_scheduler.is_pending().await {
92                //println!("JOB IS RUNNING? {}", is_running);
93                if !job_scheduler.job.is_running().await {
94                    let job_clone = job_scheduler.clone();
95
96                    let timestamp = DateTime::now_in(TimeZone::UTC).unix();
97                    let group = job_clone.job.group().to_owned();
98                    let name = job_clone.job.name().to_owned();
99
100                    let fut = instrument(timestamp, group.clone(), name.clone(), async move {
101                        #[cfg(feature = "tracing")]
102                        info!("scheduler: starting task {name}");
103                        let start = std::time::Instant::now();
104                        let result = job_clone.run().await;
105
106                        let duration = start.elapsed();
107
108                        match result {
109                            Ok(()) => {
110                                #[cfg(feature = "tracing")]
111                                info!("scheduler: task {name} completed successfully in {duration:?}")
112                            }
113                            Err(err) => {
114                                #[cfg(feature = "tracing")]
115                                error!("scheduler: task {name} failed in {duration:?} with errors: Err: {err:?}")
116                            }
117                        }
118                    });
119
120                    tokio::spawn(fut);
121                } else {
122                    #[cfg(feature = "tracing")]
123                    debug!(
124                        "Job [{}/{}] is pending but already running. It will not be executed.",
125                        job_scheduler.job.group(),
126                        job_scheduler.job.name()
127                    )
128                }
129            }
130        }
131    }
132
133    /// Adds a job to the JobExecutor.
134    async fn add_job_with_scheduler<S: Into<Scheduler>>(&self, schedule: S, job: Job) {
135        #[cfg(feature = "tracing")]
136        debug!("Add job to scheduler. Group [{}] - Name [{}]", job.group(), job.name());
137        let mut jobs = self.jobs.write().await;
138        jobs.push(Arc::new(JobScheduler::new(schedule.into(), job)));
139    }
140}
141
142impl JobExecutor {
143    /// Creates a new Executor that uses the Local time zone for the execution times evaluation.
144    /// For example, the cron expressions will refer to the Local time zone.
145    pub fn new_with_local_tz() -> JobExecutor {
146        Self::new_with_tz()
147    }
148
149    /// Creates a new Executor that uses the UTC time zone for the execution times evaluation.
150    /// For example, the cron expressions will refer to the UTC time zone.
151    pub fn new_with_utc_tz() -> JobExecutor {
152        Self::new_with_tz()
153    }
154
155    /// Creates a new Executor that uses a custom time zone for the execution times evaluation.
156    /// For example, the cron expressions will refer to the specified time zone.
157    pub fn new_with_tz() -> JobExecutor {
158        JobExecutor {
159            executor: Arc::new(JobExecutorInternal {
160                sleep_between_checks_ms: AtomicU64::new(1_000),
161                running: AtomicBool::new(false),
162                jobs: RwLock::new(vec![]),
163            }),
164        }
165    }
166
167    /// Adds a job to the JobExecutor.
168    pub async fn add_job(&self, schedule: &dyn TryToScheduler, job: Job) -> Result<(), SchedulerError> {
169        self.add_job_with_scheduler(schedule.to_scheduler()?, job).await;
170        Ok(())
171    }
172
173    /// Adds a job to the JobExecutor.
174    pub async fn add_job_with_multi_schedule(
175        &self,
176        schedule: &[&dyn TryToScheduler],
177        job: Job,
178    ) -> Result<(), SchedulerError> {
179        self.add_job_with_scheduler(schedule.to_scheduler()?, job).await;
180        Ok(())
181    }
182
183    /// Adds a job to the JobExecutor.
184    pub async fn add_job_with_scheduler<S: Into<Scheduler>>(&self, schedule: S, job: Job) {
185        self.executor.add_job_with_scheduler(schedule, job).await
186    }
187
188    /// Starts the JobExecutor
189    pub async fn run(&self) -> Result<JoinHandle<()>, SchedulerError> {
190        let was_running = self.executor.running.swap(true, Ordering::SeqCst);
191        if !was_running {
192            let executor = self.executor.clone();
193            Ok(tokio::spawn(async move {
194                #[cfg(feature = "tracing")]
195                debug!("Starting the job executor");
196                while executor.is_running() {
197                    executor.run_pending_jobs().await;
198                    tokio::time::sleep(Duration::from_millis(executor.sleep_between_checks_ms.load(Ordering::SeqCst)))
199                        .await;
200                }
201                #[cfg(feature = "tracing")]
202                debug!("Job executor stopped");
203            }))
204        } else {
205            #[cfg(feature = "tracing")]
206            warn!("The JobExecutor is already running.");
207            Err(SchedulerError::JobExecutionStateError {
208                message: "The JobExecutor is already running.".to_owned(),
209            })
210        }
211    }
212
213    /// Stops the JobExecutor
214    pub async fn stop(&self, graceful: bool) -> Result<(), SchedulerError> {
215        let was_running = self.executor.running.swap(false, Ordering::SeqCst);
216        if was_running {
217            #[cfg(feature = "tracing")]
218            debug!("Stopping the job executor");
219            if graceful {
220                #[cfg(feature = "tracing")]
221                debug!("Wait for all Jobs to complete");
222                while self.executor.is_running_job().await {
223                    tokio::time::sleep(Duration::from_millis(
224                        self.executor.sleep_between_checks_ms.load(Ordering::SeqCst),
225                    ))
226                    .await;
227                }
228                #[cfg(feature = "tracing")]
229                debug!("All Jobs completed");
230            }
231            Ok(())
232        } else {
233            #[cfg(feature = "tracing")]
234            warn!("The JobExecutor is not running.");
235            Err(SchedulerError::JobExecutionStateError {
236                message: "The JobExecutor is not running.".to_owned(),
237            })
238        }
239    }
240
241    /// Sets the sleep time between checks for pending Jobs.
242    /// The default is 1 second.
243    pub fn set_sleep_between_checks(&self, sleep_ms: u64) {
244        self.executor.sleep_between_checks_ms.store(sleep_ms, Ordering::SeqCst);
245    }
246}
247
248#[cfg(feature = "tracing")]
249fn instrument<F: std::future::Future<Output = ()>>(
250    timestamp: i64,
251    group: String,
252    name: String,
253    fut: F,
254) -> impl std::future::Future<Output = ()> {
255    let span = tracing::error_span!("run_pending", group, name, timestamp);
256    fut.instrument(span)
257}
258
259#[cfg(not(feature = "tracing"))]
260fn instrument<F: std::future::Future<Output = ()>>(
261    _timestamp: i64,
262    _group: String,
263    _name: String,
264    fut: F,
265) -> impl std::future::Future<Output = ()> {
266    fut
267}
268
269#[cfg(test)]
270pub mod test {
271
272    use std::{
273        sync::atomic::{AtomicUsize, Ordering},
274        time::Duration,
275    };
276
277    use time::{DateTime, TimeZone};
278    use tokio::sync::mpsc::channel;
279
280    use super::*;
281
282    #[tokio::test]
283    async fn should_not_run_an_already_running_job() {
284        let executor = JobExecutor::new_with_utc_tz();
285
286        let count = Arc::new(AtomicUsize::new(0));
287        let count_clone = count.clone();
288
289        let (tx, mut rx) = channel(1000);
290
291        executor
292            .add_job(
293                &Duration::new(0, 1),
294                Job::new("g", "n", None, move || {
295                    let count_clone = count_clone.clone();
296                    let tx = tx.clone();
297                    Box::pin(async move {
298                        tx.send("").await.unwrap();
299                        println!("job - started");
300                        count_clone.fetch_add(1, Ordering::SeqCst);
301                        tokio::time::sleep(Duration::new(1, 0)).await;
302                        Ok(())
303                    })
304                }),
305            )
306            .await
307            .unwrap();
308
309        for i in 0..100 {
310            println!("run_pending {i}");
311            executor.executor.run_pending_jobs().await;
312            tokio::time::sleep(Duration::new(0, 2)).await;
313        }
314
315        println!("run_pending completed");
316        rx.recv().await.unwrap();
317
318        assert_eq!(count.load(Ordering::Relaxed), 1);
319    }
320
321    #[tokio::test]
322    async fn a_running_job_should_not_block_the_executor() {
323        let executor = JobExecutor::new_with_local_tz();
324
325        let (tx, mut rx) = channel(959898);
326
327        let count_1 = Arc::new(AtomicUsize::new(0));
328        let count_1_clone = count_1.clone();
329        let tx_1 = tx.clone();
330        executor
331            .add_job_with_multi_schedule(
332                &[&Duration::new(0, 1)],
333                Job::new("g", "n", None, move || {
334                    let count_1_clone = count_1_clone.clone();
335                    let tx_1 = tx_1.clone();
336                    Box::pin(async move {
337                        tx_1.send("").await.unwrap();
338                        println!("job 1 - started");
339                        count_1_clone.fetch_add(1, Ordering::SeqCst);
340                        tokio::time::sleep(Duration::new(1, 0)).await;
341                        Ok(())
342                    })
343                }),
344            )
345            .await
346            .unwrap();
347
348        let count_2 = Arc::new(AtomicUsize::new(0));
349        let count_2_clone = count_2.clone();
350        let tx_2 = tx.clone();
351        executor
352            .add_job(
353                &Duration::new(0, 1),
354                Job::new("g", "n", None, move || {
355                    let count_2_clone = count_2_clone.clone();
356                    let tx_2 = tx_2.clone();
357                    Box::pin(async move {
358                        tx_2.send("").await.unwrap();
359                        println!("job 2 - started");
360                        count_2_clone.fetch_add(1, Ordering::SeqCst);
361                        tokio::time::sleep(Duration::new(1, 0)).await;
362                        Ok(())
363                    })
364                }),
365            )
366            .await
367            .unwrap();
368
369        let count_3 = Arc::new(AtomicUsize::new(0));
370        let count_3_clone = count_3.clone();
371        let tx_3 = tx.clone();
372        executor
373            .add_job(
374                &Duration::new(0, 1),
375                Job::new("g", "n", None, move || {
376                    let count_3_clone = count_3_clone.clone();
377                    let tx_3 = tx_3.clone();
378                    Box::pin(async move {
379                        tx_3.send("").await.unwrap();
380                        println!("job 3 - started");
381                        count_3_clone.fetch_add(1, Ordering::SeqCst);
382                        tokio::time::sleep(Duration::new(1, 0)).await;
383                        Ok(())
384                    })
385                }),
386            )
387            .await
388            .unwrap();
389
390        let before_millis = DateTime::now_in(TimeZone::UTC).unix_nanos() / 1_000_000;
391        for i in 0..100 {
392            println!("run_pending {i}");
393            executor.executor.run_pending_jobs().await;
394            tokio::time::sleep(Duration::new(0, 1_000_000)).await;
395        }
396        let after_millis = DateTime::now_in(TimeZone::UTC).unix_nanos() / 1_000_000;
397
398        assert!((after_millis - before_millis) >= 100);
399        assert!((after_millis - before_millis) < 1000);
400
401        rx.recv().await.unwrap();
402
403        assert_eq!(count_1.load(Ordering::SeqCst), 1);
404        assert_eq!(count_2.load(Ordering::SeqCst), 1);
405        assert_eq!(count_3.load(Ordering::SeqCst), 1);
406    }
407
408    #[tokio::test]
409    async fn should_gracefully_shutdown_the_job_executor() {
410        let executor = JobExecutor::new_with_utc_tz();
411
412        let count = Arc::new(AtomicUsize::new(0));
413
414        let tasks = 100;
415
416        for _i in 0..tasks {
417            let count_clone = count.clone();
418            executor
419                .add_job(
420                    &Duration::new(0, 1),
421                    Job::new("g", "n", None, move || {
422                        let count_clone = count_clone.clone();
423                        Box::pin(async move {
424                            tokio::time::sleep(Duration::new(1, 0)).await;
425                            println!("job - started");
426                            count_clone.fetch_add(1, Ordering::SeqCst);
427                            Ok(())
428                        })
429                    }),
430                )
431                .await
432                .unwrap();
433        }
434
435        executor.set_sleep_between_checks(10);
436
437        executor.run().await.unwrap();
438
439        loop {
440            if executor.executor.is_running_job().await {
441                break;
442            }
443            tokio::time::sleep(Duration::from_nanos(1)).await;
444        }
445
446        executor.stop(true).await.unwrap();
447
448        assert_eq!(count.load(Ordering::Relaxed), tasks);
449    }
450
451    #[tokio::test]
452    async fn start_should_fail_if_already_running() {
453        let executor = JobExecutor::new_with_utc_tz();
454        assert!(executor.run().await.is_ok());
455        assert!(executor.run().await.is_err());
456        assert!(executor.stop(false).await.is_ok());
457    }
458
459    #[tokio::test]
460    async fn stop_should_fail_if_not_running() {
461        let executor = JobExecutor::new_with_utc_tz();
462        assert!(executor.stop(false).await.is_err());
463        assert!(executor.run().await.is_ok());
464        assert!(executor.stop(false).await.is_ok());
465        assert!(executor.stop(false).await.is_err());
466    }
467
468    #[tokio::test]
469    async fn should_add_with_explicit_scheduler() {
470        let executor = JobExecutor::new_with_utc_tz();
471        executor
472            .add_job_with_scheduler(Scheduler::Never, Job::new("g", "n", None, move || Box::pin(async { Ok(()) })))
473            .await;
474    }
475
476    #[tokio::test]
477    async fn should_register_a_schedule_by_vec() {
478        let executor = JobExecutor::new_with_utc_tz();
479        executor
480            .add_job(
481                &vec!["0 1 * * * * *"],
482                Job::new("g", "n", None, move || Box::pin(async { Ok(()) })),
483            )
484            .await
485            .unwrap();
486        executor
487            .add_job(
488                &vec!["0 1 * * * * *".to_owned(), "0 1 * * * * *".to_owned()],
489                Job::new("g", "n", None, move || Box::pin(async { Ok(()) })),
490            )
491            .await
492            .unwrap();
493    }
494}