Skip to main content

ic_timers/
schedule.rs

1//! Checked cadence and scheduling decisions.
2
3use crate::snapshot::{TimerControlFailure, TimerDirectiveSnapshot, 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/// Private scheduling proposal after erasing a policy-specific callback result.
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum OrdinaryDirective {
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 OrdinaryDirective {
85    pub(super) fn resolve(
86        self,
87        now_ns: u64,
88        cadence: Option<TimerCadence>,
89    ) -> Result<(Option<ResolvedSchedule>, TimerDirectiveSnapshot), TimerControlFailure> {
90        let snapshot =
91            TimerDirectiveSnapshot::try_from(self).map_err(ScheduleError::control_failure)?;
92        let schedule = match snapshot {
93            TimerDirectiveSnapshot::Stop => None,
94            TimerDirectiveSnapshot::ContinueImmediately => Some(ResolvedSchedule {
95                deadline_ns: now_ns,
96                requested_delay_ns: Some(0),
97                mode: TimerSchedulingMode::Continuation,
98            }),
99            TimerDirectiveSnapshot::RetryAfter { delay_ns } => Some(ResolvedSchedule {
100                deadline_ns: checked_deadline_after(now_ns, delay_ns)
101                    .map_err(ScheduleError::control_failure)?,
102                requested_delay_ns: Some(delay_ns),
103                mode: TimerSchedulingMode::Retry,
104            }),
105            TimerDirectiveSnapshot::ScheduleAt { deadline_ns } => Some(ResolvedSchedule {
106                deadline_ns,
107                requested_delay_ns: None,
108                mode: TimerSchedulingMode::Deadline,
109            }),
110            TimerDirectiveSnapshot::RecurAfterCompletion => {
111                let cadence = cadence.ok_or(TimerControlFailure::DirectiveNotAllowed)?;
112                Some(ResolvedSchedule {
113                    deadline_ns: cadence
114                        .deadline_after(now_ns)
115                        .map_err(ScheduleError::control_failure)?,
116                    requested_delay_ns: Some(cadence.as_nanos()),
117                    mode: TimerSchedulingMode::AfterCompletion,
118                })
119            }
120        };
121        Ok((schedule, snapshot))
122    }
123}
124
125/// Failure to represent a cadence or requested deadline.
126#[non_exhaustive]
127#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
128pub enum ScheduleError {
129    /// A recurring timer must advance by a positive duration.
130    #[error("timer cadence must be greater than zero")]
131    ZeroCadence,
132    /// The duration cannot be represented as nanoseconds in a `u64`.
133    #[error("timer delay exceeds the supported nanosecond range")]
134    DelayOutOfRange,
135    /// Adding the delay to the current time overflows a `u64`.
136    #[error("timer deadline exceeds the supported timestamp range")]
137    DeadlineOverflow,
138}
139
140impl ScheduleError {
141    // Invalid callback successors are terminal control failures; explicit
142    // scheduling requests keep returning ScheduleError at their input boundary.
143    const fn control_failure(self) -> TimerControlFailure {
144        match self {
145            Self::ZeroCadence => TimerControlFailure::DirectiveNotAllowed,
146            Self::DelayOutOfRange => TimerControlFailure::DelayOutOfRange,
147            Self::DeadlineOverflow => TimerControlFailure::DeadlineOverflow,
148        }
149    }
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct ResolvedSchedule {
154    pub(crate) deadline_ns: u64,
155    pub(crate) requested_delay_ns: Option<u64>,
156    pub(crate) mode: TimerSchedulingMode,
157}
158
159pub const fn checked_deadline_after(now_ns: u64, delay_ns: u64) -> Result<u64, ScheduleError> {
160    match now_ns.checked_add(delay_ns) {
161        Some(deadline_ns) => Ok(deadline_ns),
162        None => Err(ScheduleError::DeadlineOverflow),
163    }
164}
165
166pub fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
167    u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn cadence_is_positive_bounded_and_checked() {
176        assert_eq!(
177            TimerCadence::new(Duration::ZERO),
178            Err(ScheduleError::ZeroCadence)
179        );
180        assert_eq!(
181            TimerCadence::new(Duration::from_secs(u64::MAX)),
182            Err(ScheduleError::DelayOutOfRange)
183        );
184
185        let cadence =
186            TimerCadence::new(Duration::from_nanos(5)).expect("positive cadence should be valid");
187        assert_eq!(cadence.as_nanos(), 5);
188        assert_eq!(cadence.deadline_after(10), Ok(15));
189        assert_eq!(
190            cadence.deadline_after(u64::MAX),
191            Err(ScheduleError::DeadlineOverflow)
192        );
193    }
194
195    #[test]
196    fn schedules_and_directives_resolve_without_truncation() {
197        assert_eq!(
198            TimerSchedule::After(Duration::from_nanos(5)).resolve(10),
199            Ok(ResolvedSchedule {
200                deadline_ns: 15,
201                requested_delay_ns: Some(5),
202                mode: TimerSchedulingMode::Once,
203            })
204        );
205        assert_eq!(
206            TimerSchedule::At(7).resolve(10),
207            Ok(ResolvedSchedule {
208                deadline_ns: 7,
209                requested_delay_ns: None,
210                mode: TimerSchedulingMode::Deadline,
211            })
212        );
213        assert_eq!(
214            OrdinaryDirective::Stop.resolve(10, None),
215            Ok((None, TimerDirectiveSnapshot::Stop))
216        );
217        assert_eq!(
218            OrdinaryDirective::ContinueImmediately.resolve(10, None),
219            Ok((
220                Some(ResolvedSchedule {
221                    deadline_ns: 10,
222                    requested_delay_ns: Some(0),
223                    mode: TimerSchedulingMode::Continuation,
224                }),
225                TimerDirectiveSnapshot::ContinueImmediately
226            ))
227        );
228        for (now_ns, delay_ns, deadline_ns) in [
229            (10, 5, 15),
230            (u64::MAX, 0, u64::MAX),
231            (0, u64::MAX, u64::MAX),
232        ] {
233            assert_eq!(
234                OrdinaryDirective::RetryAfter(Duration::from_nanos(delay_ns)).resolve(now_ns, None),
235                Ok((
236                    Some(ResolvedSchedule {
237                        deadline_ns,
238                        requested_delay_ns: Some(delay_ns),
239                        mode: TimerSchedulingMode::Retry,
240                    }),
241                    TimerDirectiveSnapshot::RetryAfter { delay_ns }
242                ))
243            );
244        }
245        assert_eq!(
246            OrdinaryDirective::ScheduleAt(7).resolve(10, None),
247            Ok((
248                Some(ResolvedSchedule {
249                    deadline_ns: 7,
250                    requested_delay_ns: None,
251                    mode: TimerSchedulingMode::Deadline,
252                }),
253                TimerDirectiveSnapshot::ScheduleAt { deadline_ns: 7 }
254            ))
255        );
256
257        let cadence = TimerCadence::from_nanos(9).expect("fixture cadence should be valid");
258        assert_eq!(
259            OrdinaryDirective::RecurAfterCompletion.resolve(10, Some(cadence)),
260            Ok((
261                Some(ResolvedSchedule {
262                    deadline_ns: 19,
263                    requested_delay_ns: Some(9),
264                    mode: TimerSchedulingMode::AfterCompletion,
265                }),
266                TimerDirectiveSnapshot::RecurAfterCompletion
267            ))
268        );
269        assert_eq!(
270            OrdinaryDirective::RecurAfterCompletion.resolve(10, None),
271            Err(TimerControlFailure::DirectiveNotAllowed)
272        );
273        assert_eq!(
274            OrdinaryDirective::RetryAfter(Duration::from_nanos(1)).resolve(u64::MAX, None),
275            Err(TimerControlFailure::DeadlineOverflow)
276        );
277        assert_eq!(
278            OrdinaryDirective::RetryAfter(Duration::MAX).resolve(0, None),
279            Err(TimerControlFailure::DelayOutOfRange)
280        );
281        assert_eq!(
282            OrdinaryDirective::RecurAfterCompletion.resolve(u64::MAX, Some(cadence)),
283            Err(TimerControlFailure::DeadlineOverflow)
284        );
285    }
286}