use std::future::Future;
use std::str::FromStr;
use chrono::Utc;
use cron::Schedule;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::GotchaContext;
pub struct TaskScheduler<T1: Clone + Send + Sync + 'static, T2: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default> {
context: GotchaContext<T1, T2>,
}
impl<T1, T2> TaskScheduler<T1, T2>
where
T1: Clone + Send + Sync + 'static,
T2: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
{
pub fn new(context: GotchaContext<T1, T2>) -> Self {
Self { context }
}
pub fn cron<F, FF>(&self, name: impl AsRef<str>, expression: String, task: F)
where
F: Fn(GotchaContext<T1, T2>) -> FF + Send + 'static,
FF: Future<Output = ()> + Send + 'static,
{
let name = name.as_ref().to_string();
let schedule = match Schedule::from_str(&expression) {
Ok(schedule) => schedule,
Err(e) => {
tracing::error!("cron task {name:?} has an invalid schedule {expression:?}: {e}; task not started");
return;
}
};
info!("starting cron task: {name}");
tokio::spawn(cron_proc_macro_wrapper(self.context.clone(), schedule, name, task));
}
pub fn interval<F, FF>(&self, name: impl AsRef<str>, interval: std::time::Duration, task: F)
where
F: Fn(GotchaContext<T1, T2>) -> FF + Send + 'static,
FF: Future<Output = ()> + Send + 'static,
{
let name = name.as_ref().to_string();
info!("starting interval task: {name}");
tokio::spawn(interval_proc_macro_wrapper(self.context.clone(), interval, name, task));
}
}
async fn run_supervised<FF>(name: &str, fut: FF)
where
FF: Future<Output = ()> + Send + 'static,
{
if let Err(join_error) = tokio::spawn(fut).await {
tracing::error!("scheduled task {name:?} panicked: {join_error}");
}
}
pub async fn cron_proc_macro_wrapper<T1, T2, F, FF>(context: GotchaContext<T1, T2>, schedule: Schedule, name: String, task: F)
where
T1: Clone + Send + Sync + 'static,
T2: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
F: Fn(GotchaContext<T1, T2>) -> FF + Send + 'static,
FF: Future<Output = ()> + Send + 'static,
{
for next_trigger_time in schedule.upcoming(Utc) {
let now = Utc::now();
let wait = (next_trigger_time - now).to_std().unwrap_or(std::time::Duration::ZERO);
tokio::time::sleep(wait).await;
run_supervised(&name, task(context.clone())).await;
}
}
pub async fn interval_proc_macro_wrapper<T1, T2, F, FF>(context: GotchaContext<T1, T2>, interval: std::time::Duration, name: String, task: F)
where
T1: Clone + Send + Sync + 'static,
T2: Clone + Send + Sync + 'static + Serialize + for<'de> Deserialize<'de> + Default,
F: Fn(GotchaContext<T1, T2>) -> FF + Send + 'static,
FF: Future<Output = ()> + Send + 'static,
{
let mut interval = tokio::time::interval(interval);
loop {
interval.tick().await;
run_supervised(&name, task(context.clone())).await;
}
}