use std::sync::Arc;
use std::time::{Duration, SystemTime};
use cronoq_core::{
Dep, FixedStart, MissPolicy, OverlapPolicy, Priority, RecurringSpec, RetryPolicy, SubmitError,
TaskRef, TaskSpec,
};
use crate::clock::Clock;
use crate::error::Error;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum When {
In(Duration),
At(SystemTime),
}
#[derive(Debug)]
pub struct Task<P> {
pub(crate) spec: TaskSpec<P>,
deadline: Option<When>,
start: Option<When>,
tolerance: Option<Duration>,
on_miss: Option<MissPolicy>,
}
impl<P> Clone for Task<P> {
fn clone(&self) -> Self {
Task {
spec: self.spec.clone(),
deadline: self.deadline,
start: self.start,
tolerance: self.tolerance,
on_miss: self.on_miss,
}
}
}
impl<P> From<TaskSpec<P>> for Task<P> {
fn from(spec: TaskSpec<P>) -> Self {
Task {
spec,
deadline: None,
start: None,
tolerance: None,
on_miss: None,
}
}
}
const DEFAULT_TOLERANCE: Duration = Duration::from_secs(1);
impl<P> Task<P> {
pub fn new(payload: P) -> Self {
TaskSpec::new(payload).into()
}
pub fn from_arc(payload: Arc<P>) -> Self {
TaskSpec::from_arc(payload).into()
}
pub fn spec(&self) -> &TaskSpec<P> {
&self.spec
}
pub fn kind(mut self, k: impl Into<Arc<str>>) -> Self {
self.spec.kind = k.into();
self
}
pub fn pool(mut self, p: impl Into<Arc<str>>) -> Self {
self.spec.pool = p.into();
self
}
pub fn priority(mut self, p: Priority) -> Self {
self.spec.priority = p;
self
}
pub fn key(mut self, k: impl Into<Arc<str>>) -> Self {
self.spec.key = Some(k.into());
self
}
pub fn deadline_in(mut self, d: Duration) -> Self {
self.deadline = Some(When::In(d));
self
}
pub fn deadline_at(mut self, t: SystemTime) -> Self {
self.deadline = Some(When::At(t));
self
}
pub fn start_at(mut self, t: SystemTime) -> Self {
self.start = Some(When::At(t));
self
}
pub fn start_in(mut self, d: Duration) -> Self {
self.start = Some(When::In(d));
self
}
pub fn start_tolerance(mut self, tol: Duration) -> Self {
self.tolerance = Some(tol);
self
}
pub fn on_miss(mut self, p: MissPolicy) -> Self {
self.on_miss = Some(p);
self
}
pub fn duration_hint(mut self, d: Duration) -> Self {
self.spec.duration_hint = Some(d);
self
}
pub fn timeout(mut self, d: Duration) -> Self {
self.spec.timeout = Some(d);
self
}
pub fn retry(mut self, r: RetryPolicy) -> Self {
self.spec.retry = Some(r);
self
}
pub fn no_retry(self) -> Self {
self.retry(RetryPolicy::none())
}
pub fn after(mut self, target: impl Into<TaskRef>) -> Self {
self.spec.deps.push(Dep::on_success(target));
self
}
pub fn dep(mut self, d: Dep) -> Self {
self.spec.deps.push(d);
self
}
pub fn resource(mut self, name: impl Into<Arc<str>>, amount: u32) -> Self {
self.spec.resources.push((name.into(), amount));
self
}
pub fn tag(mut self, k: impl Into<Arc<str>>, v: impl Into<Arc<str>>) -> Self {
self.spec.tags.push((k.into(), v.into()));
self
}
pub fn tenant(mut self, tenant: impl Into<Arc<str>>) -> Self {
self.spec.tenant = Some(tenant.into());
self
}
pub fn label(mut self, name: impl Into<Arc<str>>, value: impl Into<Arc<str>>) -> Self {
self.spec.labels.push((name.into(), value.into()));
self
}
fn one_shot_fields(&self) -> Option<&'static str> {
if matches!(self.deadline, Some(When::At(_))) || self.spec.deadline.is_some() {
Some("an absolute deadline (use deadline_in, relative to each occurrence)")
} else if self.start.is_some() || self.spec.fixed_start.is_some() {
Some("a fixed start")
} else if !self.spec.deps.is_empty() {
Some("a dependency (use OverlapPolicy::Queue to chain occurrences)")
} else {
None
}
}
pub(crate) fn resolve(mut self, clock: &Clock) -> Result<TaskSpec<P>, Error> {
validate(&self.spec)?;
let now = clock.now();
let at = |w: When| match w {
When::In(d) => now + d,
When::At(t) => clock.from_system(t),
};
if let Some(d) = self.deadline {
self.spec.deadline = Some(at(d));
}
if let Some(w) = self.start {
self.spec.fixed_start = Some(FixedStart {
at: at(w),
tolerance: self.tolerance.unwrap_or(DEFAULT_TOLERANCE),
on_miss: self.on_miss.unwrap_or_default(),
});
} else if let Some(f) = &mut self.spec.fixed_start {
if let Some(t) = self.tolerance {
f.tolerance = t;
}
if let Some(p) = self.on_miss {
f.on_miss = p;
}
}
if self.spec.tags.iter().all(|(k, _)| &**k != "traceparent")
&& let Some(tp) = crate::telemetry::current_traceparent()
{
self.spec.tags.push(("traceparent".into(), tp.into()));
}
Ok(self.spec)
}
}
fn invalid(what: &'static str) -> Error {
Error::Submit(SubmitError::Invalid(what))
}
pub(crate) fn validate<P>(s: &TaskSpec<P>) -> Result<(), Error> {
let nul = |v: &str| v.contains('\0');
if nul(&s.kind)
|| nul(&s.pool)
|| s.key.as_deref().is_some_and(nul)
|| s.tenant.as_deref().is_some_and(nul)
{
return Err(invalid(
"kind, pool, key and tenant must not contain NUL characters",
));
}
if s.tags.iter().any(|(k, v)| nul(k) || nul(v)) {
return Err(invalid("tags must not contain NUL characters"));
}
if s.labels.iter().any(|(k, v)| nul(k) || nul(v)) {
return Err(invalid("labels must not contain NUL characters"));
}
if s.resources.iter().any(|(n, _)| nul(n)) {
return Err(invalid("resource names must not contain NUL characters"));
}
if s.deps
.iter()
.any(|d| matches!(&d.target, TaskRef::Key(k) if nul(k)))
{
return Err(invalid("dependency keys must not contain NUL characters"));
}
if let Some(r) = &s.retry
&& (!r.multiplier.is_finite() || r.multiplier < 0.0)
{
return Err(invalid("retry multiplier must be finite and non-negative"));
}
Ok(())
}
#[derive(Debug)]
pub struct Recurring<P> {
pub(crate) spec: RecurringSpec<P>,
pub(crate) invalid: Option<String>,
}
impl<P> Recurring<P> {
pub fn new(name: impl Into<Arc<str>>, cron: impl Into<Arc<str>>, template: Task<P>) -> Self {
let invalid = template
.one_shot_fields()
.map(|f| format!("recurring templates cannot set {f}"))
.or_else(|| validate(&template.spec).err().map(|e| e.to_string()));
let name = name.into();
let invalid = invalid.or_else(|| {
name.contains('\0')
.then(|| "recurring names must not contain NUL characters".into())
});
Recurring {
spec: RecurringSpec {
name,
cron: cron.into(),
tz: None,
deadline_after: match template.deadline {
Some(When::In(d)) => Some(d),
_ => None,
},
template: template.spec,
overlap: OverlapPolicy::Skip,
tolerance: template.tolerance.unwrap_or(DEFAULT_TOLERANCE),
on_miss: template.on_miss.unwrap_or_default(),
},
invalid,
}
}
pub fn tz(mut self, tz: impl Into<Arc<str>>) -> Self {
self.spec.tz = Some(tz.into());
self
}
pub fn overlap(mut self, o: OverlapPolicy) -> Self {
self.spec.overlap = o;
self
}
pub fn tolerance(mut self, d: Duration) -> Self {
self.spec.tolerance = d;
self
}
pub fn on_miss(mut self, p: MissPolicy) -> Self {
self.spec.on_miss = p;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_options_are_order_independent() {
let clock = Clock::start();
let a = Task::new(1u64)
.start_tolerance(Duration::from_secs(60))
.on_miss(MissPolicy::Skip)
.start_in(Duration::from_secs(1));
let b = Task::new(1u64)
.start_in(Duration::from_secs(1))
.start_tolerance(Duration::from_secs(60))
.on_miss(MissPolicy::Skip);
let (fa, fb) = (
a.resolve(&clock).unwrap().fixed_start.unwrap(),
b.resolve(&clock).unwrap().fixed_start.unwrap(),
);
assert_eq!((fa.tolerance, fa.on_miss), (fb.tolerance, fb.on_miss));
assert_eq!(fa.tolerance, Duration::from_secs(60));
}
#[test]
fn rejects_unstorable_values() {
let clock = Clock::start();
assert!(Task::new(1).key("a\0b").resolve(&clock).is_err());
assert!(Task::new(1).tag("k", "v\0").resolve(&clock).is_err());
let r = RetryPolicy {
multiplier: f64::INFINITY,
..Default::default()
};
assert!(Task::new(1).retry(r).resolve(&clock).is_err());
assert!(Task::new(1).kind("ok").resolve(&clock).is_ok());
}
#[test]
fn recurring_templates_take_relative_deadlines_and_key_prefixes() {
let r = Recurring::new(
"r",
"* * * * *",
Task::new(1).deadline_in(Duration::from_secs(5)).key("k"),
);
assert!(r.invalid.is_none(), "{:?}", r.invalid);
assert_eq!(r.spec.deadline_after, Some(Duration::from_secs(5)));
assert_eq!(r.spec.template.key.as_deref(), Some("k"));
let at = SystemTime::now() + Duration::from_secs(60);
for (t, what) in [
(Task::new(1).deadline_at(at), "absolute deadline"),
(Task::new(1).start_in(Duration::from_secs(1)), "fixed start"),
(Task::new(1).after("other"), "dependency"),
] {
let r = Recurring::new("r", "* * * * *", t);
assert!(r.invalid.unwrap().contains(what), "{what}");
}
}
}