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    /// A nested request sequence reached its maximum.
142    RequestSequenceExhausted,
143    /// Checked successor deadline arithmetic overflowed.
144    DeadlineOverflow,
145    /// A requested relative delay cannot be encoded as `u64` nanoseconds.
146    DelayOutOfRange,
147    /// A directive is not legal for the timer's configured policy.
148    DirectiveNotAllowed,
149    /// A checked registry effect could not establish canonical provider ownership.
150    ProviderBindingFailed,
151}
152
153impl TimerControlFailure {
154    /// Return a stable adapter-friendly label.
155    #[must_use]
156    pub const fn label(self) -> &'static str {
157        match self {
158            Self::GenerationExhausted => "generation_exhausted",
159            Self::RequestSequenceExhausted => "request_sequence_exhausted",
160            Self::DeadlineOverflow => "deadline_overflow",
161            Self::DelayOutOfRange => "delay_out_of_range",
162            Self::DirectiveNotAllowed => "directive_not_allowed",
163            Self::ProviderBindingFailed => "provider_binding_failed",
164        }
165    }
166}
167
168/// Why a declaration currently has no scheduled or running callback generation.
169#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
170pub enum InactiveReason {
171    /// The declaration has not yet been scheduled.
172    NeverScheduled,
173    /// Work returned a terminal stop decision.
174    Stopped,
175    /// Explicit cancellation won request arbitration.
176    Cancelled,
177    /// Consumer work reported an invariant or terminal failure.
178    InvariantFailure,
179    /// Checked pure control reached a terminal failure.
180    ControlFailure(TimerControlFailure),
181}
182
183/// Coherent ordinary timer state.
184#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
185pub enum OrdinaryRuntimeStateSnapshot {
186    /// One callback generation is scheduled.
187    Scheduled {
188        /// Generation the callback must present.
189        generation: u64,
190        /// Authoritative absolute deadline.
191        deadline_ns: u64,
192    },
193    /// One callback generation owns logical execution.
194    Running {
195        /// Generation owned by the running callback.
196        generation: u64,
197    },
198}
199
200/// Status of the watchdog attempt paired with a committed successor.
201#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
202pub enum WatchdogAttemptStatus {
203    /// The scheduler committed the work callback, which has not committed a start.
204    Dispatched,
205    /// The accepted work callback is currently executing synchronously.
206    Running,
207}
208
209/// One watchdog work attempt paired with an authoritative successor.
210#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
211pub struct WatchdogAttemptSnapshot {
212    generation: u64,
213    status: WatchdogAttemptStatus,
214}
215
216impl WatchdogAttemptSnapshot {
217    pub(crate) const fn new(generation: u64, status: WatchdogAttemptStatus) -> Self {
218        Self { generation, status }
219    }
220
221    /// Return the attempt generation.
222    #[must_use]
223    pub const fn generation(self) -> u64 {
224        self.generation
225    }
226
227    /// Return whether work is dispatched or running.
228    #[must_use]
229    pub const fn status(self) -> WatchdogAttemptStatus {
230        self.status
231    }
232}
233
234/// Coherent watchdog timer state.
235#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
236pub enum WatchdogRuntimeStateSnapshot {
237    /// One scheduler generation is authoritative and no work is outstanding.
238    Scheduled {
239        /// Generation the scheduler callback must present.
240        scheduler_generation: u64,
241        /// Authoritative absolute successor deadline.
242        deadline_ns: u64,
243    },
244    /// A successor is authoritative while one work attempt is outstanding.
245    AwaitingWork {
246        /// Generation the successor scheduler must present.
247        successor_generation: u64,
248        /// Absolute successor deadline.
249        successor_deadline_ns: u64,
250        /// The one paired work attempt.
251        attempt: WatchdogAttemptSnapshot,
252    },
253}
254
255/// Closed policy-specific runtime state.
256#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
257pub enum TimerRuntimeStateSnapshot {
258    /// The declaration has no scheduled or running callback generation.
259    Inactive {
260        /// Reason scheduling is inactive.
261        reason: InactiveReason,
262    },
263    /// State legal only for `Once` and `AfterCompletion`.
264    Ordinary(OrdinaryRuntimeStateSnapshot),
265    /// State legal only for `Watchdog`.
266    Watchdog(WatchdogRuntimeStateSnapshot),
267}
268
269impl TimerRuntimeStateSnapshot {
270    pub(crate) const fn next_deadline_ns(self) -> Option<u64> {
271        match self {
272            Self::Inactive { .. }
273            | Self::Ordinary(OrdinaryRuntimeStateSnapshot::Running { .. }) => None,
274            Self::Ordinary(OrdinaryRuntimeStateSnapshot::Scheduled { deadline_ns, .. })
275            | Self::Watchdog(WatchdogRuntimeStateSnapshot::Scheduled { deadline_ns, .. }) => {
276                Some(deadline_ns)
277            }
278            Self::Watchdog(WatchdogRuntimeStateSnapshot::AwaitingWork {
279                successor_deadline_ns,
280                ..
281            }) => Some(successor_deadline_ns),
282        }
283    }
284}
285
286/// Portable projection of provider callback-generation state.
287///
288/// This is independent of declaration lifetime: a retained declaration may
289/// report `Unregistered` while it keeps callback authority for a later ensure.
290#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub enum TimerRegistrationStatus {
292    /// No callback generation is scheduled or running.
293    Unregistered,
294    /// A wake-up generation is authoritative and consumer work is not running.
295    Scheduled,
296    /// Consumer work currently owns logical execution.
297    Running,
298}
299
300impl TimerRegistrationStatus {
301    /// Return a stable adapter-friendly label.
302    #[must_use]
303    pub const fn label(self) -> &'static str {
304        match self {
305            Self::Unregistered => "unregistered",
306            Self::Scheduled => "scheduled",
307            Self::Running => "running",
308        }
309    }
310}
311
312impl From<TimerRuntimeStateSnapshot> for TimerRegistrationStatus {
313    fn from(value: TimerRuntimeStateSnapshot) -> Self {
314        match value {
315            TimerRuntimeStateSnapshot::Inactive { .. } => Self::Unregistered,
316            TimerRuntimeStateSnapshot::Ordinary(state) => match state {
317                OrdinaryRuntimeStateSnapshot::Scheduled { .. } => Self::Scheduled,
318                OrdinaryRuntimeStateSnapshot::Running { .. } => Self::Running,
319            },
320            TimerRuntimeStateSnapshot::Watchdog(state) => match state {
321                WatchdogRuntimeStateSnapshot::Scheduled { .. }
322                | WatchdogRuntimeStateSnapshot::AwaitingWork {
323                    attempt:
324                        WatchdogAttemptSnapshot {
325                            status: WatchdogAttemptStatus::Dispatched,
326                            ..
327                        },
328                    ..
329                } => Self::Scheduled,
330                WatchdogRuntimeStateSnapshot::AwaitingWork {
331                    attempt:
332                        WatchdogAttemptSnapshot {
333                            status: WatchdogAttemptStatus::Running,
334                            ..
335                        },
336                    ..
337                } => Self::Running,
338            },
339        }
340    }
341}
342
343/// Operator-facing condition derived from coherent state and scheduling mode.
344#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
345pub enum TimerProcessCondition {
346    /// Explicit cancellation disabled the current declaration.
347    Disabled,
348    /// Declared but without pending work.
349    Idle,
350    /// Scheduled or running normally.
351    Active,
352    /// Waiting for an expected retry.
353    Retrying,
354    /// Stopped by a reported failure or invalid control state.
355    Failed,
356}
357
358impl TimerProcessCondition {
359    /// Return a stable adapter-friendly label.
360    #[must_use]
361    pub const fn label(self) -> &'static str {
362        match self {
363            Self::Disabled => "disabled",
364            Self::Idle => "idle",
365            Self::Active => "active",
366            Self::Retrying => "retrying",
367            Self::Failed => "failed",
368        }
369    }
370}
371
372/// Outcome of one callback that returned to the runtime.
373#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
374pub enum TimerCompletionOutcome {
375    /// Callback completed useful work successfully.
376    Success,
377    /// Callback completed normally but found no work.
378    NoWork,
379    /// Expected failure permits retry policy.
380    RetryableFailure,
381    /// Unexpected invariant or terminal failure.
382    InvariantFailure,
383}
384
385impl TimerCompletionOutcome {
386    /// Return a stable adapter-friendly label.
387    #[must_use]
388    pub const fn label(self) -> &'static str {
389        match self {
390            Self::Success => "success",
391            Self::NoWork => "no_work",
392            Self::RetryableFailure => "retryable_failure",
393            Self::InvariantFailure => "invariant_failure",
394        }
395    }
396}
397
398/// Latest observed terminal event for one timer invocation.
399#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
400pub enum TimerLastOutcome {
401    /// One callback returned with a classified completion.
402    Completed(TimerCompletionOutcome),
403    /// A committed watchdog dispatch was retired without committed completion.
404    Unacknowledged,
405}
406
407/// Classified result of one returned consumer invocation.
408#[derive(Clone, Copy, Debug, Eq, PartialEq)]
409pub struct TimerCompletion {
410    outcome: TimerCompletionOutcome,
411    work_count: u64,
412}
413
414impl TimerCompletion {
415    /// Construct successful completed work.
416    #[must_use]
417    pub const fn success(work_count: u64) -> Self {
418        Self {
419            outcome: TimerCompletionOutcome::Success,
420            work_count,
421        }
422    }
423
424    /// Construct a valid no-work completion.
425    #[must_use]
426    pub const fn no_work() -> Self {
427        Self {
428            outcome: TimerCompletionOutcome::NoWork,
429            work_count: 0,
430        }
431    }
432
433    /// Construct an expected failure, retaining completed partial work.
434    #[must_use]
435    pub const fn retryable_failure(work_count: u64) -> Self {
436        Self {
437            outcome: TimerCompletionOutcome::RetryableFailure,
438            work_count,
439        }
440    }
441
442    /// Construct an invariant failure, retaining completed partial work.
443    #[must_use]
444    pub const fn invariant_failure(work_count: u64) -> Self {
445        Self {
446            outcome: TimerCompletionOutcome::InvariantFailure,
447            work_count,
448        }
449    }
450
451    /// Return the completion class.
452    #[must_use]
453    pub const fn outcome(self) -> TimerCompletionOutcome {
454        self.outcome
455    }
456
457    /// Return application work units reported by the consumer.
458    #[must_use]
459    pub const fn work_count(self) -> u64 {
460        self.work_count
461    }
462}
463
464/// Ordinary callback result with one scheduling proposal.
465///
466/// The registry validates that the proposal is legal for the configured
467/// policy.
468#[derive(Clone, Copy, Debug, Eq, PartialEq)]
469pub struct TimerRunResult {
470    completion: TimerCompletion,
471    directive: TimerDirective,
472}
473
474impl TimerRunResult {
475    /// Construct a result, forcing invariant failures to stop.
476    #[must_use]
477    pub const fn new(completion: TimerCompletion, directive: TimerDirective) -> Self {
478        Self {
479            directive: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
480                TimerDirective::Stop
481            } else {
482                directive
483            },
484            completion,
485        }
486    }
487
488    /// Return the completion classification and work count.
489    #[must_use]
490    pub const fn completion(self) -> TimerCompletion {
491        self.completion
492    }
493
494    /// Return the post-run scheduling proposal.
495    #[must_use]
496    pub const fn directive(self) -> TimerDirective {
497        self.directive
498    }
499}
500
501/// Watchdog decision after one synchronous bounded work attempt.
502#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
503pub enum WatchdogDecision {
504    /// Retain the successor committed by the scheduler message.
505    Continue,
506    /// Terminate and clear the committed successor.
507    Stop,
508}
509
510/// Synchronous watchdog work result.
511#[derive(Clone, Copy, Debug, Eq, PartialEq)]
512pub struct WatchdogRunResult {
513    completion: TimerCompletion,
514    decision: WatchdogDecision,
515}
516
517impl WatchdogRunResult {
518    /// Construct a result, forcing invariant failures to stop.
519    #[must_use]
520    pub const fn new(completion: TimerCompletion, decision: WatchdogDecision) -> Self {
521        Self {
522            decision: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
523                WatchdogDecision::Stop
524            } else {
525                decision
526            },
527            completion,
528        }
529    }
530
531    /// Return the completion classification and work count.
532    #[must_use]
533    pub const fn completion(self) -> TimerCompletion {
534        self.completion
535    }
536
537    /// Return whether the committed successor remains authoritative.
538    #[must_use]
539    pub const fn decision(self) -> WatchdogDecision {
540        self.decision
541    }
542}
543
544/// Latest outcome and functional failure state for one timer.
545#[derive(Clone, Copy, Debug, Eq, PartialEq)]
546pub struct TimerOutcomeSnapshot {
547    last_outcome: Option<TimerLastOutcome>,
548    last_work_count: Option<u64>,
549    last_success_at_ns: Option<u64>,
550    last_failure_at_ns: Option<u64>,
551    last_unacknowledged_at_ns: Option<u64>,
552    consecutive_expected_failures: u64,
553}
554
555impl TimerOutcomeSnapshot {
556    pub(crate) const EMPTY: Self = Self {
557        last_outcome: None,
558        last_work_count: None,
559        last_success_at_ns: None,
560        last_failure_at_ns: None,
561        last_unacknowledged_at_ns: None,
562        consecutive_expected_failures: 0,
563    };
564
565    pub(crate) const fn record_completion(
566        &mut self,
567        completion: TimerCompletion,
568        completed_at_ns: u64,
569    ) {
570        self.last_outcome = Some(TimerLastOutcome::Completed(completion.outcome));
571        self.last_work_count = Some(completion.work_count);
572        match completion.outcome {
573            TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
574                self.last_success_at_ns = Some(completed_at_ns);
575                self.consecutive_expected_failures = 0;
576            }
577            TimerCompletionOutcome::RetryableFailure => {
578                self.last_failure_at_ns = Some(completed_at_ns);
579                self.consecutive_expected_failures =
580                    self.consecutive_expected_failures.saturating_add(1);
581            }
582            TimerCompletionOutcome::InvariantFailure => {
583                self.last_failure_at_ns = Some(completed_at_ns);
584                self.consecutive_expected_failures = 0;
585            }
586        }
587    }
588
589    pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
590        self.last_outcome = Some(TimerLastOutcome::Unacknowledged);
591        self.last_work_count = None;
592        self.last_unacknowledged_at_ns = Some(observed_at_ns);
593    }
594
595    /// Return the latest terminal event.
596    #[must_use]
597    pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
598        self.last_outcome
599    }
600
601    /// Return work reported by the latest completion.
602    #[must_use]
603    pub const fn last_work_count(self) -> Option<u64> {
604        self.last_work_count
605    }
606
607    /// Return the latest successful or valid no-work completion time.
608    #[must_use]
609    pub const fn last_success_at_ns(self) -> Option<u64> {
610        self.last_success_at_ns
611    }
612
613    /// Return the latest expected or invariant failure completion time.
614    #[must_use]
615    pub const fn last_failure_at_ns(self) -> Option<u64> {
616        self.last_failure_at_ns
617    }
618
619    /// Return when a dispatched watchdog attempt was most recently retired.
620    #[must_use]
621    pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
622        self.last_unacknowledged_at_ns
623    }
624
625    /// Return consecutive retryable failures since the latest reset outcome.
626    #[must_use]
627    pub const fn consecutive_expected_failures(self) -> u64 {
628        self.consecutive_expected_failures
629    }
630}
631
632/// Identity and start time of one runtime-local observation epoch.
633#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
634pub struct TimerEpoch {
635    canister_version: u64,
636    started_at_ns: u64,
637}
638
639impl TimerEpoch {
640    pub(crate) const fn new(canister_version: u64, started_at_ns: u64) -> Self {
641        Self {
642            canister_version,
643            started_at_ns,
644        }
645    }
646
647    /// Return the IC canister version that owns this volatile epoch.
648    #[must_use]
649    pub const fn canister_version(self) -> u64 {
650        self.canister_version
651    }
652
653    /// Return the IC timestamp at which the epoch began.
654    #[must_use]
655    pub const fn started_at_ns(self) -> u64 {
656        self.started_at_ns
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn expected_failure_streak_saturates() {
666        let mut outcomes = TimerOutcomeSnapshot {
667            consecutive_expected_failures: u64::MAX,
668            ..TimerOutcomeSnapshot::EMPTY
669        };
670
671        outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
672
673        assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
674    }
675
676    #[test]
677    fn invariant_results_are_forced_to_stop() {
678        let ordinary = TimerRunResult::new(
679            TimerCompletion::invariant_failure(2),
680            TimerDirective::ContinueImmediately,
681        );
682        assert_eq!(ordinary.directive(), TimerDirective::Stop);
683
684        let watchdog = WatchdogRunResult::new(
685            TimerCompletion::invariant_failure(3),
686            WatchdogDecision::Continue,
687        );
688        assert_eq!(watchdog.decision(), WatchdogDecision::Stop);
689    }
690}