Skip to main content

scheduler/
job.rs

1use std::{future::Future, pin::Pin, sync::Arc};
2
3use time::{DateTime, TimeZone};
4use tokio::sync::{Mutex, RwLock};
5#[cfg(feature = "tracing")]
6use tracing::*;
7
8use crate::{error::SchedulerError, scheduler::Scheduler};
9
10pub struct JobScheduler {
11    pub job: Job,
12    schedule: Mutex<Scheduler>,
13    next_run_at: Mutex<Option<DateTime>>,
14    last_run_at: Mutex<Option<DateTime>>,
15}
16
17impl JobScheduler {
18    pub fn new(mut schedule: Scheduler, job: Job) -> Self {
19        // Determine the next time it should run
20        let next_run_at = schedule.next(&DateTime::now_in(TimeZone::UTC));
21        JobScheduler {
22            job,
23            schedule: Mutex::new(schedule),
24            next_run_at: Mutex::new(next_run_at),
25            last_run_at: Mutex::new(None),
26        }
27    }
28
29    /// Returns true if this job is pending execution.
30    pub async fn is_pending(&self) -> bool {
31        // Check if paused
32        if !self.job.is_active {
33            return false;
34        }
35
36        // Check if NOW is on or after next_run_at
37        if let Some(next_run_at) = self.next_run_at.lock().await.as_ref() {
38            *next_run_at < DateTime::now_in(TimeZone::UTC)
39        } else {
40            false
41        }
42    }
43
44    /// Run the job immediately and re-schedule it.
45    pub async fn run(&self) -> Result<(), SchedulerError> {
46        // Execute the job function
47        let run_result = self.job.run().await;
48
49        let now = DateTime::now_in(TimeZone::UTC);
50
51        let mut schedule = self.schedule.lock().await;
52
53        // Determine the next time it should run
54        let mut next_run_at = self.next_run_at.lock().await;
55        *next_run_at = schedule.next(&now);
56
57        // Save the last time this ran
58        let mut last_run_at = self.last_run_at.lock().await;
59        *last_run_at = Some(now);
60
61        run_result
62    }
63}
64
65pub type JobFn = dyn 'static
66    + Send
67    + Sync
68    + Fn() -> Pin<Box<dyn Future<Output = Result<(), Box<dyn core::error::Error + Send + Sync>>> + Send>>;
69
70pub struct Job {
71    function: Arc<JobFn>,
72    group: String,
73    name: String,
74    is_active: bool,
75    is_running: RwLock<bool>,
76    retries_after_failure: Option<u64>,
77}
78
79impl Job {
80    pub fn new<
81        G: Into<String>,
82        N: Into<String>,
83        F: 'static
84            + Send
85            + Sync
86            + Fn() -> Pin<Box<dyn Future<Output = Result<(), Box<dyn core::error::Error + Send + Sync>>> + Send>>,
87    >(
88        group: G,
89        name: N,
90        retries_after_failure: Option<u64>,
91        function: F,
92    ) -> Self {
93        Job {
94            function: Arc::new(function),
95            name: name.into(),
96            group: group.into(),
97            retries_after_failure,
98            is_running: RwLock::new(false),
99            is_active: true,
100        }
101    }
102
103    /// Returns true if this job is currently running.
104    pub async fn is_running(&self) -> bool {
105        let read = self.is_running.read().await;
106        *read
107    }
108
109    pub fn name(&self) -> &str {
110        &self.name
111    }
112
113    pub fn group(&self) -> &str {
114        &self.group
115    }
116
117    /// Run the job immediately and re-schedule it.
118    // `attempt` and `e` are only read by the `tracing` macros below.
119    #[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
120    pub async fn run(&self) -> Result<(), SchedulerError> {
121        self.set_running(true).await?;
122
123        // Execute the job function
124        let mut run_result = self.exec().await;
125
126        if let Some(retries) = self.retries_after_failure {
127            for attempt in 1..=retries {
128                if let Err(e) = run_result {
129                    #[cfg(feature = "tracing")]
130                    warn!(
131                        "Execution failed for job [{}/{}] - Retry execution, attempt {}/{}. Previous err: {}",
132                        self.group, self.name, attempt, retries, e
133                    );
134                    run_result = self.exec().await;
135                } else {
136                    break;
137                }
138            }
139        }
140
141        self.set_running(false).await?;
142
143        run_result.map_err(|err| SchedulerError::JobExecutionError {
144            source: err,
145        })
146    }
147
148    async fn exec(&self) -> Result<(), Box<dyn core::error::Error + Send + Sync>> {
149        let function = self.function.clone();
150        (function)().await
151    }
152
153    async fn set_running(&self, is_running: bool) -> Result<(), SchedulerError> {
154        let mut write = self.is_running.write().await;
155
156        if is_running.eq(&*write) {
157            return Err(SchedulerError::JobLockError {
158                message: format!(
159                    "Wrong Job status found for job [{}/{}]. Expected: {}",
160                    self.group, self.name, !is_running
161                ),
162            });
163        }
164
165        *write = is_running;
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171pub mod test {
172
173    use std::{sync::Arc, time::Duration};
174
175    use tokio::sync::mpsc::channel;
176
177    use super::*;
178
179    #[tokio::test]
180    async fn should_be_running() {
181        let lock = Arc::new(Mutex::new(true));
182        let lock_clone = lock.clone();
183        let (tx, mut rx) = channel(10000);
184        let tx_clone = tx.clone();
185
186        let job_scheduler = Arc::new(JobScheduler::new(
187            Scheduler::Interval {
188                interval_duration: Duration::new(1, 0),
189                execute_at_startup: false,
190            },
191            Job::new("g", "n", None, move || {
192                let lock_clone = lock_clone.clone();
193                let tx_clone = tx_clone.clone();
194                Box::pin(async move {
195                    println!("job - started");
196                    tx_clone.send("").await.unwrap();
197                    println!("job - Trying to get the lock");
198                    let _lock = lock_clone.lock().await;
199                    println!("job - lock acquired");
200                    Ok(())
201                })
202            }),
203        ));
204
205        assert!(!job_scheduler.job.is_running().await);
206
207        {
208            let _lock = lock.lock().await;
209            let job_clone = job_scheduler.clone();
210            tokio::spawn(async move {
211                println!("starting job");
212                job_clone.run().await.unwrap();
213                println!("end job execution");
214                tx.send("").await.unwrap();
215            });
216            rx.recv().await.unwrap();
217            assert!(job_scheduler.job.is_running().await);
218        }
219
220        rx.recv().await.unwrap();
221        assert!(!job_scheduler.job.is_running().await);
222    }
223
224    #[tokio::test]
225    async fn job_should_not_retry_run_if_ok() {
226        let lock = Arc::new(Mutex::new(0));
227        let lock_clone = lock.clone();
228
229        let max_retries = 12;
230
231        let job = Job::new("g", "n", Some(max_retries), move || {
232            let lock_clone = lock_clone.clone();
233            Box::pin(async move {
234                println!("job - started");
235                println!("job - Trying to get the lock");
236                let mut lock = lock_clone.lock().await;
237                let count = *lock;
238                *lock = count + 1;
239                println!("job - count {count}");
240                Ok(())
241            })
242        });
243
244        let result = job.run().await;
245
246        assert!(result.is_ok());
247
248        let lock = lock.lock().await;
249        let count = *lock;
250        assert_eq!(1, count);
251    }
252
253    #[tokio::test]
254    async fn job_should_retry_run_if_error() {
255        let lock = Arc::new(Mutex::new(0));
256        let lock_clone = lock.clone();
257
258        let max_retries = 12;
259
260        let job = Job::new("g", "n", Some(max_retries), move || {
261            let lock_clone = lock_clone.clone();
262            Box::pin(async move {
263                println!("job - started");
264                println!("job - Trying to get the lock");
265                let mut lock = lock_clone.lock().await;
266                let count = *lock;
267                *lock = count + 1;
268                println!("job - count {count}");
269                Err(SchedulerError::JobLockError {
270                    message: "".to_owned(),
271                })?
272            })
273        });
274
275        let result = job.run().await;
276
277        assert!(result.is_err());
278
279        let lock = lock.lock().await;
280        let count = *lock;
281        assert_eq!(max_retries + 1, count);
282    }
283
284    #[tokio::test]
285    async fn job_should_stop_retrying_run_if_attempt_succeed() {
286        let lock = Arc::new(Mutex::new(0));
287        let lock_clone = lock.clone();
288
289        let succeed_at = 7;
290        let max_retries = 12;
291
292        let job = Job::new("g", "n", Some(max_retries), move || {
293            let lock_clone = lock_clone.clone();
294            Box::pin(async move {
295                println!("job - started");
296                println!("job - Trying to get the lock");
297                let mut lock = lock_clone.lock().await;
298                let count = *lock;
299                *lock = count + 1;
300                println!("job - count {count}");
301
302                if count == succeed_at {
303                    Ok(())
304                } else {
305                    Err(SchedulerError::JobLockError {
306                        message: "".to_owned(),
307                    })?
308                }
309            })
310        });
311
312        let result = job.run().await;
313
314        assert!(result.is_ok());
315
316        let lock = lock.lock().await;
317        let count = *lock;
318        assert_eq!(succeed_at + 1, count);
319    }
320}