Skip to main content

ic_timers/snapshot/
model.rs

1//! Closed policy, state, outcome, and epoch values.
2
3use crate::schedule::{ScheduleError, TimerCadence, TimerDirective, duration_ns};
4
5/// Configured recurrence policy for one logical timer.
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub enum TimerPolicy {
8    /// Run at most once unless an explicit directive or request schedules it.
9    Once,
10    /// Permit recurrence at a configured cadence after callback completion.
11    AfterCompletion {
12        /// Validated configured cadence.
13        cadence: TimerCadence,
14    },
15    /// Commit a successor before dispatching synchronous fallible work.
16    Watchdog {
17        /// Validated configured cadence.
18        cadence: TimerCadence,
19    },
20}
21
22impl TimerPolicy {
23    /// Return the stable configured-policy label.
24    #[must_use]
25    pub const fn label(self) -> &'static str {
26        match self {
27            Self::Once => "once",
28            Self::AfterCompletion { .. } => "after_completion",
29            Self::Watchdog { .. } => "watchdog",
30        }
31    }
32
33    /// Return a configured cadence when the policy recurs.
34    #[must_use]
35    pub const fn cadence(self) -> Option<TimerCadence> {
36        match self {
37            Self::Once => None,
38            Self::AfterCompletion { cadence } | Self::Watchdog { cadence } => Some(cadence),
39        }
40    }
41}
42
43/// Whether a stopped declaration remains in the bounded registry.
44#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum DeclarationLifetime {
46    /// Keep callback authority available for a later ensure request.
47    Retained,
48    /// Remove the declaration after terminal completion or cancellation.
49    RemoveWhenStopped,
50}
51
52/// Latest effective scheduling reason for a declaration.
53///
54/// The value remains observable while inactive and is not itself evidence of
55/// an armed callback.
56#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum TimerSchedulingMode {
58    /// An explicit relative-delay schedule, including initial one-shot work.
59    Once,
60    /// Recurrence delayed from the previous completion.
61    AfterCompletion,
62    /// An explicit absolute deadline.
63    Deadline,
64    /// A delayed retry following an expected failure.
65    Retry,
66    /// Immediate continuation of bounded work.
67    Continuation,
68    /// A successor committed before fallible watchdog work.
69    Watchdog,
70}
71
72impl TimerSchedulingMode {
73    /// Return a stable adapter-friendly label.
74    #[must_use]
75    pub const fn label(self) -> &'static str {
76        match self {
77            Self::Once => "once",
78            Self::AfterCompletion => "after_completion",
79            Self::Deadline => "deadline",
80            Self::Retry => "retry",
81            Self::Continuation => "continuation",
82            Self::Watchdog => "watchdog",
83        }
84    }
85}
86
87/// Portable representation of the latest ordinary scheduling directive.
88#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub enum TimerDirectiveSnapshot {
90    /// Stop after the completed invocation.
91    Stop,
92    /// Continue in the next available timer message.
93    ContinueImmediately,
94    /// Retry after a relative delay.
95    RetryAfter {
96        /// Requested delay in nanoseconds.
97        delay_ns: u64,
98    },
99    /// Schedule at one absolute IC timestamp.
100    ScheduleAt {
101        /// Absolute IC timestamp in nanoseconds.
102        deadline_ns: u64,
103    },
104    /// Recur using the registration's configured cadence.
105    RecurAfterCompletion,
106}
107
108impl TimerDirectiveSnapshot {
109    pub(crate) const fn scheduling_mode(self) -> Option<TimerSchedulingMode> {
110        match self {
111            Self::Stop => None,
112            Self::ContinueImmediately => Some(TimerSchedulingMode::Continuation),
113            Self::RetryAfter { .. } => Some(TimerSchedulingMode::Retry),
114            Self::ScheduleAt { .. } => Some(TimerSchedulingMode::Deadline),
115            Self::RecurAfterCompletion => Some(TimerSchedulingMode::AfterCompletion),
116        }
117    }
118}
119
120impl TryFrom<TimerDirective> for TimerDirectiveSnapshot {
121    type Error = ScheduleError;
122
123    fn try_from(value: TimerDirective) -> Result<Self, Self::Error> {
124        Ok(match value {
125            TimerDirective::Stop => Self::Stop,
126            TimerDirective::ContinueImmediately => Self::ContinueImmediately,
127            TimerDirective::RetryAfter(delay) => Self::RetryAfter {
128                delay_ns: duration_ns(delay)?,
129            },
130            TimerDirective::ScheduleAt(deadline_ns) => Self::ScheduleAt { deadline_ns },
131            TimerDirective::RecurAfterCompletion => Self::RecurAfterCompletion,
132        })
133    }
134}
135
136/// Typed terminal failure in pure timer control.
137#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub enum TimerControlFailure {
139    /// A callback generation counter reached its maximum.
140    GenerationExhausted,
141    /// Checked successor deadline arithmetic overflowed.
142    DeadlineOverflow,
143    /// A requested relative delay cannot be encoded as `u64` nanoseconds.
144    DelayOutOfRange,
145    /// A directive is not legal for the timer's configured policy.
146    DirectiveNotAllowed,
147    /// A checked registry effect could not establish canonical provider ownership.
148    ProviderBindingFailed,
149}
150
151impl TimerControlFailure {
152    /// Return a stable adapter-friendly label.
153    #[must_use]
154    pub const fn label(self) -> &'static str {
155        match self {
156            Self::GenerationExhausted => "generation_exhausted",
157            Self::DeadlineOverflow => "deadline_overflow",
158            Self::DelayOutOfRange => "delay_out_of_range",
159            Self::DirectiveNotAllowed => "directive_not_allowed",
160            Self::ProviderBindingFailed => "provider_binding_failed",
161        }
162    }
163}
164
165/// Why a declaration currently has no scheduled or running callback generation.
166#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub enum InactiveReason {
168    /// The declaration has not yet been scheduled.
169    NeverScheduled,
170    /// Work returned a terminal stop decision.
171    Stopped,
172    /// Explicit cancellation won request arbitration.
173    Cancelled,
174    /// Consumer work reported an invariant or terminal failure.
175    InvariantFailure,
176    /// Checked pure control reached a terminal failure.
177    ControlFailure(TimerControlFailure),
178}
179
180/// Coherent ordinary timer state.
181#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
182pub enum OrdinaryRuntimeStateSnapshot {
183    /// One callback generation is scheduled.
184    Scheduled {
185        /// Generation the callback must present.
186        generation: u64,
187        /// Authoritative absolute deadline.
188        deadline_ns: u64,
189    },
190    /// One callback generation owns logical execution.
191    Running {
192        /// Generation owned by the running callback.
193        generation: u64,
194    },
195}
196
197/// Status of the watchdog attempt paired with a committed successor.
198#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
199pub enum WatchdogAttemptStatus {
200    /// The scheduler committed the work callback, which has not committed a start.
201    Dispatched,
202    /// The accepted work callback is currently executing synchronously.
203    Running,
204}
205
206/// One watchdog work attempt paired with an authoritative successor.
207#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
208pub struct WatchdogAttemptSnapshot {
209    generation: u64,
210    status: WatchdogAttemptStatus,
211}
212
213impl WatchdogAttemptSnapshot {
214    pub(crate) const fn new(generation: u64, status: WatchdogAttemptStatus) -> Self {
215        Self { generation, status }
216    }
217
218    /// Return the attempt generation.
219    #[must_use]
220    pub const fn generation(self) -> u64 {
221        self.generation
222    }
223
224    /// Return whether work is dispatched or running.
225    #[must_use]
226    pub const fn status(self) -> WatchdogAttemptStatus {
227        self.status
228    }
229}
230
231/// Coherent watchdog timer state.
232#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub enum WatchdogRuntimeStateSnapshot {
234    /// One scheduler generation is authoritative and no work is outstanding.
235    Scheduled {
236        /// Generation the scheduler callback must present.
237        scheduler_generation: u64,
238        /// Authoritative absolute successor deadline.
239        deadline_ns: u64,
240    },
241    /// A successor is authoritative while one work attempt is outstanding.
242    AwaitingWork {
243        /// Generation the successor scheduler must present.
244        successor_generation: u64,
245        /// Absolute successor deadline.
246        successor_deadline_ns: u64,
247        /// The one paired work attempt.
248        attempt: WatchdogAttemptSnapshot,
249    },
250}
251
252/// Closed policy-specific runtime state.
253#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
254pub enum TimerRuntimeStateSnapshot {
255    /// The declaration has no scheduled or running callback generation.
256    Inactive {
257        /// Reason scheduling is inactive.
258        reason: InactiveReason,
259    },
260    /// State legal only for `Once` and `AfterCompletion`.
261    Ordinary(OrdinaryRuntimeStateSnapshot),
262    /// State legal only for `Watchdog`.
263    Watchdog(WatchdogRuntimeStateSnapshot),
264}
265
266impl TimerRuntimeStateSnapshot {
267    pub(crate) const fn next_deadline_ns(self) -> Option<u64> {
268        match self {
269            Self::Inactive { .. }
270            | Self::Ordinary(OrdinaryRuntimeStateSnapshot::Running { .. }) => None,
271            Self::Ordinary(OrdinaryRuntimeStateSnapshot::Scheduled { deadline_ns, .. })
272            | Self::Watchdog(WatchdogRuntimeStateSnapshot::Scheduled { deadline_ns, .. }) => {
273                Some(deadline_ns)
274            }
275            Self::Watchdog(WatchdogRuntimeStateSnapshot::AwaitingWork {
276                successor_deadline_ns,
277                ..
278            }) => Some(successor_deadline_ns),
279        }
280    }
281}
282
283/// Portable projection of provider callback-generation state.
284///
285/// This is independent of declaration lifetime: a retained declaration may
286/// report `Unregistered` while it keeps callback authority for a later ensure.
287#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
288pub enum TimerRegistrationStatus {
289    /// No callback generation is scheduled or running.
290    Unregistered,
291    /// A wake-up generation is authoritative and consumer work is not running.
292    Scheduled,
293    /// Consumer work currently owns logical execution.
294    Running,
295}
296
297impl TimerRegistrationStatus {
298    /// Return a stable adapter-friendly label.
299    #[must_use]
300    pub const fn label(self) -> &'static str {
301        match self {
302            Self::Unregistered => "unregistered",
303            Self::Scheduled => "scheduled",
304            Self::Running => "running",
305        }
306    }
307}
308
309impl From<TimerRuntimeStateSnapshot> for TimerRegistrationStatus {
310    fn from(value: TimerRuntimeStateSnapshot) -> Self {
311        match value {
312            TimerRuntimeStateSnapshot::Inactive { .. } => Self::Unregistered,
313            TimerRuntimeStateSnapshot::Ordinary(state) => match state {
314                OrdinaryRuntimeStateSnapshot::Scheduled { .. } => Self::Scheduled,
315                OrdinaryRuntimeStateSnapshot::Running { .. } => Self::Running,
316            },
317            TimerRuntimeStateSnapshot::Watchdog(state) => match state {
318                WatchdogRuntimeStateSnapshot::Scheduled { .. }
319                | WatchdogRuntimeStateSnapshot::AwaitingWork {
320                    attempt:
321                        WatchdogAttemptSnapshot {
322                            status: WatchdogAttemptStatus::Dispatched,
323                            ..
324                        },
325                    ..
326                } => Self::Scheduled,
327                WatchdogRuntimeStateSnapshot::AwaitingWork {
328                    attempt:
329                        WatchdogAttemptSnapshot {
330                            status: WatchdogAttemptStatus::Running,
331                            ..
332                        },
333                    ..
334                } => Self::Running,
335            },
336        }
337    }
338}
339
340/// Operator-facing condition derived from coherent state and scheduling mode.
341#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
342pub enum TimerProcessCondition {
343    /// Explicit cancellation disabled the current declaration.
344    Disabled,
345    /// Declared but without pending work.
346    Idle,
347    /// Scheduled or running normally.
348    Active,
349    /// Waiting for an expected retry.
350    Retrying,
351    /// Stopped by a reported failure or invalid control state.
352    Failed,
353}
354
355impl TimerProcessCondition {
356    /// Return a stable adapter-friendly label.
357    #[must_use]
358    pub const fn label(self) -> &'static str {
359        match self {
360            Self::Disabled => "disabled",
361            Self::Idle => "idle",
362            Self::Active => "active",
363            Self::Retrying => "retrying",
364            Self::Failed => "failed",
365        }
366    }
367}
368
369/// Outcome of one callback that returned to the runtime.
370#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
371pub enum TimerCompletionOutcome {
372    /// Callback completed useful work successfully.
373    Success,
374    /// Callback completed normally but found no work.
375    NoWork,
376    /// Expected failure permits retry policy.
377    RetryableFailure,
378    /// Unexpected invariant or terminal failure.
379    InvariantFailure,
380}
381
382impl TimerCompletionOutcome {
383    /// Return a stable adapter-friendly label.
384    #[must_use]
385    pub const fn label(self) -> &'static str {
386        match self {
387            Self::Success => "success",
388            Self::NoWork => "no_work",
389            Self::RetryableFailure => "retryable_failure",
390            Self::InvariantFailure => "invariant_failure",
391        }
392    }
393}
394
395/// Latest observed terminal event for one timer invocation.
396#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
397pub enum TimerLastOutcome {
398    /// One callback returned with a classified completion.
399    Completed(TimerCompletionOutcome),
400    /// A committed watchdog dispatch was retired without committed completion.
401    Unacknowledged,
402}
403
404/// Classified result of one returned consumer invocation.
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub struct TimerCompletion {
407    outcome: TimerCompletionOutcome,
408    work_count: u64,
409}
410
411impl TimerCompletion {
412    /// Construct successful completed work.
413    #[must_use]
414    pub const fn success(work_count: u64) -> Self {
415        Self {
416            outcome: TimerCompletionOutcome::Success,
417            work_count,
418        }
419    }
420
421    /// Construct a valid no-work completion.
422    #[must_use]
423    pub const fn no_work() -> Self {
424        Self {
425            outcome: TimerCompletionOutcome::NoWork,
426            work_count: 0,
427        }
428    }
429
430    /// Construct an expected failure, retaining completed partial work.
431    #[must_use]
432    pub const fn retryable_failure(work_count: u64) -> Self {
433        Self {
434            outcome: TimerCompletionOutcome::RetryableFailure,
435            work_count,
436        }
437    }
438
439    /// Construct an invariant failure, retaining completed partial work.
440    #[must_use]
441    pub const fn invariant_failure(work_count: u64) -> Self {
442        Self {
443            outcome: TimerCompletionOutcome::InvariantFailure,
444            work_count,
445        }
446    }
447
448    /// Return the completion class.
449    #[must_use]
450    pub const fn outcome(self) -> TimerCompletionOutcome {
451        self.outcome
452    }
453
454    /// Return application work units reported by the consumer.
455    #[must_use]
456    pub const fn work_count(self) -> u64 {
457        self.work_count
458    }
459}
460
461/// Ordinary callback result with one scheduling proposal.
462///
463/// The registry validates that the proposal is legal for the configured
464/// policy.
465#[derive(Clone, Copy, Debug, Eq, PartialEq)]
466pub struct TimerRunResult {
467    completion: TimerCompletion,
468    directive: TimerDirective,
469}
470
471impl TimerRunResult {
472    /// Construct a result, forcing invariant failures to stop.
473    #[must_use]
474    pub const fn new(completion: TimerCompletion, directive: TimerDirective) -> Self {
475        Self {
476            directive: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
477                TimerDirective::Stop
478            } else {
479                directive
480            },
481            completion,
482        }
483    }
484
485    /// Return the completion classification and work count.
486    #[must_use]
487    pub const fn completion(self) -> TimerCompletion {
488        self.completion
489    }
490
491    /// Return the post-run scheduling proposal.
492    #[must_use]
493    pub const fn directive(self) -> TimerDirective {
494        self.directive
495    }
496}
497
498/// Watchdog decision after one synchronous bounded work attempt.
499#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
500pub enum WatchdogDecision {
501    /// Retain the successor committed by the scheduler message.
502    Continue,
503    /// Move the committed successor to the current IC time.
504    ///
505    /// The replacement remains a later scheduler message; this never invokes
506    /// consumer work recursively in the current message.
507    ContinueImmediately,
508    /// Replace the committed successor with an exact absolute IC deadline.
509    ///
510    /// A past deadline schedules a later message with zero delay. If work
511    /// traps, this proposal rolls back and the cadence successor remains.
512    ScheduleAt(u64),
513    /// Terminate and clear the committed successor.
514    Stop,
515}
516
517/// Synchronous watchdog work result.
518#[derive(Clone, Copy, Debug, Eq, PartialEq)]
519pub struct WatchdogRunResult {
520    completion: TimerCompletion,
521    decision: WatchdogDecision,
522}
523
524impl WatchdogRunResult {
525    /// Construct a result, forcing invariant failures to stop.
526    #[must_use]
527    pub const fn new(completion: TimerCompletion, decision: WatchdogDecision) -> Self {
528        Self {
529            decision: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
530                WatchdogDecision::Stop
531            } else {
532                decision
533            },
534            completion,
535        }
536    }
537
538    /// Return the completion classification and work count.
539    #[must_use]
540    pub const fn completion(self) -> TimerCompletion {
541        self.completion
542    }
543
544    /// Return how the committed successor should be retained or cleared.
545    #[must_use]
546    pub const fn decision(self) -> WatchdogDecision {
547        self.decision
548    }
549}
550
551/// Latest outcome and functional failure state for one timer.
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub struct TimerOutcomeSnapshot {
554    last_outcome: Option<TimerLastOutcome>,
555    last_work_count: Option<u64>,
556    last_success_at_ns: Option<u64>,
557    last_failure_at_ns: Option<u64>,
558    last_unacknowledged_at_ns: Option<u64>,
559    consecutive_expected_failures: u64,
560}
561
562impl TimerOutcomeSnapshot {
563    pub(crate) const EMPTY: Self = Self {
564        last_outcome: None,
565        last_work_count: None,
566        last_success_at_ns: None,
567        last_failure_at_ns: None,
568        last_unacknowledged_at_ns: None,
569        consecutive_expected_failures: 0,
570    };
571
572    pub(crate) const fn record_completion(
573        &mut self,
574        completion: TimerCompletion,
575        completed_at_ns: u64,
576    ) {
577        self.last_outcome = Some(TimerLastOutcome::Completed(completion.outcome));
578        self.last_work_count = Some(completion.work_count);
579        match completion.outcome {
580            TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
581                self.last_success_at_ns = Some(completed_at_ns);
582                self.consecutive_expected_failures = 0;
583            }
584            TimerCompletionOutcome::RetryableFailure => {
585                self.last_failure_at_ns = Some(completed_at_ns);
586                self.consecutive_expected_failures =
587                    self.consecutive_expected_failures.saturating_add(1);
588            }
589            TimerCompletionOutcome::InvariantFailure => {
590                self.last_failure_at_ns = Some(completed_at_ns);
591                self.consecutive_expected_failures = 0;
592            }
593        }
594    }
595
596    pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
597        self.last_outcome = Some(TimerLastOutcome::Unacknowledged);
598        self.last_work_count = None;
599        self.last_unacknowledged_at_ns = Some(observed_at_ns);
600    }
601
602    /// Return the latest terminal event.
603    #[must_use]
604    pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
605        self.last_outcome
606    }
607
608    /// Return work reported by the latest completion.
609    #[must_use]
610    pub const fn last_work_count(self) -> Option<u64> {
611        self.last_work_count
612    }
613
614    /// Return the latest successful or valid no-work completion time.
615    #[must_use]
616    pub const fn last_success_at_ns(self) -> Option<u64> {
617        self.last_success_at_ns
618    }
619
620    /// Return the latest expected or invariant failure completion time.
621    #[must_use]
622    pub const fn last_failure_at_ns(self) -> Option<u64> {
623        self.last_failure_at_ns
624    }
625
626    /// Return when a dispatched watchdog attempt was most recently retired.
627    #[must_use]
628    pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
629        self.last_unacknowledged_at_ns
630    }
631
632    /// Return consecutive retryable failures since the latest reset outcome.
633    #[must_use]
634    pub const fn consecutive_expected_failures(self) -> u64 {
635        self.consecutive_expected_failures
636    }
637}
638
639/// Identity and start time of one runtime-local observation epoch.
640#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
641pub struct TimerEpoch {
642    canister_version: u64,
643    started_at_ns: u64,
644}
645
646impl TimerEpoch {
647    pub(crate) const fn new(canister_version: u64, started_at_ns: u64) -> Self {
648        Self {
649            canister_version,
650            started_at_ns,
651        }
652    }
653
654    /// Return the IC canister version that owns this volatile epoch.
655    #[must_use]
656    pub const fn canister_version(self) -> u64 {
657        self.canister_version
658    }
659
660    /// Return the IC timestamp at which the epoch began.
661    #[must_use]
662    pub const fn started_at_ns(self) -> u64 {
663        self.started_at_ns
664    }
665}
666
667#[cfg(test)]
668mod tests {
669    use super::*;
670
671    #[test]
672    fn expected_failure_streak_saturates() {
673        let mut outcomes = TimerOutcomeSnapshot {
674            consecutive_expected_failures: u64::MAX,
675            ..TimerOutcomeSnapshot::EMPTY
676        };
677
678        outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
679
680        assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
681    }
682
683    #[test]
684    fn invariant_results_are_forced_to_stop() {
685        let ordinary = TimerRunResult::new(
686            TimerCompletion::invariant_failure(2),
687            TimerDirective::ContinueImmediately,
688        );
689        assert_eq!(ordinary.directive(), TimerDirective::Stop);
690
691        let watchdog = WatchdogRunResult::new(
692            TimerCompletion::invariant_failure(3),
693            WatchdogDecision::ContinueImmediately,
694        );
695        assert_eq!(watchdog.decision(), WatchdogDecision::Stop);
696    }
697}