use crate::snapshot::{TimerControlFailure, TimerSchedulingMode};
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(super) 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(super) 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),
mode: TimerSchedulingMode::Once,
})
}
Self::At(deadline_ns) => Ok(ResolvedSchedule {
deadline_ns,
requested_delay_ns: None,
mode: TimerSchedulingMode::Deadline,
}),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimerDirective {
Stop,
ContinueImmediately,
RetryAfter(Duration),
ScheduleAt(u64),
RecurAfterCompletion,
}
impl TimerDirective {
pub(super) fn resolve(
self,
now_ns: u64,
cadence: Option<TimerCadence>,
) -> Result<Option<ResolvedSchedule>, TimerControlFailure> {
match self {
Self::Stop => Ok(None),
Self::ContinueImmediately => Ok(Some(ResolvedSchedule {
deadline_ns: now_ns,
requested_delay_ns: Some(0),
mode: TimerSchedulingMode::Continuation,
})),
Self::RetryAfter(delay) => {
let delay_ns = duration_ns(delay).map_err(ScheduleError::control_failure)?;
Ok(Some(ResolvedSchedule {
deadline_ns: checked_deadline_after(now_ns, delay_ns)
.map_err(ScheduleError::control_failure)?,
requested_delay_ns: Some(delay_ns),
mode: TimerSchedulingMode::Retry,
}))
}
Self::ScheduleAt(deadline_ns) => Ok(Some(ResolvedSchedule {
deadline_ns,
requested_delay_ns: None,
mode: TimerSchedulingMode::Deadline,
})),
Self::RecurAfterCompletion => {
let cadence = cadence.ok_or(TimerControlFailure::DirectiveNotAllowed)?;
Ok(Some(ResolvedSchedule {
deadline_ns: cadence
.deadline_after(now_ns)
.map_err(ScheduleError::control_failure)?,
requested_delay_ns: Some(cadence.as_nanos()),
mode: TimerSchedulingMode::AfterCompletion,
}))
}
}
}
}
#[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,
}
impl ScheduleError {
const fn control_failure(self) -> TimerControlFailure {
match self {
Self::ZeroCadence => TimerControlFailure::DirectiveNotAllowed,
Self::DelayOutOfRange => TimerControlFailure::DelayOutOfRange,
Self::DeadlineOverflow => TimerControlFailure::DeadlineOverflow,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResolvedSchedule {
pub(crate) deadline_ns: u64,
pub(crate) requested_delay_ns: Option<u64>,
pub(crate) mode: TimerSchedulingMode,
}
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 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),
mode: TimerSchedulingMode::Once,
})
);
assert_eq!(
TimerSchedule::At(7).resolve(10),
Ok(ResolvedSchedule {
deadline_ns: 7,
requested_delay_ns: None,
mode: TimerSchedulingMode::Deadline,
})
);
assert_eq!(TimerDirective::Stop.resolve(10, None), Ok(None));
assert_eq!(
TimerDirective::ContinueImmediately.resolve(10, None),
Ok(Some(ResolvedSchedule {
deadline_ns: 10,
requested_delay_ns: Some(0),
mode: TimerSchedulingMode::Continuation,
}))
);
assert_eq!(
TimerDirective::RetryAfter(Duration::from_nanos(5)).resolve(10, None),
Ok(Some(ResolvedSchedule {
deadline_ns: 15,
requested_delay_ns: Some(5),
mode: TimerSchedulingMode::Retry,
}))
);
assert_eq!(
TimerDirective::ScheduleAt(7).resolve(10, None),
Ok(Some(ResolvedSchedule {
deadline_ns: 7,
requested_delay_ns: None,
mode: TimerSchedulingMode::Deadline,
}))
);
let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
assert_eq!(
TimerDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
Ok(Some(ResolvedSchedule {
deadline_ns: 19,
requested_delay_ns: Some(9),
mode: TimerSchedulingMode::AfterCompletion,
}))
);
assert_eq!(
TimerDirective::RecurAfterCompletion.resolve(10, None),
Err(TimerControlFailure::DirectiveNotAllowed)
);
assert_eq!(
TimerDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
Err(TimerControlFailure::DeadlineOverflow)
);
assert_eq!(
TimerDirective::RetryAfter(Duration::MAX).resolve(0, None),
Err(TimerControlFailure::DelayOutOfRange)
);
assert_eq!(
TimerDirective::RecurAfterCompletion.resolve(u64::MAX, Some(cadence)),
Err(TimerControlFailure::DeadlineOverflow)
);
}
}