use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use std::sync::{Arc, Mutex};
#[async_trait]
pub trait SchedulerDriver: Send + Sync {
async fn heartbeat(&self, task_id: &str, at: DateTime<Utc>) -> anyhow::Result<()>;
async fn last_run(&self, task_id: &str) -> anyhow::Result<Option<DateTime<Utc>>>;
}
struct MemorySchedulerDriver {
last_runs: Mutex<std::collections::HashMap<String, DateTime<Utc>>>,
}
impl MemorySchedulerDriver {
fn new() -> Self {
Self {
last_runs: Mutex::new(std::collections::HashMap::new()),
}
}
}
#[async_trait]
impl SchedulerDriver for MemorySchedulerDriver {
async fn heartbeat(&self, task_id: &str, at: DateTime<Utc>) -> anyhow::Result<()> {
self.last_runs.lock().unwrap().insert(task_id.into(), at);
Ok(())
}
async fn last_run(&self, task_id: &str) -> anyhow::Result<Option<DateTime<Utc>>> {
Ok(self.last_runs.lock().unwrap().get(task_id).copied())
}
}
pub struct Task {
pub name: String,
callback: Arc<dyn Fn() + Send + Sync + 'static>,
interval: Duration,
next_run: Mutex<DateTime<Utc>>,
}
pub struct Scheduler {
tasks: Vec<Task>,
driver: Arc<dyn SchedulerDriver>,
}
impl Scheduler {
pub fn new() -> Self {
Self {
tasks: Vec::new(),
driver: Arc::new(MemorySchedulerDriver::new()),
}
}
pub fn with_driver(driver: impl SchedulerDriver + 'static) -> Self {
Self {
tasks: Vec::new(),
driver: Arc::new(driver),
}
}
pub fn call<F>(&mut self, name: &str, f: F) -> TaskBuilder<'_>
where
F: Fn() + Send + Sync + 'static,
{
TaskBuilder {
scheduler: self,
name: name.to_string(),
callback: Arc::new(f),
}
}
pub fn add_task(&mut self, task: Task) {
self.tasks.push(task);
}
pub fn tick(&self) {
let now = Utc::now();
for task in &self.tasks {
let mut next_run = task.next_run.lock().unwrap();
if now >= *next_run {
(task.callback)();
*next_run = now + task.interval;
}
}
}
pub fn len(&self) -> usize {
self.tasks.len()
}
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
pub fn driver(&self) -> &Arc<dyn SchedulerDriver> {
&self.driver
}
}
impl Default for Scheduler {
fn default() -> Self {
Self::new()
}
}
pub struct TaskBuilder<'a> {
scheduler: &'a mut Scheduler,
name: String,
callback: Arc<dyn Fn() + Send + Sync + 'static>,
}
impl TaskBuilder<'_> {
pub fn every_minutes(self, minutes: i64) -> String {
let name = self.name.clone();
let task = Task {
name: self.name,
callback: self.callback,
interval: Duration::minutes(minutes),
next_run: Mutex::new(Utc::now() + Duration::minutes(minutes)),
};
self.scheduler.tasks.push(task);
name
}
pub fn every_hours(self, hours: i64) -> String {
let name = self.name.clone();
let task = Task {
name: self.name,
callback: self.callback,
interval: Duration::hours(hours),
next_run: Mutex::new(Utc::now() + Duration::hours(hours)),
};
self.scheduler.tasks.push(task);
name
}
pub fn daily(self) -> String {
self.every_hours(24)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn test_scheduler_call() {
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
let c2 = counter.clone();
let mut sched = Scheduler::new();
sched
.call("task_a", move || {
c.fetch_add(1, Ordering::SeqCst);
})
.every_minutes(5);
sched
.call("task_b", move || {
c2.fetch_add(10, Ordering::SeqCst);
})
.every_minutes(5);
for task in &sched.tasks {
let mut nr = task.next_run.lock().unwrap();
*nr = Utc::now() - Duration::minutes(1);
}
sched.tick();
sched.tick();
assert_eq!(counter.load(Ordering::SeqCst), 11);
}
#[test]
fn test_tick_only_runs_when_due() {
let counter = Arc::new(AtomicUsize::new(0));
let c = counter.clone();
let mut sched = Scheduler::new();
sched
.call("future_task", move || {
c.fetch_add(1, Ordering::SeqCst);
})
.every_minutes(60);
sched.tick();
assert_eq!(counter.load(Ordering::SeqCst), 0);
}
#[test]
fn test_scheduler_len() {
let mut sched = Scheduler::new();
assert!(sched.is_empty());
sched.call("a", || {}).every_minutes(1);
sched.call("b", || {}).every_minutes(2);
assert_eq!(sched.len(), 2);
}
}