1use std::time::Duration;
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct TimerCadence(u64);
9
10impl TimerCadence {
11 pub fn new(cadence: Duration) -> Result<Self, ScheduleError> {
13 let cadence_ns = duration_ns(cadence)?;
14 Self::from_nanos(cadence_ns)
15 }
16
17 pub const fn from_nanos(cadence_ns: u64) -> Result<Self, ScheduleError> {
19 if cadence_ns == 0 {
20 Err(ScheduleError::ZeroCadence)
21 } else {
22 Ok(Self(cadence_ns))
23 }
24 }
25
26 #[must_use]
28 pub const fn as_nanos(self) -> u64 {
29 self.0
30 }
31
32 pub(super) const fn deadline_after(self, now_ns: u64) -> Result<u64, ScheduleError> {
34 checked_deadline_after(now_ns, self.0)
35 }
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum TimerSchedule {
41 After(Duration),
43 At(u64),
45}
46
47impl TimerSchedule {
48 pub(super) fn resolve(self, now_ns: u64) -> Result<ResolvedSchedule, ScheduleError> {
50 match self {
51 Self::After(delay) => {
52 let delay_ns = duration_ns(delay)?;
53 Ok(ResolvedSchedule {
54 deadline_ns: checked_deadline_after(now_ns, delay_ns)?,
55 requested_delay_ns: Some(delay_ns),
56 })
57 }
58 Self::At(deadline_ns) => Ok(ResolvedSchedule {
59 deadline_ns,
60 requested_delay_ns: None,
61 }),
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum TimerDirective {
69 Stop,
71 ContinueImmediately,
73 RetryAfter(Duration),
75 ScheduleAt(u64),
77 RecurAfterCompletion,
79}
80
81impl TimerDirective {
82 pub(super) fn resolve(
83 self,
84 now_ns: u64,
85 cadence: Option<TimerCadence>,
86 ) -> Result<ResolvedDirective, DirectiveError> {
87 match self {
88 Self::Stop => Ok(ResolvedDirective {
89 deadline_ns: None,
90 requested_delay_ns: None,
91 }),
92 Self::ContinueImmediately => Ok(ResolvedDirective {
93 deadline_ns: Some(now_ns),
94 requested_delay_ns: Some(0),
95 }),
96 Self::RetryAfter(delay) => {
97 let delay_ns = duration_ns(delay)?;
98 Ok(ResolvedDirective {
99 deadline_ns: Some(checked_deadline_after(now_ns, delay_ns)?),
100 requested_delay_ns: Some(delay_ns),
101 })
102 }
103 Self::ScheduleAt(deadline_ns) => Ok(ResolvedDirective {
104 deadline_ns: Some(deadline_ns),
105 requested_delay_ns: None,
106 }),
107 Self::RecurAfterCompletion => {
108 let cadence = cadence.ok_or(DirectiveError::MissingCadence)?;
109 Ok(ResolvedDirective {
110 deadline_ns: Some(cadence.deadline_after(now_ns)?),
111 requested_delay_ns: Some(cadence.as_nanos()),
112 })
113 }
114 }
115 }
116}
117
118#[non_exhaustive]
120#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
121pub enum ScheduleError {
122 #[error("timer cadence must be greater than zero")]
124 ZeroCadence,
125 #[error("timer delay exceeds the supported nanosecond range")]
127 DelayOutOfRange,
128 #[error("timer deadline exceeds the supported timestamp range")]
130 DeadlineOverflow,
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum DirectiveError {
135 Schedule(ScheduleError),
136 MissingCadence,
137}
138
139impl From<ScheduleError> for DirectiveError {
140 fn from(error: ScheduleError) -> Self {
141 Self::Schedule(error)
142 }
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
146pub struct ResolvedSchedule {
147 pub(crate) deadline_ns: u64,
148 pub(crate) requested_delay_ns: Option<u64>,
149}
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub struct ResolvedDirective {
153 pub(crate) deadline_ns: Option<u64>,
154 pub(crate) requested_delay_ns: Option<u64>,
155}
156
157const fn checked_deadline_after(now_ns: u64, delay_ns: u64) -> Result<u64, ScheduleError> {
158 match now_ns.checked_add(delay_ns) {
159 Some(deadline_ns) => Ok(deadline_ns),
160 None => Err(ScheduleError::DeadlineOverflow),
161 }
162}
163
164pub fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
165 u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn cadence_is_positive_bounded_and_checked() {
174 assert_eq!(
175 TimerCadence::new(Duration::ZERO),
176 Err(ScheduleError::ZeroCadence)
177 );
178 assert_eq!(
179 TimerCadence::new(Duration::from_secs(u64::MAX)),
180 Err(ScheduleError::DelayOutOfRange)
181 );
182
183 let cadence =
184 TimerCadence::new(Duration::from_nanos(5)).expect("positive cadence should be valid");
185 assert_eq!(cadence.as_nanos(), 5);
186 assert_eq!(cadence.deadline_after(10), Ok(15));
187 assert_eq!(
188 cadence.deadline_after(u64::MAX),
189 Err(ScheduleError::DeadlineOverflow)
190 );
191 }
192
193 #[test]
194 fn schedules_and_directives_resolve_without_truncation() {
195 assert_eq!(
196 TimerSchedule::After(Duration::from_nanos(5)).resolve(10),
197 Ok(ResolvedSchedule {
198 deadline_ns: 15,
199 requested_delay_ns: Some(5),
200 })
201 );
202 assert_eq!(
203 TimerSchedule::At(7).resolve(10),
204 Ok(ResolvedSchedule {
205 deadline_ns: 7,
206 requested_delay_ns: None,
207 })
208 );
209
210 let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
211 assert_eq!(
212 TimerDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
213 Ok(ResolvedDirective {
214 deadline_ns: Some(19),
215 requested_delay_ns: Some(9),
216 })
217 );
218 assert_eq!(
219 TimerDirective::RecurAfterCompletion.resolve(10, None),
220 Err(DirectiveError::MissingCadence)
221 );
222 assert_eq!(
223 TimerDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
224 Err(DirectiveError::Schedule(ScheduleError::DeadlineOverflow))
225 );
226 }
227}