use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use chrono::{DateTime, Datelike, TimeZone, Utc};
use tokio_util::sync::CancellationToken;
use super::error::SchedulerError;
type BoxFuture = Pin<Box<dyn Future<Output = Result<(), SchedulerError>> + Send>>;
type EnqueueFn = Box<dyn Fn() -> BoxFuture + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ScheduleCadence {
Every { seconds: u64 },
Daily { hour: u8, minute: u8 },
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ScheduleBinding {
pub job: &'static str,
pub version: i16,
pub cadence: ScheduleCadence,
}
struct ScheduleEntry {
kind: &'static str,
version: i16,
cadence: ScheduleCadence,
next_fire: DateTime<Utc>,
fire: EnqueueFn,
}
pub struct Scheduler {
entries: Vec<ScheduleEntry>,
}
impl Scheduler {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
#[must_use]
pub fn schedule<F>(mut self, binding: &ScheduleBinding, fire: F) -> Self
where
F: Fn() -> BoxFuture + Send + Sync + 'static,
{
let next_fire = compute_next_fire(&binding.cadence, Utc::now());
self.entries.push(ScheduleEntry {
kind: binding.job,
version: binding.version,
cadence: binding.cadence.clone(),
next_fire,
fire: Box::new(fire),
});
self
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub async fn run(mut self, shutdown: CancellationToken) -> Result<(), SchedulerError> {
if self.entries.is_empty() {
shutdown.cancelled().await;
return Ok(());
}
loop {
let earliest = self
.entries
.iter()
.map(|e| e.next_fire)
.min()
.unwrap_or_else(Utc::now);
let now = Utc::now();
let sleep_duration = if earliest > now {
(earliest - now).to_std().unwrap_or(Duration::from_secs(0))
} else {
Duration::from_secs(0)
};
tokio::select! {
_ = shutdown.cancelled() => return Ok(()),
_ = tokio::time::sleep(sleep_duration) => {}
}
let now = Utc::now();
for entry in &mut self.entries {
if entry.next_fire <= now {
if let Err(e) = (entry.fire)().await {
eprintln!(
"scheduler enqueue error for {} v{}: {e}",
entry.kind, entry.version
);
}
entry.next_fire = compute_next_fire(&entry.cadence, now);
}
}
}
}
}
impl Default for Scheduler {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for Scheduler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Scheduler")
.field("entries", &self.entries.len())
.finish_non_exhaustive()
}
}
fn compute_next_fire(cadence: &ScheduleCadence, now: DateTime<Utc>) -> DateTime<Utc> {
match cadence {
ScheduleCadence::Every { seconds } => {
let dur = chrono::Duration::seconds(i64::try_from(*seconds).unwrap_or(i64::MAX));
now + dur
}
ScheduleCadence::Daily { hour, minute } => {
let h = u32::from(*hour);
let m = u32::from(*minute);
let today = Utc
.with_ymd_and_hms(now.year(), now.month(), now.day(), h, m, 0)
.single();
match today {
Some(t) if t > now => t,
_ => {
let tomorrow = now + chrono::Duration::days(1);
Utc.with_ymd_and_hms(tomorrow.year(), tomorrow.month(), tomorrow.day(), h, m, 0)
.single()
.unwrap_or(now)
}
}
}
}
}