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 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 an ordinary 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(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/// 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(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/// 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    /// After-completion recurrence was requested without a configured cadence.
132    #[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
155fn 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}