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