Skip to main content

ic_timers/snapshot/
model.rs

1//! Portable scheduling, state, outcome, and epoch values.
2
3use crate::{ScheduleError, TimerDirective, TimerRegistration};
4use std::time::Duration;
5
6/// Configured recurrence policy for one logical timer.
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub enum TimerPolicy {
9    /// Run at most once unless an explicit later request schedules it again.
10    Once,
11    /// Arm the next run after the current callback completes.
12    AfterCompletion {
13        /// Configured delay following completion, in nanoseconds.
14        cadence_ns: u64,
15    },
16    /// Pre-arm a successor before invoking fallible work.
17    Watchdog {
18        /// Configured watchdog cadence, in nanoseconds.
19        cadence_ns: u64,
20    },
21}
22
23impl TimerPolicy {
24    /// Return the stable configured-policy label.
25    #[must_use]
26    pub const fn label(self) -> &'static str {
27        match self {
28            Self::Once => "once",
29            Self::AfterCompletion { .. } => "after_completion",
30            Self::Watchdog { .. } => "watchdog",
31        }
32    }
33
34    /// Return a configured cadence when the policy recurs.
35    #[must_use]
36    pub const fn cadence_ns(self) -> Option<u64> {
37        match self {
38            Self::Once => None,
39            Self::AfterCompletion { cadence_ns } | Self::Watchdog { cadence_ns } => {
40                Some(cadence_ns)
41            }
42        }
43    }
44
45    /// Return the initial effective scheduling mode.
46    #[must_use]
47    pub const fn initial_mode(self) -> TimerSchedulingMode {
48        match self {
49            Self::Once => TimerSchedulingMode::Once,
50            Self::AfterCompletion { .. } => TimerSchedulingMode::AfterCompletion,
51            Self::Watchdog { .. } => TimerSchedulingMode::Watchdog,
52        }
53    }
54}
55
56/// Effective reason for the currently authoritative schedule.
57#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum TimerSchedulingMode {
59    /// Initial or explicitly requested one-shot work.
60    Once,
61    /// Recurrence delayed from the previous completion.
62    AfterCompletion,
63    /// An explicit absolute deadline.
64    Deadline,
65    /// A delayed retry following an expected failure.
66    Retry,
67    /// Immediate continuation of bounded work.
68    Continuation,
69    /// A successor committed before fallible work begins.
70    Watchdog,
71}
72
73impl TimerSchedulingMode {
74    /// Return a stable adapter-friendly label.
75    #[must_use]
76    pub const fn label(self) -> &'static str {
77        match self {
78            Self::Once => "once",
79            Self::AfterCompletion => "after_completion",
80            Self::Deadline => "deadline",
81            Self::Retry => "retry",
82            Self::Continuation => "continuation",
83            Self::Watchdog => "watchdog",
84        }
85    }
86}
87
88/// Portable representation of the latest post-run scheduling directive.
89#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub enum TimerDirectiveSnapshot {
91    /// Stop after the completed invocation.
92    Stop,
93    /// Continue in the next available timer message.
94    ContinueImmediately,
95    /// Retry after a relative delay.
96    RetryAfter {
97        /// Requested delay in nanoseconds.
98        delay_ns: u64,
99    },
100    /// Schedule at one absolute IC timestamp.
101    ScheduleAt {
102        /// Absolute IC timestamp in nanoseconds.
103        deadline_ns: u64,
104    },
105    /// Recur after a delay measured from completion.
106    RecurAfter {
107        /// Requested delay in nanoseconds.
108        delay_ns: u64,
109    },
110}
111
112impl TimerDirectiveSnapshot {
113    /// Return the scheduling mode produced by this directive, if any.
114    #[must_use]
115    pub const fn scheduling_mode(self) -> Option<TimerSchedulingMode> {
116        match self {
117            Self::Stop => None,
118            Self::ContinueImmediately => Some(TimerSchedulingMode::Continuation),
119            Self::RetryAfter { .. } => Some(TimerSchedulingMode::Retry),
120            Self::ScheduleAt { .. } => Some(TimerSchedulingMode::Deadline),
121            Self::RecurAfter { .. } => Some(TimerSchedulingMode::AfterCompletion),
122        }
123    }
124}
125
126impl TryFrom<TimerDirective> for TimerDirectiveSnapshot {
127    type Error = ScheduleError;
128
129    fn try_from(value: TimerDirective) -> Result<Self, Self::Error> {
130        Ok(match value {
131            TimerDirective::Stop => Self::Stop,
132            TimerDirective::ContinueImmediately => Self::ContinueImmediately,
133            TimerDirective::RetryAfter(delay) => Self::RetryAfter {
134                delay_ns: duration_ns(delay)?,
135            },
136            TimerDirective::ScheduleAt(deadline_ns) => Self::ScheduleAt { deadline_ns },
137            TimerDirective::RecurAfter(delay) => Self::RecurAfter {
138                delay_ns: duration_ns(delay)?,
139            },
140        })
141    }
142}
143
144impl From<TimerDirectiveSnapshot> for TimerDirective {
145    fn from(value: TimerDirectiveSnapshot) -> Self {
146        match value {
147            TimerDirectiveSnapshot::Stop => Self::Stop,
148            TimerDirectiveSnapshot::ContinueImmediately => Self::ContinueImmediately,
149            TimerDirectiveSnapshot::RetryAfter { delay_ns } => {
150                Self::RetryAfter(Duration::from_nanos(delay_ns))
151            }
152            TimerDirectiveSnapshot::ScheduleAt { deadline_ns } => Self::ScheduleAt(deadline_ns),
153            TimerDirectiveSnapshot::RecurAfter { delay_ns } => {
154                Self::RecurAfter(Duration::from_nanos(delay_ns))
155            }
156        }
157    }
158}
159
160fn duration_ns(duration: Duration) -> Result<u64, ScheduleError> {
161    u64::try_from(duration.as_nanos()).map_err(|_| ScheduleError::DelayOutOfRange)
162}
163
164/// One watchdog successor made authoritative before the current work.
165#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
166pub struct PreArmedSuccessor {
167    /// Generation the successor callback must present.
168    pub generation: u64,
169    /// Absolute successor deadline in nanoseconds.
170    pub deadline_ns: u64,
171}
172
173/// Scheduling portion of the canonical timer snapshot.
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub struct TimerSchedulingSnapshot {
176    /// Timer's configured behavior across successful runs.
177    pub configured_policy: TimerPolicy,
178    /// Reason the currently authoritative deadline was selected.
179    pub current_mode: TimerSchedulingMode,
180    /// Most recent completed callback directive.
181    pub latest_directive: Option<TimerDirectiveSnapshot>,
182    /// Most recent relative delay requested from the wrapper.
183    pub latest_requested_delay_ns: Option<u64>,
184    /// Most recent relative delay actually armed with the provider.
185    pub latest_armed_delay_ns: Option<u64>,
186    /// Next authoritative absolute deadline.
187    pub next_deadline_ns: Option<u64>,
188    /// Watchdog successor committed before current fallible work.
189    pub pre_armed_successor: Option<PreArmedSuccessor>,
190}
191
192impl TimerSchedulingSnapshot {
193    /// Construct an unscheduled snapshot for a configured policy.
194    #[must_use]
195    pub const fn new(configured_policy: TimerPolicy) -> Self {
196        Self {
197            configured_policy,
198            current_mode: configured_policy.initial_mode(),
199            latest_directive: None,
200            latest_requested_delay_ns: None,
201            latest_armed_delay_ns: None,
202            next_deadline_ns: None,
203            pre_armed_successor: None,
204        }
205    }
206}
207
208/// Portable projection of the control registration.
209#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
210pub enum TimerRegistrationStatus {
211    /// No provider callback is registered or running.
212    Unregistered,
213    /// One provider callback is scheduled.
214    Scheduled,
215    /// One callback owns logical execution.
216    Running,
217}
218
219impl TimerRegistrationStatus {
220    /// Return a stable adapter-friendly label.
221    #[must_use]
222    pub const fn label(self) -> &'static str {
223        match self {
224            Self::Unregistered => "unregistered",
225            Self::Scheduled => "scheduled",
226            Self::Running => "running",
227        }
228    }
229}
230
231impl From<TimerRegistration> for TimerRegistrationStatus {
232    fn from(value: TimerRegistration) -> Self {
233        match value {
234            TimerRegistration::Unregistered => Self::Unregistered,
235            TimerRegistration::Scheduled { .. } => Self::Scheduled,
236            TimerRegistration::Running { .. } => Self::Running,
237        }
238    }
239}
240
241/// Operator-facing condition of one timer process.
242#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
243pub enum TimerProcessCondition {
244    /// Configuration prevents the timer from running.
245    Disabled,
246    /// Enabled but without pending work.
247    Idle,
248    /// Scheduled or running normally.
249    Active,
250    /// Waiting for an expected retry.
251    Retrying,
252    /// Stopped by an invariant or terminal failure.
253    Failed,
254    /// Expected logical work has no provider registration.
255    MissingRegistration,
256}
257
258impl TimerProcessCondition {
259    /// Return a stable adapter-friendly label.
260    #[must_use]
261    pub const fn label(self) -> &'static str {
262        match self {
263            Self::Disabled => "disabled",
264            Self::Idle => "idle",
265            Self::Active => "active",
266            Self::Retrying => "retrying",
267            Self::Failed => "failed",
268            Self::MissingRegistration => "missing_registration",
269        }
270    }
271}
272
273/// State portion of the canonical timer snapshot.
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub struct TimerStateSnapshot {
276    /// Whether configuration permits future execution.
277    pub enabled: bool,
278    /// Current logical control registration.
279    pub registration: TimerRegistrationStatus,
280    /// Operator-facing process condition.
281    pub condition: TimerProcessCondition,
282    /// Latest allocated callback generation.
283    pub generation: u64,
284    /// Whether one callback currently owns logical execution.
285    pub in_flight: bool,
286}
287
288impl Default for TimerStateSnapshot {
289    fn default() -> Self {
290        Self {
291            enabled: true,
292            registration: TimerRegistrationStatus::Unregistered,
293            condition: TimerProcessCondition::Idle,
294            generation: 0,
295            in_flight: false,
296        }
297    }
298}
299
300/// Outcome of one callback that returned to the runtime.
301#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
302pub enum TimerCompletionOutcome {
303    /// Callback completed useful work successfully.
304    Success,
305    /// Callback completed normally but found no work.
306    NoWork,
307    /// Expected failure permits retry policy.
308    RetryableFailure,
309    /// Unexpected invariant or terminal failure.
310    InvariantFailure,
311}
312
313impl TimerCompletionOutcome {
314    /// Return a stable adapter-friendly label.
315    #[must_use]
316    pub const fn label(self) -> &'static str {
317        match self {
318            Self::Success => "success",
319            Self::NoWork => "no_work",
320            Self::RetryableFailure => "retryable_failure",
321            Self::InvariantFailure => "invariant_failure",
322        }
323    }
324}
325
326/// Latest observed terminal event for a timer invocation.
327#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
328pub enum TimerLastOutcome {
329    /// One callback returned with a classified completion.
330    Completed(TimerCompletionOutcome),
331    /// A started generation was later established not to have completed.
332    Interrupted,
333}
334
335/// Classified result of one returned callback.
336#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub struct TimerCompletion {
338    /// Completion classification.
339    pub outcome: TimerCompletionOutcome,
340    /// Bounded units of application work completed by this invocation.
341    pub work_count: u64,
342}
343
344impl TimerCompletion {
345    /// Construct successful completed work.
346    #[must_use]
347    pub const fn success(work_count: u64) -> Self {
348        Self {
349            outcome: TimerCompletionOutcome::Success,
350            work_count,
351        }
352    }
353
354    /// Construct a valid no-work completion.
355    #[must_use]
356    pub const fn no_work() -> Self {
357        Self {
358            outcome: TimerCompletionOutcome::NoWork,
359            work_count: 0,
360        }
361    }
362
363    /// Construct an expected failure, retaining any completed partial work.
364    #[must_use]
365    pub const fn retryable_failure(work_count: u64) -> Self {
366        Self {
367            outcome: TimerCompletionOutcome::RetryableFailure,
368            work_count,
369        }
370    }
371
372    /// Construct an invariant failure, retaining any completed partial work.
373    #[must_use]
374    pub const fn invariant_failure(work_count: u64) -> Self {
375        Self {
376            outcome: TimerCompletionOutcome::InvariantFailure,
377            work_count,
378        }
379    }
380}
381
382/// Latest outcome and functional failure state for one timer.
383#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
384pub struct TimerOutcomeSnapshot {
385    last_outcome: Option<TimerLastOutcome>,
386    last_work_count: Option<u64>,
387    last_success_at_ns: Option<u64>,
388    last_failure_at_ns: Option<u64>,
389    last_interrupted_at_ns: Option<u64>,
390    consecutive_expected_failures: u64,
391}
392
393impl TimerOutcomeSnapshot {
394    /// Record one returned callback using saturating failure-streak arithmetic.
395    pub const fn record_completion(&mut self, completion: TimerCompletion, completed_at_ns: u64) {
396        self.last_outcome = Some(TimerLastOutcome::Completed(completion.outcome));
397        self.last_work_count = Some(completion.work_count);
398        match completion.outcome {
399            TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
400                self.last_success_at_ns = Some(completed_at_ns);
401                self.consecutive_expected_failures = 0;
402            }
403            TimerCompletionOutcome::RetryableFailure => {
404                self.last_failure_at_ns = Some(completed_at_ns);
405                self.consecutive_expected_failures =
406                    self.consecutive_expected_failures.saturating_add(1);
407            }
408            TimerCompletionOutcome::InvariantFailure => {
409                self.last_failure_at_ns = Some(completed_at_ns);
410                self.consecutive_expected_failures = 0;
411            }
412        }
413    }
414
415    /// Record an interruption observed by recovery or reconstruction.
416    ///
417    /// An interruption is not a completed callback and does not change the
418    /// expected-failure streak.
419    pub const fn record_interruption(&mut self, observed_at_ns: u64) {
420        self.last_outcome = Some(TimerLastOutcome::Interrupted);
421        self.last_work_count = None;
422        self.last_interrupted_at_ns = Some(observed_at_ns);
423    }
424
425    /// Return the latest terminal event.
426    #[must_use]
427    pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
428        self.last_outcome
429    }
430
431    /// Return work reported by the latest completion, or `None` after interruption.
432    #[must_use]
433    pub const fn last_work_count(self) -> Option<u64> {
434        self.last_work_count
435    }
436
437    /// Return the latest successful or valid no-work completion time.
438    #[must_use]
439    pub const fn last_success_at_ns(self) -> Option<u64> {
440        self.last_success_at_ns
441    }
442
443    /// Return the latest expected or invariant failure completion time.
444    #[must_use]
445    pub const fn last_failure_at_ns(self) -> Option<u64> {
446        self.last_failure_at_ns
447    }
448
449    /// Return the latest time an incomplete generation was established.
450    #[must_use]
451    pub const fn last_interrupted_at_ns(self) -> Option<u64> {
452        self.last_interrupted_at_ns
453    }
454
455    /// Return consecutive retryable failures since the latest reset outcome.
456    #[must_use]
457    pub const fn consecutive_expected_failures(self) -> u64 {
458        self.consecutive_expected_failures
459    }
460}
461
462/// Identity and start time of one runtime-local observation epoch.
463#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
464pub struct TimerEpoch {
465    /// Monotonic runtime epoch chosen by the lifecycle owner.
466    pub id: u64,
467    /// IC timestamp at which this observation epoch began.
468    pub started_at_ns: u64,
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn expected_failure_streak_saturates() {
477        let mut outcomes = TimerOutcomeSnapshot {
478            consecutive_expected_failures: u64::MAX,
479            ..TimerOutcomeSnapshot::default()
480        };
481
482        outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
483
484        assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
485    }
486}