oxigdal_workflow/scheduler/
cron.rs1use crate::error::{Result, WorkflowError};
4use crate::scheduler::SchedulerConfig;
5use chrono::{DateTime, Utc};
6use cron::Schedule as CronScheduleParser;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CronSchedule {
13 pub expression: String,
15 pub timezone: String,
17 pub description: Option<String>,
19}
20
21impl CronSchedule {
22 pub fn new<S: Into<String>>(expression: S) -> Result<Self> {
24 let expr = expression.into();
25
26 CronScheduleParser::from_str(&expr).map_err(|e| {
28 WorkflowError::cron_expression(format!("Invalid cron expression '{}': {}", expr, e))
29 })?;
30
31 Ok(Self {
32 expression: expr,
33 timezone: "UTC".to_string(),
34 description: None,
35 })
36 }
37
38 pub fn with_timezone<S: Into<String>>(mut self, timezone: S) -> Self {
40 self.timezone = timezone.into();
41 self
42 }
43
44 pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
46 self.description = Some(description.into());
47 self
48 }
49
50 pub fn next_execution_from(&self, from: DateTime<Utc>) -> Result<Option<DateTime<Utc>>> {
52 let schedule = CronScheduleParser::from_str(&self.expression).map_err(|e| {
53 WorkflowError::cron_expression(format!("Invalid cron expression: {}", e))
54 })?;
55
56 Ok(schedule.after(&from).next())
57 }
58
59 pub fn executions_in_range(
61 &self,
62 start: DateTime<Utc>,
63 end: DateTime<Utc>,
64 max_count: usize,
65 ) -> Result<Vec<DateTime<Utc>>> {
66 let schedule = CronScheduleParser::from_str(&self.expression).map_err(|e| {
67 WorkflowError::cron_expression(format!("Invalid cron expression: {}", e))
68 })?;
69
70 let mut executions = Vec::new();
71 for datetime in schedule.after(&start).take(max_count) {
72 if datetime > end {
73 break;
74 }
75 executions.push(datetime);
76 }
77
78 Ok(executions)
79 }
80
81 pub fn should_execute_at(&self, time: DateTime<Utc>) -> Result<bool> {
83 let next = self.next_execution_from(
84 time - chrono::Duration::try_seconds(2)
85 .ok_or_else(|| WorkflowError::internal("Duration overflow"))?,
86 )?;
87
88 if let Some(next_time) = next {
89 let diff = (next_time - time).num_seconds().abs();
90 Ok(diff <= 1)
91 } else {
92 Ok(false)
93 }
94 }
95}
96
97pub struct CronScheduler {
99 config: SchedulerConfig,
100}
101
102impl CronScheduler {
103 pub fn new(config: SchedulerConfig) -> Self {
105 Self { config }
106 }
107
108 pub fn calculate_next_execution(
110 &self,
111 expression: &str,
112 from: DateTime<Utc>,
113 ) -> Result<Option<DateTime<Utc>>> {
114 let schedule = CronScheduleParser::from_str(expression).map_err(|e| {
115 WorkflowError::cron_expression(format!(
116 "Invalid cron expression '{}': {}",
117 expression, e
118 ))
119 })?;
120
121 Ok(schedule.after(&from).next())
122 }
123
124 pub fn calculate_missed_executions(
126 &self,
127 expression: &str,
128 last_execution: DateTime<Utc>,
129 now: DateTime<Utc>,
130 ) -> Result<Vec<DateTime<Utc>>> {
131 if !self.config.handle_missed_executions {
132 return Ok(Vec::new());
133 }
134
135 let schedule = CronScheduleParser::from_str(expression).map_err(|e| {
136 WorkflowError::cron_expression(format!("Invalid cron expression: {}", e))
137 })?;
138
139 let mut missed = Vec::new();
140 for datetime in schedule
141 .after(&last_execution)
142 .take(self.config.max_missed_executions)
143 {
144 if datetime >= now {
145 break;
146 }
147 missed.push(datetime);
148 }
149
150 Ok(missed)
151 }
152
153 pub fn validate_expression(expression: &str) -> Result<()> {
155 CronScheduleParser::from_str(expression).map_err(|e| {
156 WorkflowError::cron_expression(format!(
157 "Invalid cron expression '{}': {}",
158 expression, e
159 ))
160 })?;
161 Ok(())
162 }
163
164 pub fn describe_expression(expression: &str) -> Result<String> {
166 Self::validate_expression(expression)?;
168
169 Ok(format!("Cron schedule: {}", expression))
171 }
172}
173
174pub struct CronPatterns;
176
177impl CronPatterns {
178 pub fn every_minute() -> &'static str {
180 "0 * * * * *"
181 }
182
183 pub fn every_5_minutes() -> &'static str {
185 "0 */5 * * * *"
186 }
187
188 pub fn every_15_minutes() -> &'static str {
190 "0 */15 * * * *"
191 }
192
193 pub fn every_30_minutes() -> &'static str {
195 "0 */30 * * * *"
196 }
197
198 pub fn every_hour() -> &'static str {
200 "0 0 * * * *"
201 }
202
203 pub fn daily() -> &'static str {
205 "0 0 0 * * *"
206 }
207
208 pub fn daily_at_noon() -> &'static str {
210 "0 0 12 * * *"
211 }
212
213 pub fn weekly() -> &'static str {
215 "0 0 0 * * 0"
216 }
217
218 pub fn monthly() -> &'static str {
220 "0 0 0 1 * *"
221 }
222
223 pub fn yearly() -> &'static str {
225 "0 0 0 1 1 *"
226 }
227
228 pub fn weekdays_at_9am() -> &'static str {
230 "0 0 9 * * 1-5"
231 }
232
233 pub fn weekends_at_10am() -> &'static str {
235 "0 0 10 * * 0,6"
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_cron_schedule_creation() {
245 let schedule = CronSchedule::new("0 0 0 * * *").expect("Failed to create schedule");
246 assert_eq!(schedule.expression, "0 0 0 * * *");
247 assert_eq!(schedule.timezone, "UTC");
248 }
249
250 #[test]
251 fn test_invalid_cron_expression() {
252 let result = CronSchedule::new("invalid");
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn test_next_execution() {
258 let schedule = CronSchedule::new("0 0 0 * * *").expect("Failed to create schedule");
259 let now = Utc::now();
260 let next = schedule
261 .next_execution_from(now)
262 .expect("Failed to calculate next execution");
263 assert!(next.is_some());
264 }
265
266 #[test]
267 fn test_cron_patterns() {
268 assert_eq!(CronPatterns::every_minute(), "0 * * * * *");
269 assert_eq!(CronPatterns::daily(), "0 0 0 * * *");
270 assert_eq!(CronPatterns::weekly(), "0 0 0 * * 0");
271 }
272
273 #[test]
274 fn test_validate_expression() {
275 assert!(CronScheduler::validate_expression("0 0 0 * * *").is_ok());
276 assert!(CronScheduler::validate_expression("invalid").is_err());
277 }
278
279 #[test]
280 fn test_executions_in_range() {
281 let schedule = CronSchedule::new("0 0 * * * *").expect("Failed to create schedule");
282 let start = Utc::now();
283 let end = start + chrono::Duration::try_hours(5).expect("Duration overflow");
284
285 let executions = schedule
286 .executions_in_range(start, end, 10)
287 .expect("Failed to get executions");
288
289 assert!(!executions.is_empty());
290 assert!(executions.len() <= 10);
291 }
292}