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}