Skip to main content

rust_web_server/scheduler/
mod.rs

1//! Background task scheduler — a `@Scheduled`-style runner for fixed-rate,
2//! fixed-delay, and cron-expression tasks.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::time::Duration;
8//! use rust_web_server::scheduler::Scheduler;
9//!
10//! Scheduler::new()
11//!     // Runs every 60 s (interval measured from task start).
12//!     .every(Duration::from_secs(60), || println!("tick"))
13//!     // Waits 30 s after each run completes before starting the next.
14//!     .after(Duration::from_secs(30), || println!("heartbeat"))
15//!     // Fires at second 0 of every minute.
16//!     .cron("0 * * * * *", || println!("every minute")).unwrap()
17//!     // 10 s pause before the first run of the most recently added task.
18//!     .initial_delay(Duration::from_secs(10))
19//!     .start();
20//! ```
21
22pub mod cron;
23pub use cron::CronSchedule;
24
25#[cfg(test)]
26mod tests;
27
28use std::sync::Arc;
29use std::time::{Duration, Instant};
30
31/// A `@Scheduled`-style background task runner.
32///
33/// Register tasks with `.every()`, `.after()`, or `.cron()`, then call
34/// `.start()` to spawn one dedicated background thread per task.
35pub struct Scheduler {
36    tasks: Vec<Task>,
37}
38
39struct Task {
40    kind: TaskKind,
41    initial_delay: Duration,
42    f: Arc<dyn Fn() + Send + Sync + 'static>,
43}
44
45enum TaskKind {
46    /// Fixed rate — interval measured from the *start* of the previous run.
47    FixedRate(Duration),
48    /// Fixed delay — interval measured from the *end* of the previous run.
49    FixedDelay(Duration),
50    /// Cron — fires whenever the wall clock matches the parsed expression.
51    Cron(CronSchedule),
52}
53
54impl Scheduler {
55    pub fn new() -> Self {
56        Scheduler { tasks: Vec::new() }
57    }
58
59    /// Run `task` every `interval`, measured from the start of the previous run.
60    /// If the task takes longer than `interval`, the next run starts immediately.
61    pub fn every(mut self, interval: Duration, task: impl Fn() + Send + Sync + 'static) -> Self {
62        self.tasks.push(Task {
63            kind: TaskKind::FixedRate(interval),
64            initial_delay: Duration::ZERO,
65            f: Arc::new(task),
66        });
67        self
68    }
69
70    /// Run `task` with `delay` between the end of one run and the start of the next.
71    pub fn after(mut self, delay: Duration, task: impl Fn() + Send + Sync + 'static) -> Self {
72        self.tasks.push(Task {
73            kind: TaskKind::FixedDelay(delay),
74            initial_delay: Duration::ZERO,
75            f: Arc::new(task),
76        });
77        self
78    }
79
80    /// Run `task` according to a 6-field cron expression.
81    ///
82    /// Format: `"second minute hour day-of-month month day-of-week"` (UTC).
83    ///
84    /// Each field supports `*`, an exact value, `*/step`, an `N-M` range, and
85    /// comma-separated combinations, e.g. `"0,30 * * * * *"` fires at seconds 0 and 30.
86    ///
87    /// Day-of-week: 0 = Sunday, 6 = Saturday.
88    pub fn cron(
89        mut self,
90        expr: &str,
91        task: impl Fn() + Send + Sync + 'static,
92    ) -> Result<Self, String> {
93        let schedule = CronSchedule::parse(expr)?;
94        self.tasks.push(Task {
95            kind: TaskKind::Cron(schedule),
96            initial_delay: Duration::ZERO,
97            f: Arc::new(task),
98        });
99        Ok(self)
100    }
101
102    /// Add an initial delay before the first run of the most recently registered task.
103    pub fn initial_delay(mut self, delay: Duration) -> Self {
104        if let Some(t) = self.tasks.last_mut() {
105            t.initial_delay = delay;
106        }
107        self
108    }
109
110    /// Spawn one background thread per registered task and return immediately.
111    /// Threads run for the lifetime of the process.
112    pub fn start(self) {
113        for task in self.tasks {
114            let f = task.f.clone();
115            let initial_delay = task.initial_delay;
116            std::thread::spawn(move || {
117                if !initial_delay.is_zero() {
118                    std::thread::sleep(initial_delay);
119                }
120                match task.kind {
121                    TaskKind::FixedRate(interval) => loop {
122                        let start = Instant::now();
123                        f();
124                        let elapsed = start.elapsed();
125                        if elapsed < interval {
126                            std::thread::sleep(interval - elapsed);
127                        }
128                    },
129                    TaskKind::FixedDelay(delay) => loop {
130                        f();
131                        std::thread::sleep(delay);
132                    },
133                    TaskKind::Cron(ref schedule) => {
134                        // Poll at 200 ms resolution; track last-fired second to avoid
135                        // double-firing when the task finishes within the same second.
136                        let mut last_fired_secs: u64 = 0;
137                        loop {
138                            let now_secs = std::time::SystemTime::now()
139                                .duration_since(std::time::UNIX_EPOCH)
140                                .unwrap_or_default()
141                                .as_secs();
142                            if now_secs != last_fired_secs
143                                && schedule.matches_epoch(now_secs)
144                            {
145                                last_fired_secs = now_secs;
146                                f();
147                            }
148                            std::thread::sleep(Duration::from_millis(200));
149                        }
150                    }
151                }
152            });
153        }
154    }
155}
156
157impl Default for Scheduler {
158    fn default() -> Self {
159        Scheduler::new()
160    }
161}