use std::{
fmt::Debug,
future::Future,
pin::Pin,
task::{Context, Poll},
time::SystemTime,
};
use apalis_core::{
backend::{Backend, BackendConfig, TryNewBackend, finalize::Ephemeral},
features_table,
layers::Identity,
task::{Task, builder::TaskBuilder, task_id::TaskId},
timer::Delay,
worker::context::WorkerContext,
};
use futures_util::Stream;
use ulid::Ulid;
use crate::{config::Config, error::Error, schedule::Schedule, tick::Tick, timezone::Utc};
#[doc = features_table! {
setup = "unreachable!();",
TaskSink => not_supported("You cannot push tasks to a cron scheduler"),
}]
#[derive(Debug)]
pub struct CronScheduler<S, Tz = Utc> {
config: Config<S, Tz>,
next_tick: Option<Tick<Tz>>,
delay: Option<Delay>,
}
impl<S> CronScheduler<S, Utc> {
pub const fn new(schedule: S) -> CronScheduler<S> {
CronScheduler {
config: Config::new(schedule, Utc),
next_tick: None,
delay: None,
}
}
}
impl<Schedule: Clone, Timezone: Clone> Clone for CronScheduler<Schedule, Timezone> {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
next_tick: None,
delay: None,
}
}
}
impl<S: Schedule<Tz>, Tz> CronScheduler<S, Tz> {
pub fn into_stream(self) -> impl Stream<Item = Result<Tick<Tz>, Error>>
where
Self: Backend<Task = Task<Tick<Tz>>, Error = Error> + BackendConfig<Args = Tick<Tz>>,
{
let mut cron = self;
futures_util::stream::poll_fn(move |cx| {
match cron.poll_next(cx, &WorkerContext::new("cron-scheduler")) {
Poll::Ready(Some(Ok(task))) => Poll::Ready(Some(Ok(task.args))),
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
})
}
}
impl<S, T> CronScheduler<S, T> {
pub fn with_timezone<Tz>(self, timezone: Tz) -> CronScheduler<S, Tz> {
CronScheduler {
config: Config::new(self.config.schedule, timezone),
next_tick: None,
delay: None,
}
}
}
impl<S, Tz> Backend for CronScheduler<S, Tz>
where
S: Schedule<Tz>,
Tz: Debug + Clone,
{
type Task = Task<Tick<Tz>>;
type Error = Error;
fn poll_ready(
&mut self,
_: &mut Context<'_>,
_: &WorkerContext,
) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn poll_next(
&mut self,
cx: &mut Context<'_>,
_: &WorkerContext,
) -> Poll<Option<Result<Self::Task, Self::Error>>> {
tracing::trace!(
has_next_tick = self.next_tick.is_some(),
has_delay = self.delay.is_some(),
"polling cron scheduler"
);
let tz = self.config.timezone().clone();
let next_tick = {
if self.next_tick.is_none() {
tracing::trace!("calculating next cron tick");
self.next_tick = self.config.schedule.next_tick(&tz);
if self.next_tick.is_none() {
tracing::debug!("cron schedule exhausted");
return Poll::Ready(None);
}
}
if self.delay.is_none() {
let next = self.next_tick.as_ref().unwrap();
let now = SystemTime::now();
tracing::trace!(
tick = ?next,
now = ?now,
"creating delay until next cron tick"
);
let duration = match next.signed_duration_since(now) {
Ok(d) => d,
Err(e) => {
tracing::warn!(
tick = ?next,
error = ?e,
"cron tick is out of range"
);
return Poll::Ready(Some(Err(Error::OutOfRange {
duration: e,
tick: next.get_timestamp(),
})));
}
};
tracing::trace!(?duration, "cron delay created");
self.delay = Some(Delay::new(duration));
}
match Pin::new(self.delay.as_mut().unwrap()).poll(cx) {
Poll::Pending => {
tracing::trace!("cron delay pending");
Poll::Pending
}
Poll::Ready(()) => {
let fired = self.next_tick.take().unwrap();
tracing::debug!(
tick = ?fired,
"cron tick fired"
);
self.delay = None;
self.next_tick = self.config.schedule.next_tick(&tz);
tracing::trace!(
next_tick = ?self.next_tick,
"scheduled next cron tick"
);
Poll::Ready(Some(Ok(fired)))
}
}
};
next_tick.map(|opt| {
opt.map(|res| {
res.map(|tick| {
let timestamp: SystemTime = tick.system_time();
let task_id = Ulid::from_datetime(timestamp);
tracing::trace!(
timestamp = ?timestamp,
%task_id,
"building cron task"
);
TaskBuilder::new(tick)
.task_id(TaskId::Ulid(task_id))
.build()
})
})
})
}
fn poll_close(
&mut self,
_: &mut Context<'_>,
_: &WorkerContext,
) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}
impl<S, Tz> BackendConfig for CronScheduler<S, Tz> {
type Args = Tick<Tz>;
type Config = Config<S, Tz>;
type Kind = Ephemeral;
type Layer = Identity;
type Id = Ulid;
fn config(&self) -> &Self::Config {
&self.config
}
fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
Identity::new()
}
}
impl<S: Schedule<Tz>, Tz> TryNewBackend for CronScheduler<S, Tz>
where
Self: Backend + BackendConfig<Args = Tick<Tz>, Config = Config<S, Tz>>,
{
type Backend = Self;
fn try_new(config: Self::Config) -> Result<Self, Self::Error> {
Ok(CronScheduler {
config,
delay: None,
next_tick: None,
})
}
}