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}