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)]
89pub enum TimerDirectiveSnapshot {
90 Stop,
92 ContinueImmediately,
94 RetryAfter {
96 delay_ns: u64,
98 },
99 ScheduleAt {
101 deadline_ns: u64,
103 },
104 RecurAfterCompletion,
106}
107
108impl TimerDirectiveSnapshot {
109 pub(crate) const fn scheduling_mode(self) -> Option<TimerSchedulingMode> {
110 match self {
111 Self::Stop => None,
112 Self::ContinueImmediately => Some(TimerSchedulingMode::Continuation),
113 Self::RetryAfter { .. } => Some(TimerSchedulingMode::Retry),
114 Self::ScheduleAt { .. } => Some(TimerSchedulingMode::Deadline),
115 Self::RecurAfterCompletion => Some(TimerSchedulingMode::AfterCompletion),
116 }
117 }
118}
119
120impl TryFrom<TimerDirective> for TimerDirectiveSnapshot {
121 type Error = ScheduleError;
122
123 fn try_from(value: TimerDirective) -> Result<Self, Self::Error> {
124 Ok(match value {
125 TimerDirective::Stop => Self::Stop,
126 TimerDirective::ContinueImmediately => Self::ContinueImmediately,
127 TimerDirective::RetryAfter(delay) => Self::RetryAfter {
128 delay_ns: duration_ns(delay)?,
129 },
130 TimerDirective::ScheduleAt(deadline_ns) => Self::ScheduleAt { deadline_ns },
131 TimerDirective::RecurAfterCompletion => Self::RecurAfterCompletion,
132 })
133 }
134}
135
136#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138pub enum TimerControlFailure {
139 GenerationExhausted,
141 DeadlineOverflow,
143 DelayOutOfRange,
145 DirectiveNotAllowed,
147 ProviderBindingFailed,
149}
150
151impl TimerControlFailure {
152 #[must_use]
154 pub const fn label(self) -> &'static str {
155 match self {
156 Self::GenerationExhausted => "generation_exhausted",
157 Self::DeadlineOverflow => "deadline_overflow",
158 Self::DelayOutOfRange => "delay_out_of_range",
159 Self::DirectiveNotAllowed => "directive_not_allowed",
160 Self::ProviderBindingFailed => "provider_binding_failed",
161 }
162 }
163}
164
165#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub enum InactiveReason {
168 NeverScheduled,
170 Stopped,
172 Cancelled,
174 InvariantFailure,
176 ControlFailure(TimerControlFailure),
178}
179
180#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
182pub enum OrdinaryRuntimeStateSnapshot {
183 Scheduled {
185 generation: u64,
187 deadline_ns: u64,
189 },
190 Running {
192 generation: u64,
194 },
195}
196
197#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
199pub enum WatchdogAttemptStatus {
200 Dispatched,
202 Running,
204}
205
206#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
208pub struct WatchdogAttemptSnapshot {
209 generation: u64,
210 status: WatchdogAttemptStatus,
211}
212
213impl WatchdogAttemptSnapshot {
214 pub(crate) const fn new(generation: u64, status: WatchdogAttemptStatus) -> Self {
215 Self { generation, status }
216 }
217
218 #[must_use]
220 pub const fn generation(self) -> u64 {
221 self.generation
222 }
223
224 #[must_use]
226 pub const fn status(self) -> WatchdogAttemptStatus {
227 self.status
228 }
229}
230
231#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub enum WatchdogRuntimeStateSnapshot {
234 Scheduled {
236 scheduler_generation: u64,
238 deadline_ns: u64,
240 },
241 AwaitingWork {
243 successor_generation: u64,
245 successor_deadline_ns: u64,
247 attempt: WatchdogAttemptSnapshot,
249 },
250}
251
252#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
254pub enum TimerRuntimeStateSnapshot {
255 Inactive {
257 reason: InactiveReason,
259 },
260 Ordinary(OrdinaryRuntimeStateSnapshot),
262 Watchdog(WatchdogRuntimeStateSnapshot),
264}
265
266impl TimerRuntimeStateSnapshot {
267 pub(crate) const fn next_deadline_ns(self) -> Option<u64> {
268 match self {
269 Self::Inactive { .. }
270 | Self::Ordinary(OrdinaryRuntimeStateSnapshot::Running { .. }) => None,
271 Self::Ordinary(OrdinaryRuntimeStateSnapshot::Scheduled { deadline_ns, .. })
272 | Self::Watchdog(WatchdogRuntimeStateSnapshot::Scheduled { deadline_ns, .. }) => {
273 Some(deadline_ns)
274 }
275 Self::Watchdog(WatchdogRuntimeStateSnapshot::AwaitingWork {
276 successor_deadline_ns,
277 ..
278 }) => Some(successor_deadline_ns),
279 }
280 }
281}
282
283#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
288pub enum TimerRegistrationStatus {
289 Unregistered,
291 Scheduled,
293 Running,
295}
296
297impl TimerRegistrationStatus {
298 #[must_use]
300 pub const fn label(self) -> &'static str {
301 match self {
302 Self::Unregistered => "unregistered",
303 Self::Scheduled => "scheduled",
304 Self::Running => "running",
305 }
306 }
307}
308
309impl From<TimerRuntimeStateSnapshot> for TimerRegistrationStatus {
310 fn from(value: TimerRuntimeStateSnapshot) -> Self {
311 match value {
312 TimerRuntimeStateSnapshot::Inactive { .. } => Self::Unregistered,
313 TimerRuntimeStateSnapshot::Ordinary(state) => match state {
314 OrdinaryRuntimeStateSnapshot::Scheduled { .. } => Self::Scheduled,
315 OrdinaryRuntimeStateSnapshot::Running { .. } => Self::Running,
316 },
317 TimerRuntimeStateSnapshot::Watchdog(state) => match state {
318 WatchdogRuntimeStateSnapshot::Scheduled { .. }
319 | WatchdogRuntimeStateSnapshot::AwaitingWork {
320 attempt:
321 WatchdogAttemptSnapshot {
322 status: WatchdogAttemptStatus::Dispatched,
323 ..
324 },
325 ..
326 } => Self::Scheduled,
327 WatchdogRuntimeStateSnapshot::AwaitingWork {
328 attempt:
329 WatchdogAttemptSnapshot {
330 status: WatchdogAttemptStatus::Running,
331 ..
332 },
333 ..
334 } => Self::Running,
335 },
336 }
337 }
338}
339
340#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
342pub enum TimerProcessCondition {
343 Disabled,
345 Idle,
347 Active,
349 Retrying,
351 Failed,
353}
354
355impl TimerProcessCondition {
356 #[must_use]
358 pub const fn label(self) -> &'static str {
359 match self {
360 Self::Disabled => "disabled",
361 Self::Idle => "idle",
362 Self::Active => "active",
363 Self::Retrying => "retrying",
364 Self::Failed => "failed",
365 }
366 }
367}
368
369#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
371pub enum TimerCompletionOutcome {
372 Success,
374 NoWork,
376 RetryableFailure,
378 InvariantFailure,
380}
381
382impl TimerCompletionOutcome {
383 #[must_use]
385 pub const fn label(self) -> &'static str {
386 match self {
387 Self::Success => "success",
388 Self::NoWork => "no_work",
389 Self::RetryableFailure => "retryable_failure",
390 Self::InvariantFailure => "invariant_failure",
391 }
392 }
393}
394
395#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
397pub enum TimerLastOutcome {
398 Completed(TimerCompletionOutcome),
400 Unacknowledged,
402}
403
404#[derive(Clone, Copy, Debug, Eq, PartialEq)]
406pub struct TimerCompletion {
407 outcome: TimerCompletionOutcome,
408 work_count: u64,
409}
410
411impl TimerCompletion {
412 #[must_use]
414 pub const fn success(work_count: u64) -> Self {
415 Self {
416 outcome: TimerCompletionOutcome::Success,
417 work_count,
418 }
419 }
420
421 #[must_use]
423 pub const fn no_work() -> Self {
424 Self {
425 outcome: TimerCompletionOutcome::NoWork,
426 work_count: 0,
427 }
428 }
429
430 #[must_use]
432 pub const fn retryable_failure(work_count: u64) -> Self {
433 Self {
434 outcome: TimerCompletionOutcome::RetryableFailure,
435 work_count,
436 }
437 }
438
439 #[must_use]
441 pub const fn invariant_failure(work_count: u64) -> Self {
442 Self {
443 outcome: TimerCompletionOutcome::InvariantFailure,
444 work_count,
445 }
446 }
447
448 #[must_use]
450 pub const fn outcome(self) -> TimerCompletionOutcome {
451 self.outcome
452 }
453
454 #[must_use]
456 pub const fn work_count(self) -> u64 {
457 self.work_count
458 }
459}
460
461#[derive(Clone, Copy, Debug, Eq, PartialEq)]
466pub struct TimerRunResult {
467 completion: TimerCompletion,
468 directive: TimerDirective,
469}
470
471impl TimerRunResult {
472 #[must_use]
474 pub const fn new(completion: TimerCompletion, directive: TimerDirective) -> Self {
475 Self {
476 directive: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
477 TimerDirective::Stop
478 } else {
479 directive
480 },
481 completion,
482 }
483 }
484
485 #[must_use]
487 pub const fn completion(self) -> TimerCompletion {
488 self.completion
489 }
490
491 #[must_use]
493 pub const fn directive(self) -> TimerDirective {
494 self.directive
495 }
496}
497
498#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
500pub enum WatchdogDecision {
501 Continue,
503 ContinueImmediately,
508 ScheduleAt(u64),
513 Stop,
515}
516
517#[derive(Clone, Copy, Debug, Eq, PartialEq)]
519pub struct WatchdogRunResult {
520 completion: TimerCompletion,
521 decision: WatchdogDecision,
522}
523
524impl WatchdogRunResult {
525 #[must_use]
527 pub const fn new(completion: TimerCompletion, decision: WatchdogDecision) -> Self {
528 Self {
529 decision: if matches!(completion.outcome, TimerCompletionOutcome::InvariantFailure) {
530 WatchdogDecision::Stop
531 } else {
532 decision
533 },
534 completion,
535 }
536 }
537
538 #[must_use]
540 pub const fn completion(self) -> TimerCompletion {
541 self.completion
542 }
543
544 #[must_use]
546 pub const fn decision(self) -> WatchdogDecision {
547 self.decision
548 }
549}
550
551#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub struct TimerOutcomeSnapshot {
554 last_outcome: Option<TimerLastOutcome>,
555 last_work_count: Option<u64>,
556 last_success_at_ns: Option<u64>,
557 last_failure_at_ns: Option<u64>,
558 last_unacknowledged_at_ns: Option<u64>,
559 consecutive_expected_failures: u64,
560}
561
562impl TimerOutcomeSnapshot {
563 pub(crate) const EMPTY: Self = Self {
564 last_outcome: None,
565 last_work_count: None,
566 last_success_at_ns: None,
567 last_failure_at_ns: None,
568 last_unacknowledged_at_ns: None,
569 consecutive_expected_failures: 0,
570 };
571
572 pub(crate) const fn record_completion(
573 &mut self,
574 completion: TimerCompletion,
575 completed_at_ns: u64,
576 ) {
577 self.last_outcome = Some(TimerLastOutcome::Completed(completion.outcome));
578 self.last_work_count = Some(completion.work_count);
579 match completion.outcome {
580 TimerCompletionOutcome::Success | TimerCompletionOutcome::NoWork => {
581 self.last_success_at_ns = Some(completed_at_ns);
582 self.consecutive_expected_failures = 0;
583 }
584 TimerCompletionOutcome::RetryableFailure => {
585 self.last_failure_at_ns = Some(completed_at_ns);
586 self.consecutive_expected_failures =
587 self.consecutive_expected_failures.saturating_add(1);
588 }
589 TimerCompletionOutcome::InvariantFailure => {
590 self.last_failure_at_ns = Some(completed_at_ns);
591 self.consecutive_expected_failures = 0;
592 }
593 }
594 }
595
596 pub(crate) const fn record_unacknowledged(&mut self, observed_at_ns: u64) {
597 self.last_outcome = Some(TimerLastOutcome::Unacknowledged);
598 self.last_work_count = None;
599 self.last_unacknowledged_at_ns = Some(observed_at_ns);
600 }
601
602 #[must_use]
604 pub const fn last_outcome(self) -> Option<TimerLastOutcome> {
605 self.last_outcome
606 }
607
608 #[must_use]
610 pub const fn last_work_count(self) -> Option<u64> {
611 self.last_work_count
612 }
613
614 #[must_use]
616 pub const fn last_success_at_ns(self) -> Option<u64> {
617 self.last_success_at_ns
618 }
619
620 #[must_use]
622 pub const fn last_failure_at_ns(self) -> Option<u64> {
623 self.last_failure_at_ns
624 }
625
626 #[must_use]
628 pub const fn last_unacknowledged_at_ns(self) -> Option<u64> {
629 self.last_unacknowledged_at_ns
630 }
631
632 #[must_use]
634 pub const fn consecutive_expected_failures(self) -> u64 {
635 self.consecutive_expected_failures
636 }
637}
638
639#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
641pub struct TimerEpoch {
642 canister_version: u64,
643 started_at_ns: u64,
644}
645
646impl TimerEpoch {
647 pub(crate) const fn new(canister_version: u64, started_at_ns: u64) -> Self {
648 Self {
649 canister_version,
650 started_at_ns,
651 }
652 }
653
654 #[must_use]
656 pub const fn canister_version(self) -> u64 {
657 self.canister_version
658 }
659
660 #[must_use]
662 pub const fn started_at_ns(self) -> u64 {
663 self.started_at_ns
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn expected_failure_streak_saturates() {
673 let mut outcomes = TimerOutcomeSnapshot {
674 consecutive_expected_failures: u64::MAX,
675 ..TimerOutcomeSnapshot::EMPTY
676 };
677
678 outcomes.record_completion(TimerCompletion::retryable_failure(0), 10);
679
680 assert_eq!(outcomes.consecutive_expected_failures(), u64::MAX);
681 }
682
683 #[test]
684 fn invariant_results_are_forced_to_stop() {
685 let ordinary = TimerRunResult::new(
686 TimerCompletion::invariant_failure(2),
687 TimerDirective::ContinueImmediately,
688 );
689 assert_eq!(ordinary.directive(), TimerDirective::Stop);
690
691 let watchdog = WatchdogRunResult::new(
692 TimerCompletion::invariant_failure(3),
693 WatchdogDecision::ContinueImmediately,
694 );
695 assert_eq!(watchdog.decision(), WatchdogDecision::Stop);
696 }
697}