Skip to main content

ic_timers/snapshot/
model.rs

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