use std::num::NonZeroU32;
use std::time::Duration;
use crate::{policies::BackoffPolicy, policies::RestartPolicy, tasks::task::TaskRef};
#[inline]
fn normalize_timeout(timeout: Option<Duration>) -> Option<Duration> {
timeout.filter(|d| !d.is_zero())
}
#[derive(Clone)]
#[must_use]
pub struct TaskSpec {
timeout: Option<Duration>,
restart: RestartPolicy,
backoff: BackoffPolicy,
task: TaskRef,
max_retries: Option<NonZeroU32>,
}
impl std::fmt::Debug for TaskSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TaskSpec")
.field("restart", &self.restart)
.field("backoff", &self.backoff)
.field("timeout", &self.timeout)
.field("task", &self.task.name())
.field("max_retries", &self.max_retries)
.finish()
}
}
impl TaskSpec {
pub fn new(
task: TaskRef,
restart: RestartPolicy,
backoff: BackoffPolicy,
timeout: Option<Duration>,
) -> Self {
Self {
restart,
backoff,
task,
max_retries: None,
timeout: normalize_timeout(timeout),
}
}
pub fn once(task: TaskRef) -> Self {
Self {
backoff: BackoffPolicy::default(),
restart: RestartPolicy::Never,
max_retries: None,
timeout: None,
task,
}
}
pub fn restartable(task: TaskRef) -> Self {
Self {
restart: RestartPolicy::OnFailure,
backoff: BackoffPolicy::default(),
timeout: None,
task,
max_retries: None,
}
}
pub fn periodic(task: TaskRef, every: Duration) -> Self {
Self {
restart: RestartPolicy::Always {
interval: Some(every).filter(|d| !d.is_zero()),
},
backoff: BackoffPolicy::default(),
timeout: None,
task,
max_retries: None,
}
}
pub fn task(&self) -> &TaskRef {
&self.task
}
pub fn name(&self) -> &str {
self.task.name()
}
pub fn restart(&self) -> RestartPolicy {
self.restart
}
pub fn backoff(&self) -> BackoffPolicy {
self.backoff
}
pub fn timeout(&self) -> Option<Duration> {
self.timeout
}
pub fn max_retries(&self) -> Option<NonZeroU32> {
self.max_retries
}
pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = normalize_timeout(timeout);
self
}
pub fn with_backoff(mut self, backoff: BackoffPolicy) -> Self {
self.backoff = backoff;
self
}
pub fn with_restart(mut self, restart: RestartPolicy) -> Self {
self.restart = restart;
self
}
pub fn with_max_retries(mut self, max_retries: impl Into<Option<NonZeroU32>>) -> Self {
self.max_retries = max_retries.into();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TaskContext, TaskFn};
fn task(name: &str) -> TaskRef {
TaskFn::arc(name, |_ctx: TaskContext| async { Ok(()) })
}
#[test]
fn periodic_sets_always_restart_with_interval() {
let every = Duration::from_secs(30);
let spec = TaskSpec::periodic(task("tick"), every);
assert!(
matches!(spec.restart(), RestartPolicy::Always { interval: Some(d) } if d == every),
"periodic must set RestartPolicy::Always with the given interval, got {:?}",
spec.restart()
);
assert_eq!(spec.timeout(), None, "periodic must not set a timeout");
assert_eq!(
spec.max_retries(),
None,
"periodic must not limit retries by default"
);
}
#[test]
fn periodic_zero_interval_normalizes_to_immediate_restart() {
let spec = TaskSpec::periodic(task("tick"), Duration::ZERO);
assert!(
matches!(spec.restart(), RestartPolicy::Always { interval: None }),
"a zero interval must normalize to None (immediate restart), got {:?}",
spec.restart()
);
}
#[test]
fn zero_timeout_normalizes_to_none() {
let via_builder = TaskSpec::once(task("z")).with_timeout(Some(Duration::ZERO));
assert_eq!(
via_builder.timeout(),
None,
"with_timeout(Some(ZERO)) must normalize to None"
);
let via_new = TaskSpec::new(
task("z"),
RestartPolicy::Never,
BackoffPolicy::default(),
Some(Duration::ZERO),
);
assert_eq!(
via_new.timeout(),
None,
"new(.., Some(ZERO)) must normalize to None"
);
let positive = TaskSpec::once(task("p")).with_timeout(Some(Duration::from_secs(1)));
assert_eq!(
positive.timeout(),
Some(Duration::from_secs(1)),
"a positive timeout must be preserved"
);
}
}