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)]
48pub enum DeclarationLifetime {
49 Retained,
51 RemoveWhenStopped,
53}
54
55#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub enum TimerSchedulingMode {
61 Once,
63 AfterCompletion,
65 Deadline,
67 Retry,
69 Continuation,
71 Watchdog,
73}
74
75impl TimerSchedulingMode {
76 #[must_use]
78 pub const fn label(self) -> &'static str {
79 match self {
80 Self::Once => "once",
81 Self::AfterCompletion => "after_completion",
82 Self::Deadline => "deadline",
83 Self::Retry => "retry",
84 Self::Continuation => "continuation",
85 Self::Watchdog => "watchdog",
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub enum TimerDirectiveSnapshot {
94 Stop,
96 ContinueImmediately,
98 RetryAfter {
100 delay_ns: u64,
102 },
103 ScheduleAt {
105 deadline_ns: u64,
107 },
108 RecurAfterCompletion,
110}
111
112impl TryFrom<OrdinaryDirective> for TimerDirectiveSnapshot {
113 type Error = ScheduleError;
114
115 fn try_from(value: OrdinaryDirective) -> Result<Self, Self::Error> {
116 Ok(match value {
117 OrdinaryDirective::Stop => Self::Stop,
118 OrdinaryDirective::ContinueImmediately => Self::ContinueImmediately,
119 OrdinaryDirective::RetryAfter(delay) => Self::RetryAfter {
120 delay_ns: duration_ns(delay)?,
121 },
122 OrdinaryDirective::ScheduleAt(deadline_ns) => Self::ScheduleAt { deadline_ns },
123 OrdinaryDirective::RecurAfterCompletion => Self::RecurAfterCompletion,
124 })
125 }
126}
127
128impl TryFrom<OnceDecision> for TimerDirectiveSnapshot {
129 type Error = ScheduleError;
130
131 fn try_from(value: OnceDecision) -> Result<Self, Self::Error> {
132 Self::try_from(OrdinaryDirective::from(value))
133 }
134}
135
136impl TryFrom<AfterCompletionDecision> for TimerDirectiveSnapshot {
137 type Error = ScheduleError;
138
139 fn try_from(value: AfterCompletionDecision) -> Result<Self, Self::Error> {
140 Self::try_from(OrdinaryDirective::from(value))
141 }
142}
143
144#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
146pub enum TimerControlFailure {
147 GenerationExhausted,
150 DeadlineOverflow,
152 DelayOutOfRange,
154 DirectiveNotAllowed,
157 ProviderBindingFailed,
159}
160
161impl TimerControlFailure {
162 #[must_use]
164 pub const fn label(self) -> &'static str {
165 match self {
166 Self::GenerationExhausted => "generation_exhausted",
167 Self::DeadlineOverflow => "deadline_overflow",
168 Self::DelayOutOfRange => "delay_out_of_range",
169 Self::DirectiveNotAllowed => "directive_not_allowed",
170 Self::ProviderBindingFailed => "provider_binding_failed",
171 }
172 }
173}
174
175#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
177pub enum InactiveReason {
178 NeverScheduled,
180 Stopped,
182 Cancelled,
184 Abandoned,
187 InvariantFailure,
189 ControlFailure(TimerControlFailure),
191}
192
193#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
195pub enum OrdinaryRuntimeStateSnapshot {
196 Scheduled {
198 generation: u64,
200 deadline_ns: u64,
202 },
203 Running {
205 generation: u64,
207 },
208}
209
210#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
212pub enum WatchdogAttemptStatus {
213 Dispatched,
215 Running,
217}
218
219#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
221pub enum WatchdogRuntimeStateSnapshot {
222 Scheduled {
224 scheduler_generation: u64,
226 deadline_ns: u64,
228 },
229 AwaitingWork {
231 successor_generation: u64,
234 successor_deadline_ns: u64,
236 attempt_status: WatchdogAttemptStatus,
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_status: WatchdogAttemptStatus::Dispatched,
310 ..
311 } => Self::Scheduled,
312 WatchdogRuntimeStateSnapshot::AwaitingWork {
313 attempt_status: WatchdogAttemptStatus::Running,
314 ..
315 } => Self::Running,
316 },
317 }
318 }
319}
320
321#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
323pub enum TimerProcessCondition {
324 Disabled,
326 Idle,
328 Active,
330 Retrying,
332 Failed,
334}
335
336impl TimerProcessCondition {
337 #[must_use]
339 pub const fn label(self) -> &'static str {
340 match self {
341 Self::Disabled => "disabled",
342 Self::Idle => "idle",
343 Self::Active => "active",
344 Self::Retrying => "retrying",
345 Self::Failed => "failed",
346 }
347 }
348}
349
350#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
352pub enum TimerCompletionOutcome {
353 Success,
355 NoWork,
357 RetryableFailure,
359 InvariantFailure,
361}
362
363impl TimerCompletionOutcome {
364 #[must_use]
366 pub const fn label(self) -> &'static str {
367 match self {
368 Self::Success => "success",
369 Self::NoWork => "no_work",
370 Self::RetryableFailure => "retryable_failure",
371 Self::InvariantFailure => "invariant_failure",
372 }
373 }
374}
375
376#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
378pub enum TimerLastOutcome {
379 Completed(TimerCompletionOutcome),
381 Unacknowledged,
384}
385
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
388pub struct TimerCompletion {
389 outcome: TimerCompletionOutcome,
390 work_count: u64,
391}
392
393impl TimerCompletion {
394 #[must_use]
396 pub const fn success(work_count: u64) -> Self {
397 Self {
398 outcome: TimerCompletionOutcome::Success,
399 work_count,
400 }
401 }
402
403 #[must_use]
405 pub const fn no_work() -> Self {
406 Self {
407 outcome: TimerCompletionOutcome::NoWork,
408 work_count: 0,
409 }
410 }
411
412 #[must_use]
414 pub const fn retryable_failure(work_count: u64) -> Self {
415 Self {
416 outcome: TimerCompletionOutcome::RetryableFailure,
417 work_count,
418 }
419 }
420
421 #[must_use]
423 pub const fn invariant_failure(work_count: u64) -> Self {
424 Self {
425 outcome: TimerCompletionOutcome::InvariantFailure,
426 work_count,
427 }
428 }
429
430 #[must_use]
432 pub const fn outcome(self) -> TimerCompletionOutcome {
433 self.outcome
434 }
435
436 #[must_use]
438 pub const fn work_count(self) -> u64 {
439 self.work_count
440 }
441}
442
443#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
445pub enum WatchdogDecision {
446 Continue,
448 ContinueImmediately,
453 ScheduleAt(u64),
458 Stop,
460}
461
462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
464pub struct WatchdogRunResult {
465 completion: TimerCompletion,
466 decision: WatchdogDecision,
467}
468
469impl WatchdogRunResult {
470 #[must_use]
472 pub const fn new(completion: TimerCompletion, decision: WatchdogDecision) -> Self {
473 Self {
474 decision: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
475 WatchdogDecision::Stop
476 } else {
477 decision
478 },
479 completion,
480 }
481 }
482
483 #[must_use]
485 pub const fn completion(self) -> TimerCompletion {
486 self.completion
487 }
488
489 #[must_use]
491 pub const fn decision(self) -> WatchdogDecision {
492 self.decision
493 }
494}
495
496#[derive(Clone, Copy, Debug, Eq, PartialEq)]
498enum RecordedOutcome {
499 Completed(TimerCompletion),
500 Unacknowledged,
501}
502
503#[derive(Clone, Copy, Debug, Eq, PartialEq)]
505pub struct TimerOutcomeSnapshot {
506 last_event: Option<RecordedOutcome>,
507 last_success_at_ns: Option<u64>,
508 last_failure_at_ns: Option<u64>,
509 last_unacknowledged_at_ns: Option<u64>,
510 consecutive_expected_failures: u64,
511}
512
513impl TimerOutcomeSnapshot {
514 pub(crate) const EMPTY: Self = Self {
515 last_event: None,
516 last_success_at_ns: None,
517 last_failure_at_ns: None,
518 last_unacknowledged_at_ns: None,
519 consecutive_expected_failures: 0,
520 };
521
522 pub(crate) const fn record_completion(
523 &mut self,
524 completion: TimerCompletion,
525 completed_at_ns: u64,
526 ) {
527 self.last_event = Some(RecordedOutcome::Completed(completion));
528 match completion.outcome {
529 TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
530 self.last_success_at_ns = Some(completed_at_ns);
531 self.consecutive_expected_failures = 0;
532 }
533 TimerCompletionOutcome::RetryableFailure => {
534 self.last_failure_at_ns = Some(completed_at_ns);
535 self.consecutive_expected_failures =
536 self.consecutive_expected_failures.saturating_add(1);
537 }
538 TimerCompletionOutcome::InvariantFailure => {
539 self.last_failure_at_ns = Some(completed_at_ns);
540 self.consecutive_expected_failures = 0;
541 }
542 }
543 }
544
545 pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
546 self.last_event = Some(RecordedOutcome::Unacknowledged);
547 self.last_unacknowledged_at_ns = Some(observed_at_ns);
548 }
549
550 #[must_use]
552 pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
553 match self.last_event {
554 Some(RecordedOutcome::Completed(completion)) => {
555 Some(TimerLastOutcome::Completed(completion.outcome()))
556 }
557 Some(RecordedOutcome::Unacknowledged) => Some(TimerLastOutcome::Unacknowledged),
558 None => None,
559 }
560 }
561
562 #[must_use]
564 pub const fn last_work_count(self) -> Option<u64> {
565 match self.last_event {
566 Some(RecordedOutcome::Completed(completion)) => Some(completion.work_count()),
567 Some(RecordedOutcome::Unacknowledged) | None => None,
568 }
569 }
570
571 #[must_use]
573 pub const fn last_success_at_ns(self) -> Option<u64> {
574 self.last_success_at_ns
575 }
576
577 #[must_use]
579 pub const fn last_failure_at_ns(self) -> Option<u64> {
580 self.last_failure_at_ns
581 }
582
583 #[must_use]
586 pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
587 self.last_unacknowledged_at_ns
588 }
589
590 #[must_use]
592 pub const fn consecutive_expected_failures(self) -> u64 {
593 self.consecutive_expected_failures
594 }
595}
596
597#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
599pub struct TimerEpoch {
600 canister_version: u64,
601 started_at_ns: u64,
602}
603
604impl TimerEpoch {
605 pub(crate) const fn new(canister_version: u64, started_at_ns: u64) -> Self {
606 Self {
607 canister_version,
608 started_at_ns,
609 }
610 }
611
612 #[must_use]
614 pub const fn canister_version(self) -> u64 {
615 self.canister_version
616 }
617
618 #[must_use]
620 pub const fn started_at_ns(self) -> u64 {
621 self.started_at_ns
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn latest_outcome_keeps_work_count_and_independent_history_coherent() {
631 let mut outcomes = TimerOutcomeSnapshot::EMPTY;
632 assert_eq!(outcomes.last_outcome(), None);
633 assert_eq!(outcomes.last_work_count(), None);
634
635 for (completion, completed_at_ns) in [
636 (TimerCompletion::success(0), 10),
637 (TimerCompletion::no_work(), 11),
638 (TimerCompletion::retryable_failure(7), 12),
639 ] {
640 outcomes.record_completion(completion, completed_at_ns);
641 assert_eq!(
642 outcomes.last_outcome(),
643 Some(TimerLastOutcome::Completed(completion.outcome()))
644 );
645 assert_eq!(outcomes.last_work_count(), Some(completion.work_count()));
646 }
647 outcomes.record_unacknowledged(20);
648 assert_eq!(
649 outcomes.last_outcome(),
650 Some(TimerLastOutcome::Unacknowledged)
651 );
652 assert_eq!(outcomes.last_work_count(), None);
653 assert_eq!(outcomes.last_success_at_ns(), Some(11));
654 assert_eq!(outcomes.last_failure_at_ns(), Some(12));
655 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
656 assert_eq!(outcomes.consecutive_expected_failures(), 1);
657
658 outcomes.record_completion(TimerCompletion::invariant_failure(2), 30);
659 assert_eq!(
660 outcomes.last_outcome(),
661 Some(TimerLastOutcome::Completed(
662 TimerCompletionOutcome::InvariantFailure
663 ))
664 );
665 assert_eq!(outcomes.last_work_count(), Some(2));
666 assert_eq!(outcomes.last_success_at_ns(), Some(11));
667 assert_eq!(outcomes.last_failure_at_ns(), Some(30));
668 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
669 assert_eq!(outcomes.consecutive_expected_failures(), 0);
670
671 outcomes.record_completion(TimerCompletion::success(4), 40);
672 assert_eq!(
673 outcomes.last_outcome(),
674 Some(TimerLastOutcome::Completed(TimerCompletionOutcome::Success))
675 );
676 assert_eq!(outcomes.last_work_count(), Some(4));
677 assert_eq!(outcomes.last_success_at_ns(), Some(40));
678 assert_eq!(outcomes.last_failure_at_ns(), Some(30));
679 assert_eq!(outcomes.last_unacknowledged_at_ns(), Some(20));
680 }
681
682 #[test]
683 fn expected_failure_streak_saturates() {
684 let mut outcomes = TimerOutcomeSnapshot {
685 consecutive_expected_failures: u64::MAX,
686 ..TimerOutcomeSnapshot::EMPTY
687 };
688
689 outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
690
691 assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
692 }
693
694 #[test]
695 fn invariant_results_are_forced_to_stop() {
696 let watchdog = WatchdogRunResult::new(
697 TimerCompletion::invariant_failure(3),
698 WatchdogDecision::ContinueImmediately,
699 );
700 assert_eq!(watchdog.decision(), WatchdogDecision::Stop);
701 }
702}