1use crate::{
4 callback::{AfterCompletionDecision, OnceDecision},
5 schedule::{OrdinaryDirective, ScheduleError, TimerCadence, duration_ns},
6};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub enum TimerPolicy {
11 Once,
13 AfterCompletion {
15 cadence: TimerCadence,
17 },
18 Watchdog {
20 cadence: TimerCadence,
22 },
23}
24
25impl TimerPolicy {
26 #[must_use]
28 pub const fn label(self) -> &'static str {
29 match self {
30 Self::Once => "once",
31 Self::AfterCompletion { .. } => "after_completion",
32 Self::Watchdog { .. } => "watchdog",
33 }
34 }
35
36 #[must_use]
38 pub const fn cadence(self) -> Option<TimerCadence> {
39 match self {
40 Self::Once => None,
41 Self::AfterCompletion { cadence } | Self::Watchdog { cadence } => Some(cadence),
42 }
43 }
44}
45
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub enum DeclarationLifetime {
52 Retained,
54 RemoveWhenStopped,
56}
57
58#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub enum TimerSchedulingMode {
64 Once,
66 AfterCompletion,
68 Deadline,
70 Retry,
72 Continuation,
74 Watchdog,
76}
77
78impl TimerSchedulingMode {
79 #[must_use]
81 pub const fn label(self) -> &'static str {
82 match self {
83 Self::Once => "once",
84 Self::AfterCompletion => "after_completion",
85 Self::Deadline => "deadline",
86 Self::Retry => "retry",
87 Self::Continuation => "continuation",
88 Self::Watchdog => "watchdog",
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub enum TimerDirectiveSnapshot {
97 Stop,
99 ContinueImmediately,
101 RetryAfter {
103 delay_ns: u64,
105 },
106 ScheduleAt {
108 deadline_ns: u64,
110 },
111 RecurAfterCompletion,
113}
114
115impl TryFrom<OrdinaryDirective> for TimerDirectiveSnapshot {
116 type Error = ScheduleError;
117
118 fn try_from(value: OrdinaryDirective) -> Result<Self, Self::Error> {
119 Ok(match value {
120 OrdinaryDirective::Stop => Self::Stop,
121 OrdinaryDirective::ContinueImmediately => Self::ContinueImmediately,
122 OrdinaryDirective::RetryAfter(delay) => Self::RetryAfter {
123 delay_ns: duration_ns(delay)?,
124 },
125 OrdinaryDirective::ScheduleAt(deadline_ns) => Self::ScheduleAt { deadline_ns },
126 OrdinaryDirective::RecurAfterCompletion => Self::RecurAfterCompletion,
127 })
128 }
129}
130
131impl TryFrom<OnceDecision> for TimerDirectiveSnapshot {
132 type Error = ScheduleError;
133
134 fn try_from(value: OnceDecision) -> Result<Self, Self::Error> {
135 Self::try_from(OrdinaryDirective::from(value))
136 }
137}
138
139impl TryFrom<AfterCompletionDecision> for TimerDirectiveSnapshot {
140 type Error = ScheduleError;
141
142 fn try_from(value: AfterCompletionDecision) -> Result<Self, Self::Error> {
143 Self::try_from(OrdinaryDirective::from(value))
144 }
145}
146
147#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
149pub enum TimerControlFailure {
150 GenerationExhausted,
153 DeadlineOverflow,
155 DelayOutOfRange,
157 DirectiveNotAllowed,
160 ProviderBindingFailed,
162}
163
164impl TimerControlFailure {
165 #[must_use]
167 pub const fn label(self) -> &'static str {
168 match self {
169 Self::GenerationExhausted => "generation_exhausted",
170 Self::DeadlineOverflow => "deadline_overflow",
171 Self::DelayOutOfRange => "delay_out_of_range",
172 Self::DirectiveNotAllowed => "directive_not_allowed",
173 Self::ProviderBindingFailed => "provider_binding_failed",
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum InactiveReason {
181 NeverScheduled,
183 Stopped,
185 Cancelled,
187 Abandoned,
190 InvariantFailure,
192 ControlFailure(TimerControlFailure),
194}
195
196#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
198pub enum OrdinaryRuntimeStateSnapshot {
199 Scheduled {
201 generation: u64,
203 deadline_ns: u64,
205 },
206 Running {
208 generation: u64,
210 },
211}
212
213#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
215pub enum WatchdogAttemptStatus {
216 Dispatched,
218 Running,
220}
221
222#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
224pub enum WatchdogRuntimeStateSnapshot {
225 Scheduled {
227 scheduler_generation: u64,
229 deadline_ns: u64,
231 },
232 AwaitingWork {
234 successor_generation: u64,
237 successor_deadline_ns: u64,
239 attempt_status: WatchdogAttemptStatus,
241 },
242}
243
244#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
246pub enum TimerRuntimeStateSnapshot {
247 Inactive {
249 reason: InactiveReason,
251 },
252 Ordinary(OrdinaryRuntimeStateSnapshot),
254 Watchdog(WatchdogRuntimeStateSnapshot),
256}
257
258impl TimerRuntimeStateSnapshot {
259 pub(crate) const fn next_deadline_ns(self) -> Option<u64> {
260 match self {
261 Self::Inactive { .. }
262 | Self::Ordinary(OrdinaryRuntimeStateSnapshot::Running { .. }) => None,
263 Self::Ordinary(OrdinaryRuntimeStateSnapshot::Scheduled { deadline_ns, .. })
264 | Self::Watchdog(WatchdogRuntimeStateSnapshot::Scheduled { deadline_ns, .. }) => {
265 Some(deadline_ns)
266 }
267 Self::Watchdog(WatchdogRuntimeStateSnapshot::AwaitingWork {
268 successor_deadline_ns,
269 ..
270 }) => Some(successor_deadline_ns),
271 }
272 }
273}
274
275#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
280pub enum TimerRegistrationStatus {
281 Unregistered,
283 Scheduled,
285 Running,
287}
288
289impl TimerRegistrationStatus {
290 #[must_use]
292 pub const fn label(self) -> &'static str {
293 match self {
294 Self::Unregistered => "unregistered",
295 Self::Scheduled => "scheduled",
296 Self::Running => "running",
297 }
298 }
299}
300
301impl From<TimerRuntimeStateSnapshot> for TimerRegistrationStatus {
302 fn from(value: TimerRuntimeStateSnapshot) -> Self {
303 match value {
304 TimerRuntimeStateSnapshot::Inactive { .. } => Self::Unregistered,
305 TimerRuntimeStateSnapshot::Ordinary(state) => match state {
306 OrdinaryRuntimeStateSnapshot::Scheduled { .. } => Self::Scheduled,
307 OrdinaryRuntimeStateSnapshot::Running { .. } => Self::Running,
308 },
309 TimerRuntimeStateSnapshot::Watchdog(state) => match state {
310 WatchdogRuntimeStateSnapshot::Scheduled { .. }
311 | WatchdogRuntimeStateSnapshot::AwaitingWork {
312 attempt_status: WatchdogAttemptStatus::Dispatched,
313 ..
314 } => Self::Scheduled,
315 WatchdogRuntimeStateSnapshot::AwaitingWork {
316 attempt_status: WatchdogAttemptStatus::Running,
317 ..
318 } => Self::Running,
319 },
320 }
321 }
322}
323
324#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
326pub enum TimerCompletionOutcome {
327 Success,
329 NoWork,
331 RetryableFailure,
333 InvariantFailure,
335}
336
337impl TimerCompletionOutcome {
338 #[must_use]
340 pub const fn label(self) -> &'static str {
341 match self {
342 Self::Success => "success",
343 Self::NoWork => "no_work",
344 Self::RetryableFailure => "retryable_failure",
345 Self::InvariantFailure => "invariant_failure",
346 }
347 }
348}
349
350#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
352pub enum TimerLastOutcome {
353 Completed(TimerCompletionOutcome),
355 Unacknowledged,
358}
359
360#[derive(Clone, Copy, Debug, Eq, PartialEq)]
362pub struct TimerCompletion {
363 outcome: TimerCompletionOutcome,
364 work_count: u64,
365}
366
367impl TimerCompletion {
368 #[must_use]
370 pub const fn success(work_count: u64) -> Self {
371 Self {
372 outcome: TimerCompletionOutcome::Success,
373 work_count,
374 }
375 }
376
377 #[must_use]
379 pub const fn no_work() -> Self {
380 Self {
381 outcome: TimerCompletionOutcome::NoWork,
382 work_count: 0,
383 }
384 }
385
386 #[must_use]
388 pub const fn retryable_failure(work_count: u64) -> Self {
389 Self {
390 outcome: TimerCompletionOutcome::RetryableFailure,
391 work_count,
392 }
393 }
394
395 #[must_use]
397 pub const fn invariant_failure(work_count: u64) -> Self {
398 Self {
399 outcome: TimerCompletionOutcome::InvariantFailure,
400 work_count,
401 }
402 }
403
404 #[must_use]
406 pub const fn outcome(self) -> TimerCompletionOutcome {
407 self.outcome
408 }
409
410 #[must_use]
412 pub const fn work_count(self) -> u64 {
413 self.work_count
414 }
415}
416
417#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
419pub enum WatchdogDecision {
420 Continue,
422 ContinueImmediately,
427 ScheduleAt(u64),
432 Stop,
434}
435
436#[derive(Clone, Copy, Debug, Eq, PartialEq)]
438pub struct WatchdogRunResult {
439 completion: TimerCompletion,
440 decision: WatchdogDecision,
441}
442
443impl WatchdogRunResult {
444 #[must_use]
451 pub const fn completed_batch(work_count: u64, next_due_ns: Option<u64>) -> Self {
452 let completion = if work_count == 0 {
453 TimerCompletion::no_work()
454 } else {
455 TimerCompletion::success(work_count)
456 };
457 let decision = match next_due_ns {
458 Some(at_ns) => WatchdogDecision::ScheduleAt(at_ns),
459 None => WatchdogDecision::Stop,
460 };
461 Self::new(completion, decision)
462 }
463
464 #[must_use]
466 pub const fn new(completion: TimerCompletion, decision: WatchdogDecision) -> Self {
467 Self {
468 decision: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
469 WatchdogDecision::Stop
470 } else {
471 decision
472 },
473 completion,
474 }
475 }
476
477 #[must_use]
479 pub const fn completion(self) -> TimerCompletion {
480 self.completion
481 }
482
483 #[must_use]
485 pub const fn decision(self) -> WatchdogDecision {
486 self.decision
487 }
488}
489
490#[derive(Clone, Copy, Debug, Eq, PartialEq)]
492enum RecordedOutcome {
493 Completed(TimerCompletion),
494 Unacknowledged,
495}
496
497#[derive(Clone, Copy, Debug, Eq, PartialEq)]
499pub struct TimerOutcomeSnapshot {
500 last_event: Option<RecordedOutcome>,
501 last_success_at_ns: Option<u64>,
502 last_failure_at_ns: Option<u64>,
503 last_unacknowledged_at_ns: Option<u64>,
504 consecutive_expected_failures: u64,
505}
506
507impl TimerOutcomeSnapshot {
508 pub(crate) const EMPTY: Self = Self {
509 last_event: None,
510 last_success_at_ns: None,
511 last_failure_at_ns: None,
512 last_unacknowledged_at_ns: None,
513 consecutive_expected_failures: 0,
514 };
515
516 pub(crate) const fn record_completion(
517 &mut self,
518 completion: TimerCompletion,
519 completed_at_ns: u64,
520 ) {
521 self.last_event = Some(RecordedOutcome::Completed(completion));
522 match completion.outcome {
523 TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
524 self.last_success_at_ns = Some(completed_at_ns);
525 self.consecutive_expected_failures = 0;
526 }
527 TimerCompletionOutcome::RetryableFailure => {
528 self.last_failure_at_ns = Some(completed_at_ns);
529 self.consecutive_expected_failures =
530 self.consecutive_expected_failures.saturating_add(1);
531 }
532 TimerCompletionOutcome::InvariantFailure => {
533 self.last_failure_at_ns = Some(completed_at_ns);
534 self.consecutive_expected_failures = 0;
535 }
536 }
537 }
538
539 pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
540 self.last_event = Some(RecordedOutcome::Unacknowledged);
541 self.last_unacknowledged_at_ns = Some(observed_at_ns);
542 }
543
544 #[must_use]
546 pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
547 match self.last_event {
548 Some(RecordedOutcome::Completed(completion)) => {
549 Some(TimerLastOutcome::Completed(completion.outcome()))
550 }
551 Some(RecordedOutcome::Unacknowledged) => Some(TimerLastOutcome::Unacknowledged),
552 None => None,
553 }
554 }
555
556 #[must_use]
558 pub const fn last_work_count(self) -> Option<u64> {
559 match self.last_event {
560 Some(RecordedOutcome::Completed(completion)) => Some(completion.work_count()),
561 Some(RecordedOutcome::Unacknowledged) | None => None,
562 }
563 }
564
565 #[must_use]
567 pub const fn last_success_at_ns(self) -> Option<u64> {
568 self.last_success_at_ns
569 }
570
571 #[must_use]
573 pub const fn last_failure_at_ns(self) -> Option<u64> {
574 self.last_failure_at_ns
575 }
576
577 #[must_use]
580 pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
581 self.last_unacknowledged_at_ns
582 }
583
584 #[must_use]
586 pub const fn consecutive_expected_failures(self) -> u64 {
587 self.consecutive_expected_failures
588 }
589}
590
591#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
593pub struct TimerEpoch {
594 canister_version: u64,
595 started_at_ns: u64,
596}
597
598impl TimerEpoch {
599 pub(crate) const fn new(canister_version: u64, started_at_ns: u64) -> Self {
600 Self {
601 canister_version,
602 started_at_ns,
603 }
604 }
605
606 #[must_use]
608 pub const fn canister_version(self) -> u64 {
609 self.canister_version
610 }
611
612 #[must_use]
614 pub const fn started_at_ns(self) -> u64 {
615 self.started_at_ns
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 #[test]
624 fn completed_batch_keeps_work_classification_and_deadline_independent() {
625 for work_count in [0, 1, u64::MAX] {
626 for next_due_ns in [None, Some(0), Some(u64::MAX)] {
627 let result = WatchdogRunResult::completed_batch(work_count, next_due_ns);
628 assert_eq!(result.completion().work_count(), work_count);
629 assert_eq!(
630 result.completion().outcome(),
631 if work_count == 0 {
632 TimerCompletionOutcome::NoWork
633 } else {
634 TimerCompletionOutcome::Success
635 }
636 );
637 assert_eq!(
638 result.decision(),
639 next_due_ns.map_or(WatchdogDecision::Stop, WatchdogDecision::ScheduleAt)
640 );
641 }
642 }
643 }
644
645 #[test]
646 fn latest_outcome_keeps_work_count_and_independent_history_coherent() {
647 let mut outcomes = TimerOutcomeSnapshot::EMPTY;
648 assert_eq!(outcomes.last_outcome(), None);
649 assert_eq!(outcomes.last_work_count(), None);
650
651 for (completion, completed_at_ns) in [
652 (TimerCompletion::success(0), 10),
653 (TimerCompletion::no_work(), 11),
654 (TimerCompletion::retryable_failure(7), 12),
655 ] {
656 outcomes.record_completion(completion, completed_at_ns);
657 assert_eq!(
658 outcomes.last_outcome(),
659 Some(TimerLastOutcome::Completed(completion.outcome()))
660 );
661 assert_eq!(outcomes.last_work_count(), Some(completion.work_count()));
662 }
663 outcomes.record_unacknowledged(20);
664 assert_eq!(
665 outcomes.last_outcome(),
666 Some(TimerLastOutcome::Unacknowledged)
667 );
668 assert_eq!(outcomes.last_work_count(), None);
669 assert_eq!(outcomes.last_success_at_ns(), Some(11));
670 assert_eq!(outcomes.last_failure_at_ns(), Some(12));
671 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
672 assert_eq!(outcomes.consecutive_expected_failures(), 1);
673
674 outcomes.record_completion(TimerCompletion::invariant_failure(2), 30);
675 assert_eq!(
676 outcomes.last_outcome(),
677 Some(TimerLastOutcome::Completed(
678 TimerCompletionOutcome::InvariantFailure
679 ))
680 );
681 assert_eq!(outcomes.last_work_count(), Some(2));
682 assert_eq!(outcomes.last_success_at_ns(), Some(11));
683 assert_eq!(outcomes.last_failure_at_ns(), Some(30));
684 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
685 assert_eq!(outcomes.consecutive_expected_failures(), 0);
686
687 outcomes.record_completion(TimerCompletion::success(4), 40);
688 assert_eq!(
689 outcomes.last_outcome(),
690 Some(TimerLastOutcome::Completed(TimerCompletionOutcome::Success))
691 );
692 assert_eq!(outcomes.last_work_count(), Some(4));
693 assert_eq!(outcomes.last_success_at_ns(), Some(40));
694 assert_eq!(outcomes.last_failure_at_ns(), Some(30));
695 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
696 }
697
698 #[test]
699 fn expected_failure_streak_saturates() {
700 let mut outcomes = TimerOutcomeSnapshot {
701 consecutive_expected_failures: u64::MAX,
702 ..TimerOutcomeSnapshot::EMPTY
703 };
704
705 outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
706
707 assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
708 }
709
710 #[test]
711 fn invariant_results_are_forced_to_stop() {
712 let watchdog = WatchdogRunResult::new(
713 TimerCompletion::invariant_failure(3),
714 WatchdogDecision::ContinueImmediately,
715 );
716 assert_eq!(watchdog.decision(), WatchdogDecision::Stop);
717 }
718}