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