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
22// `cron` is pure date/time math (no threads) and stays available on every
23// target — `app::controller::static_resource` uses it unconditionally for
24// Last-Modified/cache-header formatting. `Scheduler` itself spawns one OS
25// thread per task and is native-only; see spec/WASM_SHIM.md.
26pub mod cron;
27pub use cron::CronSchedule;
28
29#[cfg(all(test, not(target_arch = "wasm32")))]
30mod tests;
31
32#[cfg(not(target_arch = "wasm32"))]
33use std::sync::Arc;
34#[cfg(not(target_arch = "wasm32"))]
35use std::time::{Duration, Instant};
36
37/// A `@Scheduled`-style background task runner.
38///
39/// Register tasks with `.every()`, `.after()`, or `.cron()`, then call
40/// `.start()` to spawn one dedicated background thread per task.
41#[cfg(not(target_arch = "wasm32"))]
42pub struct Scheduler {
43    tasks: Vec<Task>,
44}
45
46#[cfg(not(target_arch = "wasm32"))]
47struct Task {
48    kind: TaskKind,
49    initial_delay: Duration,
50    f: Arc<dyn Fn() + Send + Sync + 'static>,
51}
52
53#[cfg(not(target_arch = "wasm32"))]
54enum TaskKind {
55    /// Fixed rate — interval measured from the *start* of the previous run.
56    FixedRate(Duration),
57    /// Fixed delay — interval measured from the *end* of the previous run.
58    FixedDelay(Duration),
59    /// Cron — fires whenever the wall clock matches the parsed expression.
60    Cron(CronSchedule),
61}
62
63#[cfg(not(target_arch = "wasm32"))]
64impl Scheduler {
65    pub fn new() -> Self {
66        Scheduler { tasks: Vec::new() }
67    }
68
69    /// Run `task` every `interval`, measured from the start of the previous run.
70    /// If the task takes longer than `interval`, the next run starts immediately.
71    pub fn every(mut self, interval: Duration, task: impl Fn() + Send + Sync + 'static) -> Self {
72        self.tasks.push(Task {
73            kind: TaskKind::FixedRate(interval),
74            initial_delay: Duration::ZERO,
75            f: Arc::new(task),
76        });
77        self
78    }
79
80    /// Run `task` with `delay` between the end of one run and the start of the next.
81    pub fn after(mut self, delay: Duration, task: impl Fn() + Send + Sync + 'static) -> Self {
82        self.tasks.push(Task {
83            kind: TaskKind::FixedDelay(delay),
84            initial_delay: Duration::ZERO,
85            f: Arc::new(task),
86        });
87        self
88    }
89
90    /// Run `task` according to a 6-field cron expression.
91    ///
92    /// Format: `"second minute hour day-of-month month day-of-week"` (UTC).
93    ///
94    /// Each field supports `*`, an exact value, `*/step`, an `N-M` range, and
95    /// comma-separated combinations, e.g. `"0,30 * * * * *"` fires at seconds 0 and 30.
96    ///
97    /// Day-of-week: 0 = Sunday, 6 = Saturday.
98    pub fn cron(
99        mut self,
100        expr: &str,
101        task: impl Fn() + Send + Sync + 'static,
102    ) -> Result<Self, String> {
103        let schedule = CronSchedule::parse(expr)?;
104        self.tasks.push(Task {
105            kind: TaskKind::Cron(schedule),
106            initial_delay: Duration::ZERO,
107            f: Arc::new(task),
108        });
109        Ok(self)
110    }
111
112    /// Add an initial delay before the first run of the most recently registered task.
113    pub fn initial_delay(mut self, delay: Duration) -> Self {
114        if let Some(t) = self.tasks.last_mut() {
115            t.initial_delay = delay;
116        }
117        self
118    }
119
120    /// Spawn one background thread per registered task and return immediately.
121    /// Threads run for the lifetime of the process.
122    pub fn start(self) {
123        for task in self.tasks {
124            let f = task.f.clone();
125            let initial_delay = task.initial_delay;
126            std::thread::spawn(move || {
127                if !initial_delay.is_zero() {
128                    std::thread::sleep(initial_delay);
129                }
130                match task.kind {
131                    TaskKind::FixedRate(interval) => loop {
132                        let start = Instant::now();
133                        f();
134                        let elapsed = start.elapsed();
135                        if elapsed < interval {
136                            std::thread::sleep(interval - elapsed);
137                        }
138                    },
139                    TaskKind::FixedDelay(delay) => loop {
140                        f();
141                        std::thread::sleep(delay);
142                    },
143                    TaskKind::Cron(ref schedule) => {
144                        // Poll at 200 ms resolution; track last-fired second to avoid
145                        // double-firing when the task finishes within the same second.
146                        let mut last_fired_secs: u64 = 0;
147                        loop {
148                            let now_secs = std::time::SystemTime::now()
149                                .duration_since(std::time::UNIX_EPOCH)
150                                .unwrap_or_default()
151                                .as_secs();
152                            if now_secs != last_fired_secs
153                                && schedule.matches_epoch(now_secs)
154                            {
155                                last_fired_secs = now_secs;
156                                f();
157                            }
158                            std::thread::sleep(Duration::from_millis(200));
159                        }
160                    }
161                }
162            });
163        }
164    }
165}
166
167#[cfg(not(target_arch = "wasm32"))]
168impl Default for Scheduler {
169    fn default() -> Self {
170        Scheduler::new()
171    }
172}