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 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(crate) 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(crate) fn resolve(
83 self,
84 now_ns: u64,
85 cadence: Option<TimerCadence>,
86 ) -> Result<ResolvedDirective, ScheduleError> {
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(ScheduleError::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 #[error("timer directive requires an after-completion cadence")]
133 MissingCadence,
134}
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub(crate) struct ResolvedSchedule {
138 pub(crate) deadline_ns: u64,
139 pub(crate) requested_delay_ns: Option<u64>,
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub(crate) struct ResolvedDirective {
144 pub(crate) deadline_ns: Option<u64>,
145 pub(crate) requested_delay_ns: Option<u64>,
146}
147
148const fn checked_deadline_after(now_ns: u64, delay_ns: u64) -> Result<u64, ScheduleError> {
149 match now_ns.checked_add(delay_ns) {
150 Some(deadline_ns) => Ok(deadline_ns),
151 None => Err(ScheduleError::DeadlineOverflow),
152 }
153}
154
155pub(crate) fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
156 u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn cadence_is_positive_bounded_and_checked() {
165 assert_eq!(
166 TimerCadence::new(Duration::ZERO),
167 Err(ScheduleError::ZeroCadence)
168 );
169 assert_eq!(
170 TimerCadence::new(Duration::from_secs(u64::MAX)),
171 Err(ScheduleError::DelayOutOfRange)
172 );
173
174 let cadence =
175 TimerCadence::new(Duration::from_nanos(5)).expect("positive cadence should be valid");
176 assert_eq!(cadence.as_nanos(), 5);
177 assert_eq!(cadence.deadline_after(10), Ok(15));
178 assert_eq!(
179 cadence.deadline_after(u64::MAX),
180 Err(ScheduleError::DeadlineOverflow)
181 );
182 }
183
184 #[test]
185 fn schedules_and_directives_resolve_without_truncation() {
186 assert_eq!(
187 TimerSchedule::After(Duration::from_nanos(5)).resolve(10),
188 Ok(ResolvedSchedule {
189 deadline_ns: 15,
190 requested_delay_ns: Some(5),
191 })
192 );
193 assert_eq!(
194 TimerSchedule::At(7).resolve(10),
195 Ok(ResolvedSchedule {
196 deadline_ns: 7,
197 requested_delay_ns: None,
198 })
199 );
200
201 let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
202 assert_eq!(
203 TimerDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
204 Ok(ResolvedDirective {
205 deadline_ns: Some(19),
206 requested_delay_ns: Some(9),
207 })
208 );
209 assert_eq!(
210 TimerDirective::RecurAfterCompletion.resolve(10, None),
211 Err(ScheduleError::MissingCadence)
212 );
213 assert_eq!(
214 TimerDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
215 Err(ScheduleError::DeadlineOverflow)
216 );
217 }
218}