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 fn is_running(&self) -> bool {
68 self.running.load(Ordering::SeqCst)
69 }
70
71 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 #[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 if job_scheduler.is_pending().await {
92 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 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 pub fn new_with_local_tz() -> JobExecutor {
146 Self::new_with_tz()
147 }
148
149 pub fn new_with_utc_tz() -> JobExecutor {
152 Self::new_with_tz()
153 }
154
155 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 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 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 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 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 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 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}