1use crate::error::{Result, WorkflowError};
4use crate::workflow::{Workflow, WorkflowId};
5use chrono::{DateTime, Utc};
6use cron::Schedule;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::str::FromStr;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info};
14
15pub trait Clock: Send + Sync {
22 fn now(&self) -> chrono::DateTime<chrono::Utc>;
24}
25
26#[derive(Debug, Clone, Copy, Default)]
28pub struct SystemClock;
29
30impl Clock for SystemClock {
31 fn now(&self) -> chrono::DateTime<chrono::Utc> {
32 chrono::Utc::now()
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum Trigger {
40 Cron {
42 expression: String,
44 #[serde(default)]
46 timezone: String,
47 },
48
49 WatchFolder {
51 path: PathBuf,
53 #[serde(default)]
55 pattern: String,
56 #[serde(default)]
58 recursive: bool,
59 },
60
61 Manual,
63
64 Time {
66 at: DateTime<Utc>,
68 },
69
70 Event {
72 event_type: String,
74 #[serde(default)]
76 conditions: HashMap<String, String>,
77 },
78
79 Interval {
81 seconds: u64,
83 },
84}
85
86#[derive(Debug, Clone)]
88pub struct ScheduledWorkflow {
89 pub workflow: Workflow,
91 pub trigger: Trigger,
93 pub enabled: bool,
95 pub last_execution: Option<DateTime<Utc>>,
97 pub next_execution: Option<DateTime<Utc>>,
99}
100
101impl ScheduledWorkflow {
102 #[must_use]
104 pub fn new(workflow: Workflow, trigger: Trigger) -> Self {
105 let next_execution = Self::calculate_next_execution(&trigger, None);
106 Self {
107 workflow,
108 trigger,
109 enabled: true,
110 last_execution: None,
111 next_execution,
112 }
113 }
114
115 fn calculate_next_execution(
117 trigger: &Trigger,
118 after: Option<DateTime<Utc>>,
119 ) -> Option<DateTime<Utc>> {
120 let now = after.unwrap_or_else(Utc::now);
121
122 match trigger {
123 Trigger::Cron { expression, .. } => {
124 if let Ok(schedule) = Schedule::from_str(expression) {
125 schedule.after(&now).next()
126 } else {
127 None
128 }
129 }
130 Trigger::Time { at } => {
131 if *at > now {
132 Some(*at)
133 } else {
134 None
135 }
136 }
137 Trigger::Interval { seconds } => {
138 Some(now + chrono::Duration::seconds(i64::try_from(*seconds).unwrap_or(60)))
139 }
140 Trigger::Manual | Trigger::WatchFolder { .. } | Trigger::Event { .. } => None,
141 }
142 }
143
144 pub fn update_next_execution(&mut self) {
146 self.update_next_execution_at(Utc::now());
147 }
148
149 pub(crate) fn update_next_execution_at(&mut self, now: DateTime<Utc>) {
154 self.last_execution = Some(now);
155 self.next_execution = Self::calculate_next_execution(&self.trigger, self.last_execution);
156 }
157
158 #[must_use]
160 pub fn should_execute(&self) -> bool {
161 self.should_execute_at(Utc::now())
162 }
163
164 #[must_use]
169 pub(crate) fn should_execute_at(&self, now: DateTime<Utc>) -> bool {
170 if !self.enabled {
171 return false;
172 }
173
174 match self.next_execution {
175 Some(next) => now >= next,
176 None => false,
177 }
178 }
179}
180
181pub struct WorkflowScheduler {
183 schedules: Arc<RwLock<HashMap<WorkflowId, ScheduledWorkflow>>>,
185 running: Arc<RwLock<bool>>,
187 clock: Arc<dyn Clock>,
189}
190
191impl WorkflowScheduler {
192 #[must_use]
194 pub fn new() -> Self {
195 Self::with_clock(Arc::new(SystemClock))
196 }
197
198 #[must_use]
204 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
205 Self {
206 schedules: Arc::new(RwLock::new(HashMap::new())),
207 running: Arc::new(RwLock::new(false)),
208 clock,
209 }
210 }
211
212 pub async fn add_schedule(&self, workflow: Workflow, trigger: Trigger) -> Result<WorkflowId> {
214 let workflow_id = workflow.id;
215 let scheduled = ScheduledWorkflow::new(workflow, trigger);
216
217 let mut schedules = self.schedules.write().await;
218 schedules.insert(workflow_id, scheduled);
219
220 info!("Added scheduled workflow: {}", workflow_id);
221 Ok(workflow_id)
222 }
223
224 pub async fn remove_schedule(&self, workflow_id: WorkflowId) -> Result<()> {
226 let mut schedules = self.schedules.write().await;
227 schedules.remove(&workflow_id);
228 info!("Removed scheduled workflow: {}", workflow_id);
229 Ok(())
230 }
231
232 pub async fn set_schedule_enabled(&self, workflow_id: WorkflowId, enabled: bool) -> Result<()> {
234 let mut schedules = self.schedules.write().await;
235 if let Some(schedule) = schedules.get_mut(&workflow_id) {
236 schedule.enabled = enabled;
237 debug!("Schedule {} enabled: {}", workflow_id, enabled);
238 Ok(())
239 } else {
240 Err(WorkflowError::WorkflowNotFound(workflow_id.to_string()))
241 }
242 }
243
244 pub async fn list_schedules(&self) -> Vec<(WorkflowId, ScheduledWorkflow)> {
246 let schedules = self.schedules.read().await;
247 schedules
248 .iter()
249 .map(|(id, sched)| (*id, sched.clone()))
250 .collect()
251 }
252
253 pub async fn start(&self) -> Result<()> {
255 let mut running = self.running.write().await;
256 if *running {
257 return Err(WorkflowError::AlreadyRunning("Scheduler".to_string()));
258 }
259 *running = true;
260 drop(running);
261
262 info!("Scheduler started");
263 Ok(())
264 }
265
266 pub async fn stop(&self) -> Result<()> {
268 let mut running = self.running.write().await;
269 if !*running {
270 return Err(WorkflowError::NotRunning("Scheduler".to_string()));
271 }
272 *running = false;
273
274 info!("Scheduler stopped");
275 Ok(())
276 }
277
278 pub async fn check_schedules(&self) -> Vec<Workflow> {
280 let running = self.running.read().await;
281 if !*running {
282 return Vec::new();
283 }
284 drop(running);
285
286 let now = self.clock.now();
289
290 let mut schedules = self.schedules.write().await;
291 let mut ready_workflows = Vec::new();
292
293 for (_, schedule) in schedules.iter_mut() {
294 if schedule.should_execute_at(now) {
295 ready_workflows.push(schedule.workflow.clone());
296 schedule.update_next_execution_at(now);
297 debug!(
298 "Workflow {} ready for execution. Next: {:?}",
299 schedule.workflow.id, schedule.next_execution
300 );
301 }
302 }
303
304 ready_workflows
305 }
306
307 pub async fn get_next_execution(&self, workflow_id: WorkflowId) -> Option<DateTime<Utc>> {
309 let schedules = self.schedules.read().await;
310 schedules.get(&workflow_id).and_then(|s| s.next_execution)
311 }
312}
313
314impl Default for WorkflowScheduler {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320pub struct FileWatcher {
322 watches: Arc<RwLock<HashMap<PathBuf, Vec<WorkflowId>>>>,
324}
325
326impl FileWatcher {
327 #[must_use]
329 pub fn new() -> Self {
330 Self {
331 watches: Arc::new(RwLock::new(HashMap::new())),
332 }
333 }
334
335 pub async fn add_watch(&self, path: PathBuf, workflow_id: WorkflowId) -> Result<()> {
337 let mut watches = self.watches.write().await;
338 watches.entry(path.clone()).or_default().push(workflow_id);
339 info!("Added file watch: {:?} -> {}", path, workflow_id);
340 Ok(())
341 }
342
343 pub async fn remove_watch(&self, path: &PathBuf, workflow_id: WorkflowId) -> Result<()> {
345 let mut watches = self.watches.write().await;
346 if let Some(workflows) = watches.get_mut(path) {
347 workflows.retain(|id| *id != workflow_id);
348 if workflows.is_empty() {
349 watches.remove(path);
350 }
351 }
352 info!("Removed file watch: {:?} -> {}", path, workflow_id);
353 Ok(())
354 }
355
356 pub async fn get_workflows_for_path(&self, path: &PathBuf) -> Vec<WorkflowId> {
358 let watches = self.watches.read().await;
359 watches.get(path).cloned().unwrap_or_default()
360 }
361}
362
363impl Default for FileWatcher {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_cron_trigger_parsing() {
375 let trigger = Trigger::Cron {
376 expression: "0 0 * * * *".to_string(), timezone: "UTC".to_string(),
378 };
379
380 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
381 assert!(next.is_some());
382 }
383
384 #[test]
385 fn test_interval_trigger() {
386 let trigger = Trigger::Interval { seconds: 60 };
387 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
388 assert!(next.is_some());
389 }
390
391 #[test]
392 fn test_time_trigger_future() {
393 let future_time = Utc::now() + chrono::Duration::hours(1);
394 let trigger = Trigger::Time { at: future_time };
395 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
396 assert_eq!(next, Some(future_time));
397 }
398
399 #[test]
400 fn test_time_trigger_past() {
401 let past_time = Utc::now() - chrono::Duration::hours(1);
402 let trigger = Trigger::Time { at: past_time };
403 let next = ScheduledWorkflow::calculate_next_execution(&trigger, None);
404 assert!(next.is_none());
405 }
406
407 #[test]
408 fn test_scheduled_workflow_creation() {
409 let workflow = Workflow::new("test");
410 let trigger = Trigger::Manual;
411 let scheduled = ScheduledWorkflow::new(workflow, trigger);
412
413 assert!(scheduled.enabled);
414 assert!(scheduled.last_execution.is_none());
415 }
416
417 #[tokio::test]
418 async fn test_scheduler_creation() {
419 let scheduler = WorkflowScheduler::new();
420 assert!(!*scheduler.running.read().await);
421 }
422
423 #[tokio::test]
424 async fn test_add_schedule() {
425 let scheduler = WorkflowScheduler::new();
426 let workflow = Workflow::new("test-workflow");
427 let trigger = Trigger::Manual;
428
429 let workflow_id = scheduler
430 .add_schedule(workflow, trigger)
431 .await
432 .expect("should succeed in test");
433 let schedules = scheduler.list_schedules().await;
434
435 assert_eq!(schedules.len(), 1);
436 assert_eq!(schedules[0].0, workflow_id);
437 }
438
439 #[tokio::test]
440 async fn test_remove_schedule() {
441 let scheduler = WorkflowScheduler::new();
442 let workflow = Workflow::new("test-workflow");
443 let trigger = Trigger::Manual;
444
445 let workflow_id = scheduler
446 .add_schedule(workflow, trigger)
447 .await
448 .expect("should succeed in test");
449 scheduler
450 .remove_schedule(workflow_id)
451 .await
452 .expect("should succeed in test");
453
454 let schedules = scheduler.list_schedules().await;
455 assert_eq!(schedules.len(), 0);
456 }
457
458 #[tokio::test]
459 async fn test_enable_disable_schedule() {
460 let scheduler = WorkflowScheduler::new();
461 let workflow = Workflow::new("test-workflow");
462 let trigger = Trigger::Manual;
463
464 let workflow_id = scheduler
465 .add_schedule(workflow, trigger)
466 .await
467 .expect("should succeed in test");
468
469 scheduler
470 .set_schedule_enabled(workflow_id, false)
471 .await
472 .expect("should succeed in test");
473 let schedules = scheduler.list_schedules().await;
474 assert!(!schedules[0].1.enabled);
475
476 scheduler
477 .set_schedule_enabled(workflow_id, true)
478 .await
479 .expect("should succeed in test");
480 let schedules = scheduler.list_schedules().await;
481 assert!(schedules[0].1.enabled);
482 }
483
484 #[tokio::test]
485 async fn test_scheduler_start_stop() {
486 let scheduler = WorkflowScheduler::new();
487
488 scheduler.start().await.expect("should succeed in test");
489 assert!(*scheduler.running.read().await);
490
491 scheduler.stop().await.expect("should succeed in test");
492 assert!(!*scheduler.running.read().await);
493 }
494
495 #[tokio::test]
496 async fn test_file_watcher() {
497 let watcher = FileWatcher::new();
498 let path = std::env::temp_dir().join("oximedia-workflow-scheduler-test");
499 let workflow_id = WorkflowId::new();
500
501 watcher
502 .add_watch(path.clone(), workflow_id)
503 .await
504 .expect("should succeed in test");
505 let workflows = watcher.get_workflows_for_path(&path).await;
506 assert_eq!(workflows.len(), 1);
507 assert_eq!(workflows[0], workflow_id);
508
509 watcher
510 .remove_watch(&path, workflow_id)
511 .await
512 .expect("should succeed in test");
513 let workflows = watcher.get_workflows_for_path(&path).await;
514 assert_eq!(workflows.len(), 0);
515 }
516}