use crate::RuntimeError;
use crate::runtime_state::LifecycleSignals;
use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ScheduleHandle {
cancelled: Arc<AtomicBool>,
trigger: Arc<tokio::sync::Notify>,
}
impl ScheduleHandle {
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
self.trigger.notify_one();
}
pub fn trigger(&self) {
self.trigger.notify_one();
}
fn paired(trigger: &Arc<tokio::sync::Notify>) -> (Self, Arc<AtomicBool>) {
let cancelled = Arc::new(AtomicBool::new(false));
let handle = Self {
cancelled: Arc::clone(&cancelled),
trigger: Arc::clone(trigger),
};
(handle, cancelled)
}
}
pub fn every<F>(interval: Duration, f: F) -> Result<ScheduleHandle, RuntimeError>
where
F: Fn() + Send + 'static,
{
every_async(interval, move || {
f();
std::future::ready(())
})
}
pub fn every_async<F, Fut>(interval: Duration, f: F) -> Result<ScheduleHandle, RuntimeError>
where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
every_async_notified(interval, Arc::new(tokio::sync::Notify::new()), f)
}
pub fn every_async_notified<F, Fut>(
interval: Duration,
trigger: Arc<tokio::sync::Notify>,
f: F,
) -> Result<ScheduleHandle, RuntimeError>
where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
validate_interval(interval)?;
let (handle, cancelled) = ScheduleHandle::paired(&trigger);
crate::task::admit_signalled_loop(move |signals| {
run_interval_async(cancelled, signals, trigger, interval, f)
})?;
Ok(handle)
}
pub fn cron<F>(expr: &str, f: F) -> Result<ScheduleHandle, RuntimeError>
where
F: Fn() + Send + 'static,
{
let normalized = normalize_cron_expr(expr);
let schedule: cron::Schedule = normalized
.parse()
.map_err(|e: cron::error::Error| RuntimeError::Schedule(e.to_string().into()))?;
reject_exhausted(&schedule, expr)?;
let trigger = Arc::new(tokio::sync::Notify::new());
let (handle, cancelled) = ScheduleHandle::paired(&trigger);
crate::task::admit_signalled_loop(move |signals| {
run_cron(cancelled, signals, trigger, schedule, f)
})?;
Ok(handle)
}
fn should_stop(cancel: &AtomicBool, signals: &LifecycleSignals) -> bool {
cancel.load(Ordering::Acquire) || signals.is_fired()
}
enum Wake {
Due,
Triggered,
}
async fn next_wake(
due: impl Future<Output = ()>,
trigger: &tokio::sync::Notify,
cancel: &AtomicBool,
signals: &LifecycleSignals,
) -> Option<Wake> {
let wake = tokio::select! {
() = due => Wake::Due,
() = trigger.notified() => Wake::Triggered,
() = signals.wait() => return None,
};
match should_stop(cancel, signals) {
true => None,
false => Some(wake),
}
}
async fn tick_due(tick: &mut tokio::time::Interval) {
tick.tick().await;
}
async fn run_interval_async<F, Fut>(
cancel: Arc<AtomicBool>,
signals: LifecycleSignals,
trigger: Arc<tokio::sync::Notify>,
interval: Duration,
f: F,
) where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
let mut tick = tokio::time::interval(interval);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
tick.tick().await;
loop {
let wake = match next_wake(tick_due(&mut tick), &trigger, &cancel, &signals).await {
None => break,
Some(wake) => wake,
};
f().await;
match wake {
Wake::Triggered => tick.reset(),
Wake::Due => {}
}
}
}
async fn run_cron<F>(
cancel: Arc<AtomicBool>,
signals: LifecycleSignals,
trigger: Arc<tokio::sync::Notify>,
schedule: cron::Schedule,
f: F,
) where
F: Fn() + Send + 'static,
{
loop {
let now = chrono::Utc::now();
let next = match schedule.after(&now).next() {
Some(next) => next,
None => {
tracing::warn!(
expr = schedule.source(),
"schedule: cron expression has no further occurrences; \
this schedule will never fire again"
);
break;
}
};
let until = (next - now).to_std().unwrap_or(Duration::ZERO);
let due = tokio::time::sleep(until);
match next_wake(due, &trigger, &cancel, &signals).await {
None => break,
Some(Wake::Due) => f(),
Some(Wake::Triggered) => {}
}
}
}
fn reject_exhausted(schedule: &cron::Schedule, expr: &str) -> Result<(), RuntimeError> {
match schedule.after(&chrono::Utc::now()).next() {
Some(_) => Ok(()),
None => Err(RuntimeError::Schedule(
format!("cron expression `{expr}` has no future occurrences").into(),
)),
}
}
fn validate_interval(interval: Duration) -> Result<(), RuntimeError> {
match interval.is_zero() {
true => Err(RuntimeError::InvalidArgument(
"schedule interval must be non-zero".into(),
)),
false => Ok(()),
}
}
fn normalize_cron_expr(expr: &str) -> std::borrow::Cow<'_, str> {
let fields = expr.split_whitespace().count();
match fields {
5 => std::borrow::Cow::Owned(format!("0 {expr}")),
_ => std::borrow::Cow::Borrowed(expr),
}
}