Skip to main content

ic_timers/
schedule.rs

1//! Checked cadence and scheduling decisions.
2
3use std::time::Duration;
4use thiserror::Error;
5
6/// Validated positive recurrence cadence.
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct TimerCadence(u64);
9
10impl TimerCadence {
11    /// Validate a recurrence cadence.
12    pub fn new(cadence: Duration) -> Result<Self, ScheduleError> {
13        let cadence_ns = duration_ns(cadence)?;
14        Self::from_nanos(cadence_ns)
15    }
16
17    /// Validate an already encoded nanosecond cadence.
18    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    /// Return the encoded cadence in nanoseconds.
27    #[must_use]
28    pub const fn as_nanos(self) -> u64 {
29        self.0
30    }
31
32    /// Resolve one successor relative to the supplied dispatch time.
33    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/// Initial or explicit request for a timer deadline.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum TimerSchedule {
41    /// Schedule relative to the current IC time.
42    After(Duration),
43    /// Schedule at an absolute IC timestamp in nanoseconds.
44    At(u64),
45}
46
47impl TimerSchedule {
48    /// Resolve the request to an absolute deadline and optional relative delay.
49    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/// Scheduling decision returned after one bounded ordinary invocation.
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum TimerDirective {
69    /// Do not schedule another invocation.
70    Stop,
71    /// Continue as soon as the runtime can execute another message.
72    ContinueImmediately,
73    /// Retry after the provided delay.
74    RetryAfter(Duration),
75    /// Schedule at an absolute IC timestamp in nanoseconds.
76    ScheduleAt(u64),
77    /// Recur using the registration's configured after-completion cadence.
78    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/// Failure to represent a cadence or requested deadline.
119#[non_exhaustive]
120#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
121pub enum ScheduleError {
122    /// A recurring timer must advance by a positive duration.
123    #[error("timer cadence must be greater than zero")]
124    ZeroCadence,
125    /// The duration cannot be represented as nanoseconds in a `u64`.
126    #[error("timer delay exceeds the supported nanosecond range")]
127    DelayOutOfRange,
128    /// Adding the delay to the current time overflows a `u64`.
129    #[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}