Skip to main content

ic_timers/
schedule.rs

1//! Checked cadence and scheduling decisions.
2
3use crate::snapshot::TimerSchedulingMode;
4use std::time::Duration;
5use thiserror::Error;
6
7/// Validated positive recurrence cadence.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct TimerCadence(u64);
10
11impl TimerCadence {
12    /// Validate a recurrence cadence.
13    pub fn new(cadence: Duration) -> Result<Self, ScheduleError> {
14        let cadence_ns = duration_ns(cadence)?;
15        Self::from_nanos(cadence_ns)
16    }
17
18    /// Validate an already encoded nanosecond cadence.
19    pub const fn from_nanos(cadence_ns: u64) -> Result<Self, ScheduleError> {
20        if cadence_ns == 0 {
21            Err(ScheduleError::ZeroCadence)
22        } else {
23            Ok(Self(cadence_ns))
24        }
25    }
26
27    /// Return the encoded cadence in nanoseconds.
28    #[must_use]
29    pub const fn as_nanos(self) -> u64 {
30        self.0
31    }
32
33    /// Resolve one successor relative to the supplied dispatch time.
34    pub(super) const fn deadline_after(self, now_ns: u64) -> Result<u64, ScheduleError> {
35        checked_deadline_after(now_ns, self.0)
36    }
37}
38
39/// Initial or explicit request for a timer deadline.
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum TimerSchedule {
42    /// Schedule relative to the current IC time.
43    After(Duration),
44    /// Schedule at an absolute IC timestamp in nanoseconds.
45    At(u64),
46}
47
48impl TimerSchedule {
49    /// Resolve the request with its absolute deadline, relative delay and mode.
50    pub(super) fn resolve(self, now_ns: u64) -> Result<ResolvedSchedule, ScheduleError> {
51        match self {
52            Self::After(delay) => {
53                let delay_ns = duration_ns(delay)?;
54                Ok(ResolvedSchedule {
55                    deadline_ns: checked_deadline_after(now_ns, delay_ns)?,
56                    requested_delay_ns: Some(delay_ns),
57                    mode: TimerSchedulingMode::Once,
58                })
59            }
60            Self::At(deadline_ns) => Ok(ResolvedSchedule {
61                deadline_ns,
62                requested_delay_ns: None,
63                mode: TimerSchedulingMode::Deadline,
64            }),
65        }
66    }
67}
68
69/// Scheduling decision returned after one bounded ordinary invocation.
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum TimerDirective {
72    /// Do not schedule another invocation.
73    Stop,
74    /// Continue as soon as the runtime can execute another message.
75    ContinueImmediately,
76    /// Retry after the provided delay.
77    RetryAfter(Duration),
78    /// Schedule at an absolute IC timestamp in nanoseconds.
79    ScheduleAt(u64),
80    /// Recur using the registration's configured after-completion cadence.
81    RecurAfterCompletion,
82}
83
84impl TimerDirective {
85    pub(super) fn resolve(
86        self,
87        now_ns: u64,
88        cadence: Option<TimerCadence>,
89    ) -> Result<Option<ResolvedSchedule>, DirectiveError> {
90        match self {
91            Self::Stop => Ok(None),
92            Self::ContinueImmediately => Ok(Some(ResolvedSchedule {
93                deadline_ns: now_ns,
94                requested_delay_ns: Some(0),
95                mode: TimerSchedulingMode::Continuation,
96            })),
97            Self::RetryAfter(delay) => {
98                let delay_ns = duration_ns(delay)?;
99                Ok(Some(ResolvedSchedule {
100                    deadline_ns: checked_deadline_after(now_ns, delay_ns)?,
101                    requested_delay_ns: Some(delay_ns),
102                    mode: TimerSchedulingMode::Retry,
103                }))
104            }
105            Self::ScheduleAt(deadline_ns) => Ok(Some(ResolvedSchedule {
106                deadline_ns,
107                requested_delay_ns: None,
108                mode: TimerSchedulingMode::Deadline,
109            })),
110            Self::RecurAfterCompletion => {
111                let cadence = cadence.ok_or(DirectiveError::MissingCadence)?;
112                Ok(Some(ResolvedSchedule {
113                    deadline_ns: cadence.deadline_after(now_ns)?,
114                    requested_delay_ns: Some(cadence.as_nanos()),
115                    mode: TimerSchedulingMode::AfterCompletion,
116                }))
117            }
118        }
119    }
120}
121
122/// Failure to represent a cadence or requested deadline.
123#[non_exhaustive]
124#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
125pub enum ScheduleError {
126    /// A recurring timer must advance by a positive duration.
127    #[error("timer cadence must be greater than zero")]
128    ZeroCadence,
129    /// The duration cannot be represented as nanoseconds in a `u64`.
130    #[error("timer delay exceeds the supported nanosecond range")]
131    DelayOutOfRange,
132    /// Adding the delay to the current time overflows a `u64`.
133    #[error("timer deadline exceeds the supported timestamp range")]
134    DeadlineOverflow,
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum DirectiveError {
139    Schedule(ScheduleError),
140    MissingCadence,
141}
142
143impl From<ScheduleError> for DirectiveError {
144    fn from(error: ScheduleError) -> Self {
145        Self::Schedule(error)
146    }
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct ResolvedSchedule {
151    pub(crate) deadline_ns: u64,
152    pub(crate) requested_delay_ns: Option<u64>,
153    pub(crate) mode: TimerSchedulingMode,
154}
155
156const fn checked_deadline_after(now_ns: u64, delay_ns: u64) -> Result<u64, ScheduleError> {
157    match now_ns.checked_add(delay_ns) {
158        Some(deadline_ns) => Ok(deadline_ns),
159        None => Err(ScheduleError::DeadlineOverflow),
160    }
161}
162
163pub fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
164    u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn cadence_is_positive_bounded_and_checked() {
173        assert_eq!(
174            TimerCadence::new(Duration::ZERO),
175            Err(ScheduleError::ZeroCadence)
176        );
177        assert_eq!(
178            TimerCadence::new(Duration::from_secs(u64::MAX)),
179            Err(ScheduleError::DelayOutOfRange)
180        );
181
182        let cadence =
183            TimerCadence::new(Duration::from_nanos(5)).expect("positive cadence should be valid");
184        assert_eq!(cadence.as_nanos(), 5);
185        assert_eq!(cadence.deadline_after(10), Ok(15));
186        assert_eq!(
187            cadence.deadline_after(u64::MAX),
188            Err(ScheduleError::DeadlineOverflow)
189        );
190    }
191
192    #[test]
193    fn schedules_and_directives_resolve_without_truncation() {
194        assert_eq!(
195            TimerSchedule::After(Duration::from_nanos(5)).resolve(10),
196            Ok(ResolvedSchedule {
197                deadline_ns: 15,
198                requested_delay_ns: Some(5),
199                mode: TimerSchedulingMode::Once,
200            })
201        );
202        assert_eq!(
203            TimerSchedule::At(7).resolve(10),
204            Ok(ResolvedSchedule {
205                deadline_ns: 7,
206                requested_delay_ns: None,
207                mode: TimerSchedulingMode::Deadline,
208            })
209        );
210        assert_eq!(TimerDirective::Stop.resolve(10, None), Ok(None));
211        assert_eq!(
212            TimerDirective::ContinueImmediately.resolve(10, None),
213            Ok(Some(ResolvedSchedule {
214                deadline_ns: 10,
215                requested_delay_ns: Some(0),
216                mode: TimerSchedulingMode::Continuation,
217            }))
218        );
219        assert_eq!(
220            TimerDirective::RetryAfter(Duration::from_nanos(5)).resolve(10, None),
221            Ok(Some(ResolvedSchedule {
222                deadline_ns: 15,
223                requested_delay_ns: Some(5),
224                mode: TimerSchedulingMode::Retry,
225            }))
226        );
227        assert_eq!(
228            TimerDirective::ScheduleAt(7).resolve(10, None),
229            Ok(Some(ResolvedSchedule {
230                deadline_ns: 7,
231                requested_delay_ns: None,
232                mode: TimerSchedulingMode::Deadline,
233            }))
234        );
235
236        let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
237        assert_eq!(
238            TimerDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
239            Ok(Some(ResolvedSchedule {
240                deadline_ns: 19,
241                requested_delay_ns: Some(9),
242                mode: TimerSchedulingMode::AfterCompletion,
243            }))
244        );
245        assert_eq!(
246            TimerDirective::RecurAfterCompletion.resolve(10, None),
247            Err(DirectiveError::MissingCadence)
248        );
249        assert_eq!(
250            TimerDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
251            Err(DirectiveError::Schedule(ScheduleError::DeadlineOverflow))
252        );
253    }
254}