1use crate::schedule::{ScheduleError, TimerCadence, TimerDirective, duration_ns};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub enum TimerPolicy {
8 Once,
10 AfterCompletion {
12 cadence: TimerCadence,
14 },
15 Watchdog {
17 cadence: TimerCadence,
19 },
20}
21
22impl TimerPolicy {
23 #[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum DeclarationLifetime {
46 Retained,
48 RemoveWhenStopped,
50}
51
52#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
57pub enum TimerSchedulingMode {
58 Once,
60 AfterCompletion,
62 Deadline,
64 Retry,
66 Continuation,
68 Watchdog,
70}
71
72impl TimerSchedulingMode {
73 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub enum TimerDirectiveSnapshot {
91 Stop,
93 ContinueImmediately,
95 RetryAfter {
97 delay_ns: u64,
99 },
100 ScheduleAt {
102 deadline_ns: u64,
104 },
105 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
127pub enum TimerControlFailure {
128 GenerationExhausted,
130 DeadlineOverflow,
132 DelayOutOfRange,
134 DirectiveNotAllowed,
136 ProviderBindingFailed,
138}
139
140impl TimerControlFailure {
141 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
156pub enum InactiveReason {
157 NeverScheduled,
159 Stopped,
161 Cancelled,
163 InvariantFailure,
165 ControlFailure(TimerControlFailure),
167}
168
169#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub enum OrdinaryRuntimeStateSnapshot {
172 Scheduled {
174 generation: u64,
176 deadline_ns: u64,
178 },
179 Running {
181 generation: u64,
183 },
184}
185
186#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub enum WatchdogAttemptStatus {
189 Dispatched,
191 Running,
193}
194
195#[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 #[must_use]
209 pub const fn generation(self) -> u64 {
210 self.generation
211 }
212
213 #[must_use]
215 pub const fn status(self) -> WatchdogAttemptStatus {
216 self.status
217 }
218}
219
220#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
222pub enum WatchdogRuntimeStateSnapshot {
223 Scheduled {
225 scheduler_generation: u64,
227 deadline_ns: u64,
229 },
230 AwaitingWork {
232 successor_generation: u64,
234 successor_deadline_ns: u64,
236 attempt: WatchdogAttemptSnapshot,
238 },
239}
240
241#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
243pub enum TimerRuntimeStateSnapshot {
244 Inactive {
246 reason: InactiveReason,
248 },
249 Ordinary(OrdinaryRuntimeStateSnapshot),
251 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
277pub enum TimerRegistrationStatus {
278 Unregistered,
280 Scheduled,
282 Running,
284}
285
286impl TimerRegistrationStatus {
287 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
331pub enum TimerProcessCondition {
332 Disabled,
334 Idle,
336 Active,
338 Retrying,
340 Failed,
342}
343
344impl TimerProcessCondition {
345 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
360pub enum TimerCompletionOutcome {
361 Success,
363 NoWork,
365 RetryableFailure,
367 InvariantFailure,
369}
370
371impl TimerCompletionOutcome {
372 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
386pub enum TimerLastOutcome {
387 Completed(TimerCompletionOutcome),
389 Unacknowledged,
391}
392
393#[derive(Clone, Copy, Debug, Eq, PartialEq)]
395pub struct TimerCompletion {
396 outcome: TimerCompletionOutcome,
397 work_count: u64,
398}
399
400impl TimerCompletion {
401 #[must_use]
403 pub const fn success(work_count: u64) -> Self {
404 Self {
405 outcome: TimerCompletionOutcome::Success,
406 work_count,
407 }
408 }
409
410 #[must_use]
412 pub const fn no_work() -> Self {
413 Self {
414 outcome: TimerCompletionOutcome::NoWork,
415 work_count: 0,
416 }
417 }
418
419 #[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 #[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 #[must_use]
439 pub const fn outcome(self) -> TimerCompletionOutcome {
440 self.outcome
441 }
442
443 #[must_use]
445 pub const fn work_count(self) -> u64 {
446 self.work_count
447 }
448}
449
450#[derive(Clone, Copy, Debug, Eq, PartialEq)]
456pub struct TimerRunResult {
457 completion: TimerCompletion,
458 directive: TimerDirective,
459}
460
461impl TimerRunResult {
462 #[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 #[must_use]
477 pub const fn completion(self) -> TimerCompletion {
478 self.completion
479 }
480
481 #[must_use]
483 pub const fn directive(self) -> TimerDirective {
484 self.directive
485 }
486}
487
488#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
490pub enum WatchdogDecision {
491 Continue,
493 ContinueImmediately,
498 ScheduleAt(u64),
503 Stop,
505}
506
507#[derive(Clone, Copy, Debug, Eq, PartialEq)]
509pub struct WatchdogRunResult {
510 completion: TimerCompletion,
511 decision: WatchdogDecision,
512}
513
514impl WatchdogRunResult {
515 #[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 #[must_use]
530 pub const fn completion(self) -> TimerCompletion {
531 self.completion
532 }
533
534 #[must_use]
536 pub const fn decision(self) -> WatchdogDecision {
537 self.decision
538 }
539}
540
541#[derive(Clone, Copy, Debug, Eq, PartialEq)]
543enum RecordedOutcome {
544 Completed(TimerCompletion),
545 Unacknowledged,
546}
547
548#[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 #[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 #[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 #[must_use]
618 pub const fn last_success_at_ns(self) -> Option<u64> {
619 self.last_success_at_ns
620 }
621
622 #[must_use]
624 pub const fn last_failure_at_ns(self) -> Option<u64> {
625 self.last_failure_at_ns
626 }
627
628 #[must_use]
630 pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
631 self.last_unacknowledged_at_ns
632 }
633
634 #[must_use]
636 pub const fn consecutive_expected_failures(self) -> u64 {
637 self.consecutive_expected_failures
638 }
639}
640
641#[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 #[must_use]
658 pub const fn canister_version(self) -> u64 {
659 self.canister_version
660 }
661
662 #[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}