use std::time::Duration;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TimerCadence(u64);
impl TimerCadence {
pub fn new(cadence: Duration) -> Result<Self, ScheduleError> {
let cadence_ns = duration_ns(cadence)?;
Self::from_nanos(cadence_ns)
}
pub const fn from_nanos(cadence_ns: u64) -> Result<Self, ScheduleError> {
if cadence_ns == 0 {
Err(ScheduleError::ZeroCadence)
} else {
Ok(Self(cadence_ns))
}
}
#[must_use]
pub const fn as_nanos(self) -> u64 {
self.0
}
pub const fn deadline_after(self, now_ns: u64) -> Result<u64, ScheduleError> {
checked_deadline_after(now_ns, self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimerSchedule {
After(Duration),
At(u64),
}
impl TimerSchedule {
pub(crate) fn resolve(self, now_ns: u64) -> Result<ResolvedSchedule, ScheduleError> {
match self {
Self::After(delay) => {
let delay_ns = duration_ns(delay)?;
Ok(ResolvedSchedule {
deadline_ns: checked_deadline_after(now_ns, delay_ns)?,
requested_delay_ns: Some(delay_ns),
})
}
Self::At(deadline_ns) => Ok(ResolvedSchedule {
deadline_ns,
requested_delay_ns: None,
}),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimerDirective {
Stop,
ContinueImmediately,
RetryAfter(Duration),
ScheduleAt(u64),
RecurAfterCompletion,
}
impl TimerDirective {
pub(crate) fn resolve(
self,
now_ns: u64,
cadence: Option<TimerCadence>,
) -> Result<ResolvedDirective, ScheduleError> {
match self {
Self::Stop => Ok(ResolvedDirective {
deadline_ns: None,
requested_delay_ns: None,
}),
Self::ContinueImmediately => Ok(ResolvedDirective {
deadline_ns: Some(now_ns),
requested_delay_ns: Some(0),
}),
Self::RetryAfter(delay) => {
let delay_ns = duration_ns(delay)?;
Ok(ResolvedDirective {
deadline_ns: Some(checked_deadline_after(now_ns, delay_ns)?),
requested_delay_ns: Some(delay_ns),
})
}
Self::ScheduleAt(deadline_ns) => Ok(ResolvedDirective {
deadline_ns: Some(deadline_ns),
requested_delay_ns: None,
}),
Self::RecurAfterCompletion => {
let cadence = cadence.ok_or(ScheduleError::MissingCadence)?;
Ok(ResolvedDirective {
deadline_ns: Some(cadence.deadline_after(now_ns)?),
requested_delay_ns: Some(cadence.as_nanos()),
})
}
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum ScheduleError {
#[error("timer cadence must be greater than zero")]
ZeroCadence,
#[error("timer delay exceeds the supported nanosecond range")]
DelayOutOfRange,
#[error("timer deadline exceeds the supported timestamp range")]
DeadlineOverflow,
#[error("timer directive requires an after-completion cadence")]
MissingCadence,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedSchedule {
pub(crate) deadline_ns: u64,
pub(crate) requested_delay_ns: Option<u64>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedDirective {
pub(crate) deadline_ns: Option<u64>,
pub(crate) requested_delay_ns: Option<u64>,
}
const fn checked_deadline_after(now_ns: u64, delay_ns: u64) -> Result<u64, ScheduleError> {
match now_ns.checked_add(delay_ns) {
Some(deadline_ns) => Ok(deadline_ns),
None => Err(ScheduleError::DeadlineOverflow),
}
}
pub(crate) fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cadence_is_positive_bounded_and_checked() {
assert_eq!(
TimerCadence::new(Duration::ZERO),
Err(ScheduleError::ZeroCadence)
);
assert_eq!(
TimerCadence::new(Duration::from_secs(u64::MAX)),
Err(ScheduleError::DelayOutOfRange)
);
let cadence =
TimerCadence::new(Duration::from_nanos(5)).expect("positive cadence should be valid");
assert_eq!(cadence.as_nanos(), 5);
assert_eq!(cadence.deadline_after(10), Ok(15));
assert_eq!(
cadence.deadline_after(u64::MAX),
Err(ScheduleError::DeadlineOverflow)
);
}
#[test]
fn schedules_and_directives_resolve_without_truncation() {
assert_eq!(
TimerSchedule::After(Duration::from_nanos(5)).resolve(10),
Ok(ResolvedSchedule {
deadline_ns: 15,
requested_delay_ns: Some(5),
})
);
assert_eq!(
TimerSchedule::At(7).resolve(10),
Ok(ResolvedSchedule {
deadline_ns: 7,
requested_delay_ns: None,
})
);
let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
assert_eq!(
TimerDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
Ok(ResolvedDirective {
deadline_ns: Some(19),
requested_delay_ns: Some(9),
})
);
assert_eq!(
TimerDirective::RecurAfterCompletion.resolve(10, None),
Err(ScheduleError::MissingCadence)
);
assert_eq!(
TimerDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
Err(ScheduleError::DeadlineOverflow)
);
}
}