Skip to main content

ic_timers/registry/
mod.rs

1//! Pure bounded registry and policy-specific transition engine.
2//!
3//! It emits provider-neutral effects; the runtime binds those effects to
4//! linear `ic-cdk-timers` handles without replacing this ownership or state
5//! model.
6
7use crate::{
8    DeclarationLifetime, InactiveReason, OrdinaryRuntimeStateSnapshot, ScheduleError, TimerCadence,
9    TimerCompletion, TimerCompletionOutcome, TimerControl, TimerControlAction, TimerControlError,
10    TimerControlFailure, TimerDirective, TimerDirectiveSnapshot, TimerEpoch, TimerIdentity,
11    TimerObservabilitySnapshot, TimerPolicy, TimerRunResult, TimerRuntimeStateSnapshot,
12    TimerSchedule, TimerSchedulingMode, TimerSnapshot, WatchdogAttemptSnapshot,
13    WatchdogAttemptStatus, WatchdogDecision, WatchdogRunResult, WatchdogRuntimeStateSnapshot,
14    platform::TimerHandle,
15    runtime::{TimerContext, TimerFuture},
16};
17use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
18use thiserror::Error;
19
20/// Maximum declarations owned by one canonical registry.
21pub const MAX_TIMER_REGISTRATIONS: usize = 64;
22
23/// Opaque logical ownership claim returned by pure registration.
24#[derive(Debug, Eq, PartialEq)]
25pub struct RegistrationClaim {
26    identity: TimerIdentity,
27    claim_generation: u64,
28}
29
30impl RegistrationClaim {
31    pub(crate) const fn identity(&self) -> &TimerIdentity {
32        &self.identity
33    }
34
35    pub(crate) const fn claim_generation(&self) -> u64 {
36        self.claim_generation
37    }
38
39    pub(crate) const fn delegated(identity: TimerIdentity, claim_generation: u64) -> Self {
40        Self {
41            identity,
42            claim_generation,
43        }
44    }
45}
46
47/// Internal callback role carried by a generation-checked dispatch token.
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum CallbackRole {
50    OrdinaryWork,
51    WatchdogScheduler,
52    WatchdogWork,
53}
54
55/// Identity and generations an internal callback must present.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct CallbackToken {
58    identity: TimerIdentity,
59    claim_generation: u64,
60    callback_generation: u64,
61    role: CallbackRole,
62}
63
64impl CallbackToken {
65    const fn new(
66        identity: TimerIdentity,
67        claim_generation: u64,
68        callback_generation: u64,
69        role: CallbackRole,
70    ) -> Self {
71        Self {
72            identity,
73            claim_generation,
74            callback_generation,
75            role,
76        }
77    }
78
79    pub(crate) const fn identity(&self) -> &TimerIdentity {
80        &self.identity
81    }
82
83    #[cfg(test)]
84    pub(crate) const fn callback_generation(&self) -> u64 {
85        self.callback_generation
86    }
87
88    pub(crate) const fn claim_generation(&self) -> u64 {
89        self.claim_generation
90    }
91
92    pub(crate) const fn role(&self) -> CallbackRole {
93        self.role
94    }
95}
96
97/// Provider-neutral effect emitted by one pure transition.
98#[derive(Debug, Eq, PartialEq)]
99pub enum RegistryEffect {
100    None,
101    ArmWakeup {
102        token: CallbackToken,
103        deadline_ns: u64,
104        delay_ns: u64,
105        replace: bool,
106    },
107    ClearCallbacks {
108        identity: TimerIdentity,
109        clear_wakeup: bool,
110        clear_work: bool,
111    },
112    DispatchWatchdog {
113        successor: CallbackToken,
114        successor_deadline_ns: u64,
115        successor_delay_ns: u64,
116        work: CallbackToken,
117    },
118}
119
120/// One pure transition and any terminal checked-control failure it produced.
121#[derive(Debug, Eq, PartialEq)]
122pub struct RegistryTransition {
123    effect: RegistryEffect,
124    failure: Option<TimerControlFailure>,
125}
126
127impl RegistryTransition {
128    const fn normal(effect: RegistryEffect) -> Self {
129        Self {
130            effect,
131            failure: None,
132        }
133    }
134
135    const fn terminal(effect: RegistryEffect, failure: TimerControlFailure) -> Self {
136        Self {
137            effect,
138            failure: Some(failure),
139        }
140    }
141
142    pub(crate) const fn effect(&self) -> &RegistryEffect {
143        &self.effect
144    }
145
146    pub(crate) const fn failure(&self) -> Option<TimerControlFailure> {
147        self.failure
148    }
149
150    pub(crate) fn into_effect(self) -> RegistryEffect {
151        self.effect
152    }
153}
154
155/// Whether an internal callback generation won arbitration.
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
157pub enum CallbackAcceptance {
158    Accepted,
159    Stale,
160}
161
162/// Failure to claim one canonical timer identity.
163#[non_exhaustive]
164#[derive(Clone, Debug, Eq, Error, PartialEq)]
165pub enum RegisterError {
166    /// The identity already has one canonical claimant.
167    #[error("timer identity is already registered: {0:?}")]
168    IdentityAlreadyRegistered(TimerIdentity),
169    /// The fixed registry capacity has been reached.
170    #[error("timer registry capacity of {max} declarations has been reached")]
171    CapacityExceeded {
172        /// Fixed registry capacity.
173        max: usize,
174    },
175    /// Logical registration claim generations are exhausted.
176    #[error("timer registration claim generation exhausted")]
177    ClaimGenerationExhausted,
178}
179
180/// Invalid request against a logical registration claim.
181#[non_exhaustive]
182#[derive(Clone, Debug, Eq, Error, PartialEq)]
183pub enum RegistryError {
184    #[error("timer registration is no longer present")]
185    UnknownRegistration,
186    #[error("timer registration claim is stale")]
187    StaleRegistration,
188    #[error("timer operation is not legal for policy {actual}")]
189    WrongPolicy { actual: &'static str },
190    #[error("timer callback token is stale")]
191    StaleCallback,
192    #[error("timer registration has no ordinary callback")]
193    MissingCallback,
194    #[error("timer registration already owns the provider handle for this callback role")]
195    ProviderHandleAlreadyOwned,
196    #[error(transparent)]
197    Schedule(#[from] ScheduleError),
198}
199
200pub type OrdinaryCallback = Rc<RefCell<Box<dyn FnMut(TimerContext) -> TimerFuture>>>;
201pub type WatchdogCallback = Rc<RefCell<Box<dyn FnMut(TimerContext) -> WatchdogRunResult>>>;
202
203enum EntryCallback {
204    #[cfg(test)]
205    None,
206    Ordinary(OrdinaryCallback),
207    Watchdog(WatchdogCallback),
208}
209
210struct OwnedProviderHandle {
211    callback_generation: u64,
212    role: CallbackRole,
213    handle: TimerHandle,
214}
215
216/// One temporarily detached exact provider capability.
217pub struct ProviderHandle {
218    token: CallbackToken,
219    handle: TimerHandle,
220}
221
222impl ProviderHandle {
223    pub fn into_parts(self) -> (CallbackToken, TimerHandle) {
224        (self.token, self.handle)
225    }
226}
227
228/// The at-most-two provider capabilities owned by one timer entry.
229#[derive(Default)]
230pub struct ProviderHandles {
231    wakeup: Option<ProviderHandle>,
232    work: Option<ProviderHandle>,
233}
234
235impl ProviderHandles {
236    pub const fn from_parts(wakeup: Option<ProviderHandle>, work: Option<ProviderHandle>) -> Self {
237        Self { wakeup, work }
238    }
239
240    pub const fn take_wakeup(&mut self) -> Option<ProviderHandle> {
241        self.wakeup.take()
242    }
243
244    pub const fn take_work(&mut self) -> Option<ProviderHandle> {
245        self.work.take()
246    }
247}
248
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
250struct PendingSchedule {
251    deadline_ns: u64,
252    requested_delay_ns: Option<u64>,
253    mode: TimerSchedulingMode,
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257enum OrdinaryPending {
258    Cancel,
259    Reconcile(PendingSchedule),
260    Unregister,
261    Schedule(PendingSchedule),
262}
263
264#[derive(Clone, Copy, Debug, Eq, PartialEq)]
265enum OrdinaryRequest {
266    Ensure,
267    Reconcile,
268}
269
270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
271enum WatchdogPending {
272    Cancel,
273    Ensure,
274    Unregister,
275}
276
277#[derive(Debug)]
278enum EntryControl {
279    Ordinary {
280        control: TimerControl,
281        pending: Option<OrdinaryPending>,
282        inactive_reason: InactiveReason,
283    },
284    Watchdog(WatchdogControl),
285}
286
287#[derive(Clone, Copy, Debug, Eq, PartialEq)]
288enum WatchdogState {
289    Inactive,
290    Scheduled {
291        scheduler_generation: u64,
292        deadline_ns: u64,
293    },
294    AwaitingWork {
295        successor_generation: u64,
296        successor_deadline_ns: u64,
297        attempt_generation: u64,
298        attempt_status: WatchdogAttemptStatus,
299    },
300}
301
302#[derive(Debug)]
303struct WatchdogControl {
304    scheduler_generation: u64,
305    attempt_generation: u64,
306    request_sequence: u64,
307    state: WatchdogState,
308    pending: Option<WatchdogPending>,
309    inactive_reason: InactiveReason,
310}
311
312impl Default for WatchdogControl {
313    fn default() -> Self {
314        Self {
315            scheduler_generation: 0,
316            attempt_generation: 0,
317            request_sequence: 0,
318            state: WatchdogState::Inactive,
319            pending: None,
320            inactive_reason: InactiveReason::NeverScheduled,
321        }
322    }
323}
324
325struct Entry {
326    claim_generation: u64,
327    policy: TimerPolicy,
328    lifetime: DeclarationLifetime,
329    control: EntryControl,
330    scheduling_mode: TimerSchedulingMode,
331    latest_directive: Option<TimerDirectiveSnapshot>,
332    latest_requested_delay_ns: Option<u64>,
333    latest_armed_delay_ns: Option<u64>,
334    confirmed_wakeup_generation: Option<u64>,
335    confirmed_work_generation: Option<u64>,
336    callback: EntryCallback,
337    wakeup: Option<OwnedProviderHandle>,
338    work: Option<OwnedProviderHandle>,
339    observability: TimerObservabilitySnapshot,
340}
341
342impl Entry {
343    fn new(
344        claim_generation: u64,
345        policy: TimerPolicy,
346        lifetime: DeclarationLifetime,
347        epoch: TimerEpoch,
348        callback: EntryCallback,
349    ) -> Self {
350        let (control, scheduling_mode) = match policy {
351            TimerPolicy::Once => (
352                EntryControl::Ordinary {
353                    control: TimerControl::default(),
354                    pending: None,
355                    inactive_reason: InactiveReason::NeverScheduled,
356                },
357                TimerSchedulingMode::Once,
358            ),
359            TimerPolicy::AfterCompletion { .. } => (
360                EntryControl::Ordinary {
361                    control: TimerControl::default(),
362                    pending: None,
363                    inactive_reason: InactiveReason::NeverScheduled,
364                },
365                TimerSchedulingMode::AfterCompletion,
366            ),
367            TimerPolicy::Watchdog { .. } => (
368                EntryControl::Watchdog(WatchdogControl::default()),
369                TimerSchedulingMode::Watchdog,
370            ),
371        };
372
373        Self {
374            claim_generation,
375            policy,
376            lifetime,
377            control,
378            scheduling_mode,
379            latest_directive: None,
380            latest_requested_delay_ns: None,
381            latest_armed_delay_ns: None,
382            confirmed_wakeup_generation: None,
383            confirmed_work_generation: None,
384            callback,
385            wakeup: None,
386            work: None,
387            observability: TimerObservabilitySnapshot::new(epoch),
388        }
389    }
390
391    const fn snapshot(&self, identity: TimerIdentity) -> TimerSnapshot {
392        let state = match &self.control {
393            EntryControl::Ordinary {
394                control,
395                inactive_reason,
396                ..
397            } => match control.registration() {
398                crate::TimerRegistration::Unregistered => TimerRuntimeStateSnapshot::Inactive {
399                    reason: *inactive_reason,
400                },
401                crate::TimerRegistration::Scheduled {
402                    generation,
403                    deadline_ns,
404                } => TimerRuntimeStateSnapshot::Ordinary(OrdinaryRuntimeStateSnapshot::Scheduled {
405                    generation,
406                    deadline_ns,
407                }),
408                crate::TimerRegistration::Running { generation } => {
409                    TimerRuntimeStateSnapshot::Ordinary(OrdinaryRuntimeStateSnapshot::Running {
410                        generation,
411                    })
412                }
413            },
414            EntryControl::Watchdog(control) => match control.state {
415                WatchdogState::Inactive => TimerRuntimeStateSnapshot::Inactive {
416                    reason: control.inactive_reason,
417                },
418                WatchdogState::Scheduled {
419                    scheduler_generation,
420                    deadline_ns,
421                } => TimerRuntimeStateSnapshot::Watchdog(WatchdogRuntimeStateSnapshot::Scheduled {
422                    scheduler_generation,
423                    deadline_ns,
424                }),
425                WatchdogState::AwaitingWork {
426                    successor_generation,
427                    successor_deadline_ns,
428                    attempt_generation,
429                    attempt_status,
430                } => TimerRuntimeStateSnapshot::Watchdog(
431                    WatchdogRuntimeStateSnapshot::AwaitingWork {
432                        successor_generation,
433                        successor_deadline_ns,
434                        attempt: WatchdogAttemptSnapshot::new(attempt_generation, attempt_status),
435                    },
436                ),
437            },
438        };
439
440        TimerSnapshot::new(
441            identity,
442            self.policy,
443            self.lifetime,
444            state,
445            self.scheduling_mode,
446            self.latest_directive,
447            self.latest_requested_delay_ns,
448            self.latest_armed_delay_ns,
449            &self.observability,
450        )
451    }
452}
453
454/// Pure, fixed-capacity canonical registry.
455pub struct TimerRegistry {
456    epoch: TimerEpoch,
457    next_claim_generation: u64,
458    entries: BTreeMap<TimerIdentity, Entry>,
459}
460
461impl TimerRegistry {
462    pub(crate) const fn new(epoch: TimerEpoch) -> Self {
463        Self {
464            epoch,
465            next_claim_generation: 0,
466            entries: BTreeMap::new(),
467        }
468    }
469
470    #[cfg(test)]
471    pub(crate) fn len(&self) -> usize {
472        self.entries.len()
473    }
474
475    #[cfg(test)]
476    pub(crate) fn is_empty(&self) -> bool {
477        self.entries.is_empty()
478    }
479
480    pub(crate) const fn epoch(&self) -> TimerEpoch {
481        self.epoch
482    }
483
484    #[cfg(test)]
485    pub(crate) fn register_once(
486        &mut self,
487        identity: TimerIdentity,
488        lifetime: DeclarationLifetime,
489    ) -> Result<RegistrationClaim, RegisterError> {
490        self.register(identity, TimerPolicy::Once, lifetime, EntryCallback::None)
491    }
492
493    #[cfg(test)]
494    pub(crate) fn register_after_completion(
495        &mut self,
496        identity: TimerIdentity,
497        cadence: TimerCadence,
498        lifetime: DeclarationLifetime,
499    ) -> Result<RegistrationClaim, RegisterError> {
500        self.register(
501            identity,
502            TimerPolicy::AfterCompletion { cadence },
503            lifetime,
504            EntryCallback::None,
505        )
506    }
507
508    #[cfg(test)]
509    pub(crate) fn register_watchdog(
510        &mut self,
511        identity: TimerIdentity,
512        cadence: TimerCadence,
513        lifetime: DeclarationLifetime,
514    ) -> Result<RegistrationClaim, RegisterError> {
515        self.register(
516            identity,
517            TimerPolicy::Watchdog { cadence },
518            lifetime,
519            EntryCallback::None,
520        )
521    }
522
523    pub(crate) fn register_once_with_callback(
524        &mut self,
525        identity: TimerIdentity,
526        lifetime: DeclarationLifetime,
527        callback: OrdinaryCallback,
528    ) -> Result<RegistrationClaim, RegisterError> {
529        self.register(
530            identity,
531            TimerPolicy::Once,
532            lifetime,
533            EntryCallback::Ordinary(callback),
534        )
535    }
536
537    pub(crate) fn register_after_completion_with_callback(
538        &mut self,
539        identity: TimerIdentity,
540        cadence: TimerCadence,
541        lifetime: DeclarationLifetime,
542        callback: OrdinaryCallback,
543    ) -> Result<RegistrationClaim, RegisterError> {
544        self.register(
545            identity,
546            TimerPolicy::AfterCompletion { cadence },
547            lifetime,
548            EntryCallback::Ordinary(callback),
549        )
550    }
551
552    pub(crate) fn register_watchdog_with_callback(
553        &mut self,
554        identity: TimerIdentity,
555        cadence: TimerCadence,
556        lifetime: DeclarationLifetime,
557        callback: WatchdogCallback,
558    ) -> Result<RegistrationClaim, RegisterError> {
559        self.register(
560            identity,
561            TimerPolicy::Watchdog { cadence },
562            lifetime,
563            EntryCallback::Watchdog(callback),
564        )
565    }
566
567    fn register(
568        &mut self,
569        identity: TimerIdentity,
570        policy: TimerPolicy,
571        lifetime: DeclarationLifetime,
572        callback: EntryCallback,
573    ) -> Result<RegistrationClaim, RegisterError> {
574        if self.entries.contains_key(&identity) {
575            return Err(RegisterError::IdentityAlreadyRegistered(identity));
576        }
577        if self.entries.len() == MAX_TIMER_REGISTRATIONS {
578            return Err(RegisterError::CapacityExceeded {
579                max: MAX_TIMER_REGISTRATIONS,
580            });
581        }
582        let claim_generation = self
583            .next_claim_generation
584            .checked_add(1)
585            .ok_or(RegisterError::ClaimGenerationExhausted)?;
586
587        self.next_claim_generation = claim_generation;
588        self.entries.insert(
589            identity.clone(),
590            Entry::new(claim_generation, policy, lifetime, self.epoch, callback),
591        );
592        Ok(RegistrationClaim {
593            identity,
594            claim_generation,
595        })
596    }
597
598    pub(crate) fn ensure_once(
599        &mut self,
600        claim: &RegistrationClaim,
601        now_ns: u64,
602        schedule: TimerSchedule,
603    ) -> Result<RegistryTransition, RegistryError> {
604        let resolved = schedule.resolve(now_ns)?;
605        let mode = match schedule {
606            TimerSchedule::After(_) => TimerSchedulingMode::Once,
607            TimerSchedule::At(_) => TimerSchedulingMode::Deadline,
608        };
609        self.schedule_ordinary(
610            claim,
611            now_ns,
612            PendingSchedule {
613                deadline_ns: resolved.deadline_ns,
614                requested_delay_ns: resolved.requested_delay_ns,
615                mode,
616            },
617            true,
618        )
619    }
620
621    /// Reconcile an ordinary declaration to one exact desired schedule.
622    ///
623    /// Unlike `ensure`, reconciliation may move an existing deadline later.
624    /// `None` retains callback authority while cancelling live work.
625    pub(crate) fn reconcile_ordinary(
626        &mut self,
627        claim: &RegistrationClaim,
628        now_ns: u64,
629        schedule: Option<TimerSchedule>,
630    ) -> Result<RegistryTransition, RegistryError> {
631        self.validate_ordinary_claim(claim)?;
632        let Some(schedule) = schedule else {
633            return self.cancel(claim);
634        };
635        let resolved = schedule.resolve(now_ns)?;
636        let mode = match schedule {
637            TimerSchedule::After(_) => TimerSchedulingMode::Once,
638            TimerSchedule::At(_) => TimerSchedulingMode::Deadline,
639        };
640        self.request_ordinary(
641            claim,
642            now_ns,
643            PendingSchedule {
644                deadline_ns: resolved.deadline_ns,
645                requested_delay_ns: resolved.requested_delay_ns,
646                mode,
647            },
648            false,
649            OrdinaryRequest::Reconcile,
650        )
651    }
652
653    pub(crate) fn validate_ordinary_claim(
654        &self,
655        claim: &RegistrationClaim,
656    ) -> Result<(), RegistryError> {
657        let entry = self.entry(claim)?;
658        if matches!(entry.control, EntryControl::Ordinary { .. }) {
659            Ok(())
660        } else {
661            Err(RegistryError::WrongPolicy {
662                actual: entry.policy.label(),
663            })
664        }
665    }
666
667    pub(crate) fn ensure_recurring(
668        &mut self,
669        claim: &RegistrationClaim,
670        now_ns: u64,
671    ) -> Result<RegistryTransition, RegistryError> {
672        let entry = self.entry(claim)?;
673        let (cadence, mode, ordinary, already_scheduled) = match entry.policy {
674            TimerPolicy::Once => {
675                return Err(RegistryError::WrongPolicy { actual: "once" });
676            }
677            TimerPolicy::AfterCompletion { cadence } => {
678                let already_scheduled = matches!(
679                    entry.control,
680                    EntryControl::Ordinary {
681                        ref control,
682                        ..
683                    } if matches!(
684                        control.registration(),
685                        crate::TimerRegistration::Scheduled { .. }
686                    )
687                );
688                (
689                    cadence,
690                    TimerSchedulingMode::AfterCompletion,
691                    true,
692                    already_scheduled,
693                )
694            }
695            TimerPolicy::Watchdog { cadence } => {
696                (cadence, TimerSchedulingMode::Watchdog, false, false)
697            }
698        };
699        if ordinary {
700            if already_scheduled {
701                let entry = self.entry_mut(claim)?;
702                entry.observability.counters_mut().record_schedule_request();
703                entry.observability.counters_mut().record_coalesced();
704                entry.latest_requested_delay_ns = Some(cadence.as_nanos());
705                return Ok(RegistryTransition::normal(RegistryEffect::None));
706            }
707            let deadline_ns = cadence.deadline_after(now_ns)?;
708            self.schedule_ordinary(
709                claim,
710                now_ns,
711                PendingSchedule {
712                    deadline_ns,
713                    requested_delay_ns: Some(cadence.as_nanos()),
714                    mode,
715                },
716                false,
717            )
718        } else {
719            self.ensure_watchdog(claim, now_ns, cadence)
720        }
721    }
722
723    fn schedule_ordinary(
724        &mut self,
725        claim: &RegistrationClaim,
726        now_ns: u64,
727        requested: PendingSchedule,
728        require_once: bool,
729    ) -> Result<RegistryTransition, RegistryError> {
730        self.request_ordinary(
731            claim,
732            now_ns,
733            requested,
734            require_once,
735            OrdinaryRequest::Ensure,
736        )
737    }
738
739    fn request_ordinary(
740        &mut self,
741        claim: &RegistrationClaim,
742        now_ns: u64,
743        requested: PendingSchedule,
744        require_once: bool,
745        request: OrdinaryRequest,
746    ) -> Result<RegistryTransition, RegistryError> {
747        let entry = self.entry_mut(claim)?;
748        if require_once && !matches!(entry.policy, TimerPolicy::Once) {
749            return Err(RegistryError::WrongPolicy {
750                actual: entry.policy.label(),
751            });
752        }
753        if !matches!(entry.control, EntryControl::Ordinary { .. }) {
754            return Err(RegistryError::WrongPolicy {
755                actual: entry.policy.label(),
756            });
757        }
758
759        entry.observability.counters_mut().record_schedule_request();
760        entry.latest_requested_delay_ns = requested.requested_delay_ns;
761
762        let (action, was_running) = match &mut entry.control {
763            EntryControl::Ordinary { control, .. } => {
764                let was_running = matches!(
765                    control.registration(),
766                    crate::TimerRegistration::Running { .. }
767                );
768                let action = match request {
769                    OrdinaryRequest::Ensure => control.schedule(requested.deadline_ns),
770                    OrdinaryRequest::Reconcile => control.reconcile(requested.deadline_ns),
771                };
772                (action, was_running)
773            }
774            EntryControl::Watchdog(_) => {
775                return Err(RegistryError::WrongPolicy {
776                    actual: entry.policy.label(),
777                });
778            }
779        };
780
781        let action = match action {
782            Ok(action) => action,
783            Err(error) => {
784                return Ok(terminal_ordinary(entry, claim.identity.clone(), error));
785            }
786        };
787
788        if was_running {
789            let EntryControl::Ordinary { pending, .. } = &mut entry.control else {
790                return Err(RegistryError::WrongPolicy {
791                    actual: entry.policy.label(),
792                });
793            };
794            *pending = Some(select_pending_ordinary(*pending, request, requested));
795        }
796        if matches!(request, OrdinaryRequest::Reconcile) {
797            entry.scheduling_mode = requested.mode;
798        }
799
800        Ok(apply_ordinary_action(
801            entry,
802            claim.identity.clone(),
803            now_ns,
804            action,
805            requested,
806        ))
807    }
808
809    fn ensure_watchdog(
810        &mut self,
811        claim: &RegistrationClaim,
812        now_ns: u64,
813        cadence: TimerCadence,
814    ) -> Result<RegistryTransition, RegistryError> {
815        let entry = self.entry_mut(claim)?;
816        if !matches!(entry.policy, TimerPolicy::Watchdog { .. }) {
817            return Err(RegistryError::WrongPolicy {
818                actual: entry.policy.label(),
819            });
820        }
821        entry.observability.counters_mut().record_schedule_request();
822        entry.latest_requested_delay_ns = Some(cadence.as_nanos());
823
824        let EntryControl::Watchdog(control) = &mut entry.control else {
825            return Err(RegistryError::WrongPolicy {
826                actual: entry.policy.label(),
827            });
828        };
829        match control.state {
830            WatchdogState::Inactive => {
831                let deadline_ns = cadence.deadline_after(now_ns)?;
832                let Some(generation) = control.scheduler_generation.checked_add(1) else {
833                    control.inactive_reason =
834                        InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
835                    return Ok(RegistryTransition::terminal(
836                        RegistryEffect::None,
837                        TimerControlFailure::GenerationExhausted,
838                    ));
839                };
840                control.scheduler_generation = generation;
841                control.state = WatchdogState::Scheduled {
842                    scheduler_generation: generation,
843                    deadline_ns,
844                };
845                control.pending = None;
846                entry.scheduling_mode = TimerSchedulingMode::Watchdog;
847                Ok(RegistryTransition::normal(RegistryEffect::ArmWakeup {
848                    token: token_for(claim, generation, CallbackRole::WatchdogScheduler),
849                    deadline_ns,
850                    delay_ns: deadline_ns.saturating_sub(now_ns),
851                    replace: false,
852                }))
853            }
854            WatchdogState::Scheduled { .. }
855            | WatchdogState::AwaitingWork {
856                attempt_status: WatchdogAttemptStatus::Dispatched,
857                ..
858            } => {
859                entry.observability.counters_mut().record_coalesced();
860                Ok(RegistryTransition::normal(RegistryEffect::None))
861            }
862            WatchdogState::AwaitingWork {
863                attempt_status: WatchdogAttemptStatus::Running,
864                ..
865            } => {
866                let Some(sequence) = control.request_sequence.checked_add(1) else {
867                    control.state = WatchdogState::Inactive;
868                    control.inactive_reason = InactiveReason::ControlFailure(
869                        TimerControlFailure::RequestSequenceExhausted,
870                    );
871                    return Ok(RegistryTransition::terminal(
872                        clear_callbacks(claim.identity.clone(), true, false),
873                        TimerControlFailure::RequestSequenceExhausted,
874                    ));
875                };
876                control.request_sequence = sequence;
877                if !matches!(control.pending, Some(WatchdogPending::Unregister)) {
878                    control.pending = Some(WatchdogPending::Ensure);
879                }
880                entry.observability.counters_mut().record_coalesced();
881                Ok(RegistryTransition::normal(RegistryEffect::None))
882            }
883        }
884    }
885
886    pub(crate) fn cancel(
887        &mut self,
888        claim: &RegistrationClaim,
889    ) -> Result<RegistryTransition, RegistryError> {
890        let identity = claim.identity.clone();
891        let (transition, remove) = {
892            let entry = self.entry_mut(claim)?;
893            match &mut entry.control {
894                EntryControl::Ordinary {
895                    control,
896                    pending,
897                    inactive_reason,
898                } => {
899                    let before = control.registration();
900                    let action = match control.cancel() {
901                        Ok(action) => action,
902                        Err(error) => {
903                            return Ok(terminal_ordinary(entry, identity.clone(), error));
904                        }
905                    };
906                    let mut remove = false;
907                    let transition = match (before, action) {
908                        (crate::TimerRegistration::Unregistered, TimerControlAction::None) => {
909                            RegistryTransition::normal(RegistryEffect::None)
910                        }
911                        (crate::TimerRegistration::Running { .. }, TimerControlAction::None) => {
912                            if !matches!(*pending, Some(OrdinaryPending::Unregister)) {
913                                *pending = Some(OrdinaryPending::Cancel);
914                            }
915                            RegistryTransition::normal(RegistryEffect::None)
916                        }
917                        (_, TimerControlAction::Clear) => {
918                            *inactive_reason = InactiveReason::Cancelled;
919                            entry.observability.counters_mut().record_cancellation();
920                            remove =
921                                matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
922                            RegistryTransition::normal(clear_callbacks(
923                                identity.clone(),
924                                true,
925                                false,
926                            ))
927                        }
928                        (crate::TimerRegistration::Scheduled { .. }, TimerControlAction::None)
929                        | (
930                            _,
931                            TimerControlAction::Arm { .. }
932                            | TimerControlAction::Replace { .. }
933                            | TimerControlAction::Disarm { .. },
934                        ) => {
935                            let clear_wakeup = control.terminate();
936                            *pending = None;
937                            *inactive_reason = InactiveReason::ControlFailure(
938                                TimerControlFailure::DirectiveNotAllowed,
939                            );
940                            RegistryTransition::terminal(
941                                clear_callbacks(identity.clone(), clear_wakeup, false),
942                                TimerControlFailure::DirectiveNotAllowed,
943                            )
944                        }
945                    };
946                    (transition, remove)
947                }
948                EntryControl::Watchdog(control) => {
949                    let (transition, changed) = cancel_watchdog(control, &identity);
950                    if changed && transition.failure().is_none() {
951                        entry.observability.counters_mut().record_cancellation();
952                    }
953                    let stopped = changed || transition.failure().is_some();
954                    (
955                        transition,
956                        stopped && matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped),
957                    )
958                }
959            }
960        };
961
962        if remove {
963            self.entries.remove(&identity);
964        }
965        Ok(transition)
966    }
967
968    /// Consume one logical claim and remove its declaration.
969    ///
970    /// Removal requested by running work is finalized by that work's normal
971    /// completion. A trapping work message rolls the request back with the
972    /// rest of its heap mutations.
973    #[allow(clippy::needless_pass_by_value)] // Ownership consumption invalidates the claim.
974    pub(crate) fn unregister(
975        &mut self,
976        claim: RegistrationClaim,
977    ) -> Result<RegistryTransition, RegistryError> {
978        let identity = claim.identity.clone();
979        let running_role = {
980            let entry = self.entry(&claim)?;
981            match &entry.control {
982                EntryControl::Ordinary { control, .. }
983                    if matches!(
984                        control.registration(),
985                        crate::TimerRegistration::Running { .. }
986                    ) =>
987                {
988                    Some(CallbackRole::OrdinaryWork)
989                }
990                EntryControl::Watchdog(control)
991                    if matches!(
992                        control.state,
993                        WatchdogState::AwaitingWork {
994                            attempt_status: WatchdogAttemptStatus::Running,
995                            ..
996                        }
997                    ) =>
998                {
999                    Some(CallbackRole::WatchdogWork)
1000                }
1001                EntryControl::Ordinary { .. } | EntryControl::Watchdog(_) => None,
1002            }
1003        };
1004
1005        match running_role {
1006            Some(CallbackRole::OrdinaryWork) => {
1007                let entry = self.entry_mut(&claim)?;
1008                let EntryControl::Ordinary {
1009                    control,
1010                    pending,
1011                    inactive_reason,
1012                } = &mut entry.control
1013                else {
1014                    return Err(RegistryError::WrongPolicy {
1015                        actual: entry.policy.label(),
1016                    });
1017                };
1018                let transition = match control.cancel() {
1019                    Ok(TimerControlAction::None) => {
1020                        *pending = Some(OrdinaryPending::Unregister);
1021                        RegistryTransition::normal(RegistryEffect::None)
1022                    }
1023                    Ok(_) => RegistryTransition::terminal(
1024                        RegistryEffect::None,
1025                        TimerControlFailure::DirectiveNotAllowed,
1026                    ),
1027                    Err(error) => {
1028                        let failure = map_control_failure(error);
1029                        let clear_wakeup = control.terminate();
1030                        *pending = None;
1031                        *inactive_reason = InactiveReason::ControlFailure(failure);
1032                        RegistryTransition::terminal(
1033                            clear_callbacks(identity.clone(), clear_wakeup, false),
1034                            failure,
1035                        )
1036                    }
1037                };
1038                if transition.failure().is_some() {
1039                    self.entries.remove(&identity);
1040                }
1041                Ok(transition)
1042            }
1043            Some(CallbackRole::WatchdogWork) => {
1044                let entry = self.entry_mut(&claim)?;
1045                let EntryControl::Watchdog(control) = &mut entry.control else {
1046                    return Err(RegistryError::WrongPolicy {
1047                        actual: entry.policy.label(),
1048                    });
1049                };
1050                let Some(sequence) = control.request_sequence.checked_add(1) else {
1051                    control.state = WatchdogState::Inactive;
1052                    control.inactive_reason = InactiveReason::ControlFailure(
1053                        TimerControlFailure::RequestSequenceExhausted,
1054                    );
1055                    let transition = RegistryTransition::terminal(
1056                        clear_callbacks(identity.clone(), true, false),
1057                        TimerControlFailure::RequestSequenceExhausted,
1058                    );
1059                    self.entries.remove(&identity);
1060                    return Ok(transition);
1061                };
1062                control.request_sequence = sequence;
1063                control.pending = Some(WatchdogPending::Unregister);
1064                Ok(RegistryTransition::normal(RegistryEffect::None))
1065            }
1066            Some(CallbackRole::WatchdogScheduler) => Err(RegistryError::StaleCallback),
1067            None => {
1068                let transition = self.cancel(&claim)?;
1069                self.entries.remove(&identity);
1070                Ok(transition)
1071            }
1072        }
1073    }
1074
1075    pub(crate) fn begin_ordinary(&mut self, token: &CallbackToken) -> CallbackAcceptance {
1076        let Some(entry) = self.entries.get_mut(token.identity()) else {
1077            return CallbackAcceptance::Stale;
1078        };
1079        if token.role != CallbackRole::OrdinaryWork
1080            || token.claim_generation != entry.claim_generation
1081        {
1082            entry.observability.counters_mut().record_stale_wakeup();
1083            return CallbackAcceptance::Stale;
1084        }
1085        let EntryControl::Ordinary { control, .. } = &mut entry.control else {
1086            entry.observability.counters_mut().record_stale_wakeup();
1087            return CallbackAcceptance::Stale;
1088        };
1089        if control.begin(token.callback_generation) {
1090            entry.observability.counters_mut().record_work_started();
1091            CallbackAcceptance::Accepted
1092        } else {
1093            entry.observability.counters_mut().record_stale_wakeup();
1094            CallbackAcceptance::Stale
1095        }
1096    }
1097
1098    #[allow(clippy::too_many_lines)] // One atomic policy transition; splitting obscures rollback state.
1099    pub(crate) fn complete_ordinary(
1100        &mut self,
1101        token: &CallbackToken,
1102        now_ns: u64,
1103        result: TimerRunResult,
1104    ) -> Result<RegistryTransition, RegistryError> {
1105        let identity = token.identity.clone();
1106        let (transition, remove) = {
1107            let entry = self.entry_by_token_mut(token, CallbackRole::OrdinaryWork)?;
1108            let EntryControl::Ordinary {
1109                control,
1110                pending,
1111                inactive_reason,
1112            } = &mut entry.control
1113            else {
1114                return Err(RegistryError::StaleCallback);
1115            };
1116            if control.registration()
1117                != (crate::TimerRegistration::Running {
1118                    generation: token.callback_generation,
1119                })
1120            {
1121                return Err(RegistryError::StaleCallback);
1122            }
1123
1124            let completion = result.completion();
1125            let pending_command = *pending;
1126            let terminal_pending = matches!(
1127                pending_command,
1128                Some(OrdinaryPending::Cancel | OrdinaryPending::Unregister)
1129            );
1130            if completion.outcome() == TimerCompletionOutcome::InvariantFailure {
1131                let transition =
1132                    invariant_completion(entry, token.callback_generation, completion, now_ns);
1133                let remove = matches!(pending_command, Some(OrdinaryPending::Unregister))
1134                    || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1135                (transition, remove)
1136            } else if !terminal_pending
1137                && matches!(entry.policy, TimerPolicy::Once)
1138                && matches!(result.directive(), TimerDirective::RecurAfterCompletion)
1139            {
1140                let transition = terminal_completion(
1141                    entry,
1142                    token.callback_generation,
1143                    completion.work_count(),
1144                    now_ns,
1145                    TimerControlFailure::DirectiveNotAllowed,
1146                );
1147                let remove = matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1148                (transition, remove)
1149            } else {
1150                let effective_directive = if terminal_pending {
1151                    TimerDirective::Stop
1152                } else {
1153                    result.directive()
1154                };
1155                let cadence = match entry.policy {
1156                    TimerPolicy::AfterCompletion { cadence } => Some(cadence),
1157                    TimerPolicy::Once => None,
1158                    TimerPolicy::Watchdog { .. } => {
1159                        return Err(RegistryError::StaleCallback);
1160                    }
1161                };
1162                let resolved = match effective_directive.resolve(now_ns, cadence) {
1163                    Ok(value) => value,
1164                    Err(error) => {
1165                        let transition = terminal_completion(
1166                            entry,
1167                            token.callback_generation,
1168                            completion.work_count(),
1169                            now_ns,
1170                            map_schedule_failure(error),
1171                        );
1172                        let remove = matches!(pending_command, Some(OrdinaryPending::Unregister))
1173                            || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1174                        return Ok(remove_after(
1175                            &mut self.entries,
1176                            &identity,
1177                            transition,
1178                            remove,
1179                        ));
1180                    }
1181                };
1182                let directive_snapshot = TimerDirectiveSnapshot::try_from(effective_directive)
1183                    .map_err(RegistryError::Schedule)?;
1184                let callback_schedule = resolved.deadline_ns.map(|deadline_ns| PendingSchedule {
1185                    deadline_ns,
1186                    requested_delay_ns: resolved.requested_delay_ns,
1187                    mode: directive_snapshot
1188                        .scheduling_mode()
1189                        .unwrap_or(entry.scheduling_mode),
1190                });
1191                let selected_schedule =
1192                    select_completion_schedule(pending_command, callback_schedule);
1193                let action = match control.complete(
1194                    token.callback_generation,
1195                    selected_schedule.map(|value| value.deadline_ns),
1196                    terminal_pending,
1197                ) {
1198                    Ok(action) => action,
1199                    Err(TimerControlError::StaleCompletion) => {
1200                        return Err(RegistryError::StaleCallback);
1201                    }
1202                    Err(error) => {
1203                        let transition = terminal_completion(
1204                            entry,
1205                            token.callback_generation,
1206                            completion.work_count(),
1207                            now_ns,
1208                            map_control_failure(error),
1209                        );
1210                        let remove = matches!(pending_command, Some(OrdinaryPending::Unregister))
1211                            || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1212                        return Ok(remove_after(
1213                            &mut self.entries,
1214                            &identity,
1215                            transition,
1216                            remove,
1217                        ));
1218                    }
1219                };
1220                *pending = None;
1221                entry.latest_directive = Some(directive_snapshot);
1222                entry.observability.record_completion(completion, now_ns);
1223
1224                let mut remove = false;
1225                let transition = match action {
1226                    TimerControlAction::Arm {
1227                        generation,
1228                        deadline_ns,
1229                    }
1230                    | TimerControlAction::Replace {
1231                        generation,
1232                        deadline_ns,
1233                    } => {
1234                        let selected = selected_schedule.unwrap_or(PendingSchedule {
1235                            deadline_ns,
1236                            requested_delay_ns: None,
1237                            mode: entry.scheduling_mode,
1238                        });
1239                        let replace = matches!(action, TimerControlAction::Replace { .. });
1240                        entry.scheduling_mode = selected.mode;
1241                        entry.latest_requested_delay_ns = selected.requested_delay_ns;
1242                        RegistryTransition::normal(RegistryEffect::ArmWakeup {
1243                            token: CallbackToken::new(
1244                                identity.clone(),
1245                                entry.claim_generation,
1246                                generation,
1247                                CallbackRole::OrdinaryWork,
1248                            ),
1249                            deadline_ns,
1250                            delay_ns: deadline_ns.saturating_sub(now_ns),
1251                            replace,
1252                        })
1253                    }
1254                    TimerControlAction::Disarm { cancelled } => {
1255                        *inactive_reason = if cancelled {
1256                            entry.observability.counters_mut().record_cancellation();
1257                            InactiveReason::Cancelled
1258                        } else {
1259                            InactiveReason::Stopped
1260                        };
1261                        remove = matches!(pending_command, Some(OrdinaryPending::Unregister))
1262                            || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1263                        RegistryTransition::normal(RegistryEffect::None)
1264                    }
1265                    TimerControlAction::None | TimerControlAction::Clear => {
1266                        let clear_wakeup = control.terminate();
1267                        *inactive_reason = InactiveReason::ControlFailure(
1268                            TimerControlFailure::DirectiveNotAllowed,
1269                        );
1270                        remove = matches!(pending_command, Some(OrdinaryPending::Unregister))
1271                            || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped);
1272                        RegistryTransition::terminal(
1273                            clear_callbacks(identity.clone(), clear_wakeup, false),
1274                            TimerControlFailure::DirectiveNotAllowed,
1275                        )
1276                    }
1277                };
1278                (transition, remove)
1279            }
1280        };
1281
1282        Ok(remove_after(
1283            &mut self.entries,
1284            &identity,
1285            transition,
1286            remove,
1287        ))
1288    }
1289
1290    #[allow(clippy::too_many_lines)] // The bounded scheduler protocol is audited as one path.
1291    pub(crate) fn begin_watchdog_scheduler(
1292        &mut self,
1293        token: &CallbackToken,
1294        now_ns: u64,
1295    ) -> RegistryTransition {
1296        let Some(entry) = self.entries.get_mut(token.identity()) else {
1297            return RegistryTransition::normal(RegistryEffect::None);
1298        };
1299        if token.role != CallbackRole::WatchdogScheduler
1300            || token.claim_generation != entry.claim_generation
1301        {
1302            entry.observability.counters_mut().record_stale_wakeup();
1303            return RegistryTransition::normal(RegistryEffect::None);
1304        }
1305        let TimerPolicy::Watchdog { cadence } = entry.policy else {
1306            entry.observability.counters_mut().record_stale_wakeup();
1307            return RegistryTransition::normal(RegistryEffect::None);
1308        };
1309        let EntryControl::Watchdog(control) = &mut entry.control else {
1310            entry.observability.counters_mut().record_stale_wakeup();
1311            return RegistryTransition::normal(RegistryEffect::None);
1312        };
1313
1314        let accepted = match control.state {
1315            WatchdogState::Scheduled {
1316                scheduler_generation,
1317                ..
1318            } => scheduler_generation == token.callback_generation,
1319            WatchdogState::AwaitingWork {
1320                successor_generation,
1321                attempt_status: WatchdogAttemptStatus::Dispatched,
1322                ..
1323            } => successor_generation == token.callback_generation,
1324            WatchdogState::Inactive
1325            | WatchdogState::AwaitingWork {
1326                attempt_status: WatchdogAttemptStatus::Running,
1327                ..
1328            } => false,
1329        };
1330        if !accepted {
1331            entry.observability.counters_mut().record_stale_wakeup();
1332            return RegistryTransition::normal(RegistryEffect::None);
1333        }
1334
1335        entry
1336            .observability
1337            .counters_mut()
1338            .record_scheduler_started();
1339        if matches!(control.state, WatchdogState::AwaitingWork { .. }) {
1340            entry.observability.record_unacknowledged(now_ns);
1341        }
1342        let Some(successor_generation) = control.scheduler_generation.checked_add(1) else {
1343            control.state = WatchdogState::Inactive;
1344            control.inactive_reason =
1345                InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
1346            return RegistryTransition::terminal(
1347                clear_callbacks(token.identity.clone(), false, true),
1348                TimerControlFailure::GenerationExhausted,
1349            );
1350        };
1351        let Some(attempt_generation) = control.attempt_generation.checked_add(1) else {
1352            control.state = WatchdogState::Inactive;
1353            control.inactive_reason =
1354                InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
1355            return RegistryTransition::terminal(
1356                clear_callbacks(token.identity.clone(), false, true),
1357                TimerControlFailure::GenerationExhausted,
1358            );
1359        };
1360        let Ok(successor_deadline_ns) = cadence.deadline_after(now_ns) else {
1361            control.state = WatchdogState::Inactive;
1362            control.inactive_reason =
1363                InactiveReason::ControlFailure(TimerControlFailure::DeadlineOverflow);
1364            return RegistryTransition::terminal(
1365                clear_callbacks(token.identity.clone(), false, true),
1366                TimerControlFailure::DeadlineOverflow,
1367            );
1368        };
1369
1370        control.scheduler_generation = successor_generation;
1371        control.attempt_generation = attempt_generation;
1372        control.state = WatchdogState::AwaitingWork {
1373            successor_generation,
1374            successor_deadline_ns,
1375            attempt_generation,
1376            attempt_status: WatchdogAttemptStatus::Dispatched,
1377        };
1378        control.pending = None;
1379        RegistryTransition::normal(RegistryEffect::DispatchWatchdog {
1380            successor: CallbackToken::new(
1381                token.identity.clone(),
1382                entry.claim_generation,
1383                successor_generation,
1384                CallbackRole::WatchdogScheduler,
1385            ),
1386            successor_deadline_ns,
1387            successor_delay_ns: cadence.as_nanos(),
1388            work: CallbackToken::new(
1389                token.identity.clone(),
1390                entry.claim_generation,
1391                attempt_generation,
1392                CallbackRole::WatchdogWork,
1393            ),
1394        })
1395    }
1396
1397    pub(crate) fn begin_watchdog_work(&mut self, token: &CallbackToken) -> CallbackAcceptance {
1398        let Some(entry) = self.entries.get_mut(token.identity()) else {
1399            return CallbackAcceptance::Stale;
1400        };
1401        if token.role != CallbackRole::WatchdogWork
1402            || token.claim_generation != entry.claim_generation
1403        {
1404            entry.observability.counters_mut().record_stale_work();
1405            return CallbackAcceptance::Stale;
1406        }
1407        let EntryControl::Watchdog(control) = &mut entry.control else {
1408            entry.observability.counters_mut().record_stale_work();
1409            return CallbackAcceptance::Stale;
1410        };
1411        match &mut control.state {
1412            WatchdogState::AwaitingWork {
1413                attempt_generation,
1414                attempt_status,
1415                ..
1416            } if *attempt_generation == token.callback_generation
1417                && *attempt_status == WatchdogAttemptStatus::Dispatched =>
1418            {
1419                *attempt_status = WatchdogAttemptStatus::Running;
1420                entry.observability.counters_mut().record_work_started();
1421                CallbackAcceptance::Accepted
1422            }
1423            WatchdogState::Inactive
1424            | WatchdogState::Scheduled { .. }
1425            | WatchdogState::AwaitingWork { .. } => {
1426                entry.observability.counters_mut().record_stale_work();
1427                CallbackAcceptance::Stale
1428            }
1429        }
1430    }
1431
1432    pub(crate) fn complete_watchdog_work(
1433        &mut self,
1434        token: &CallbackToken,
1435        now_ns: u64,
1436        result: WatchdogRunResult,
1437    ) -> Result<RegistryTransition, RegistryError> {
1438        let identity = token.identity.clone();
1439        let (transition, remove) = {
1440            let entry = self.entry_by_token_mut(token, CallbackRole::WatchdogWork)?;
1441            let EntryControl::Watchdog(control) = &mut entry.control else {
1442                return Err(RegistryError::StaleCallback);
1443            };
1444            let (successor_generation, successor_deadline_ns) = match control.state {
1445                WatchdogState::AwaitingWork {
1446                    successor_generation,
1447                    successor_deadline_ns,
1448                    attempt_generation,
1449                    attempt_status: WatchdogAttemptStatus::Running,
1450                } if attempt_generation == token.callback_generation => {
1451                    (successor_generation, successor_deadline_ns)
1452                }
1453                WatchdogState::Inactive
1454                | WatchdogState::Scheduled { .. }
1455                | WatchdogState::AwaitingWork { .. } => {
1456                    return Err(RegistryError::StaleCallback);
1457                }
1458            };
1459
1460            let completion = result.completion();
1461            entry.observability.record_completion(completion, now_ns);
1462            let decision = if completion.outcome() == TimerCompletionOutcome::InvariantFailure {
1463                WatchdogDecision::Stop
1464            } else {
1465                match control.pending {
1466                    Some(WatchdogPending::Cancel | WatchdogPending::Unregister) => {
1467                        WatchdogDecision::Stop
1468                    }
1469                    Some(WatchdogPending::Ensure) => WatchdogDecision::Continue,
1470                    None => result.decision(),
1471                }
1472            };
1473            let cancelled = matches!(control.pending, Some(WatchdogPending::Cancel));
1474            let unregister = matches!(control.pending, Some(WatchdogPending::Unregister));
1475            control.pending = None;
1476
1477            match decision {
1478                WatchdogDecision::Continue => {
1479                    control.state = WatchdogState::Scheduled {
1480                        scheduler_generation: successor_generation,
1481                        deadline_ns: successor_deadline_ns,
1482                    };
1483                    (RegistryTransition::normal(RegistryEffect::None), false)
1484                }
1485                WatchdogDecision::Stop => {
1486                    control.state = WatchdogState::Inactive;
1487                    control.inactive_reason = if cancelled {
1488                        entry.observability.counters_mut().record_cancellation();
1489                        InactiveReason::Cancelled
1490                    } else if completion.outcome() == TimerCompletionOutcome::InvariantFailure {
1491                        InactiveReason::InvariantFailure
1492                    } else {
1493                        InactiveReason::Stopped
1494                    };
1495                    (
1496                        RegistryTransition::normal(clear_callbacks(identity.clone(), true, false)),
1497                        unregister
1498                            || matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped),
1499                    )
1500                }
1501            }
1502        };
1503
1504        Ok(remove_after(
1505            &mut self.entries,
1506            &identity,
1507            transition,
1508            remove,
1509        ))
1510    }
1511
1512    pub(crate) fn snapshot(&self, identity: &TimerIdentity) -> Option<TimerSnapshot> {
1513        self.entries
1514            .get(identity)
1515            .map(|entry| entry.snapshot(identity.clone()))
1516    }
1517
1518    pub(crate) fn snapshots(&self) -> Vec<TimerSnapshot> {
1519        self.entries
1520            .iter()
1521            .map(|(identity, entry)| entry.snapshot(identity.clone()))
1522            .collect()
1523    }
1524
1525    pub(crate) fn consecutive_expected_failures(&self, identity: &TimerIdentity) -> Option<u64> {
1526        self.entries
1527            .get(identity)
1528            .map(|entry| entry.observability.consecutive_expected_failures())
1529    }
1530
1531    pub(crate) fn record_scheduler_instructions(
1532        &mut self,
1533        token: &CallbackToken,
1534        instructions: u64,
1535    ) {
1536        let Some(entry) = self.entries.get_mut(token.identity()) else {
1537            return;
1538        };
1539        if token.claim_generation == entry.claim_generation
1540            && token.role == CallbackRole::WatchdogScheduler
1541            && matches!(entry.control, EntryControl::Watchdog(_))
1542        {
1543            entry
1544                .observability
1545                .record_scheduler_instructions(instructions);
1546        }
1547    }
1548
1549    pub(crate) fn record_work_instructions(&mut self, token: &CallbackToken, instructions: u64) {
1550        let Some(entry) = self.entries.get_mut(token.identity()) else {
1551            return;
1552        };
1553        let role_matches = matches!(
1554            (&entry.control, token.role),
1555            (EntryControl::Ordinary { .. }, CallbackRole::OrdinaryWork)
1556                | (EntryControl::Watchdog(_), CallbackRole::WatchdogWork)
1557        );
1558        if token.claim_generation == entry.claim_generation && role_matches {
1559            entry.observability.record_work_instructions(instructions);
1560        }
1561    }
1562
1563    pub(crate) fn fail_registration(
1564        &mut self,
1565        claim: &RegistrationClaim,
1566        failure: TimerControlFailure,
1567    ) -> Result<ProviderHandles, RegistryError> {
1568        let identity = claim.identity.clone();
1569        let (handles, remove) = {
1570            let entry = self.entry_mut(claim)?;
1571            match &mut entry.control {
1572                EntryControl::Ordinary {
1573                    control,
1574                    pending,
1575                    inactive_reason,
1576                } => {
1577                    control.terminate();
1578                    *pending = None;
1579                    *inactive_reason = InactiveReason::ControlFailure(failure);
1580                }
1581                EntryControl::Watchdog(control) => {
1582                    control.state = WatchdogState::Inactive;
1583                    control.pending = None;
1584                    control.inactive_reason = InactiveReason::ControlFailure(failure);
1585                }
1586            }
1587            let handles =
1588                ProviderHandles {
1589                    wakeup: entry.wakeup.take().map(|owned| {
1590                        detach_provider_handle(&identity, entry.claim_generation, owned)
1591                    }),
1592                    work: entry.work.take().map(|owned| {
1593                        detach_provider_handle(&identity, entry.claim_generation, owned)
1594                    }),
1595                };
1596            (
1597                handles,
1598                matches!(entry.lifetime, DeclarationLifetime::RemoveWhenStopped),
1599            )
1600        };
1601        if remove {
1602            self.entries.remove(&identity);
1603        }
1604        Ok(handles)
1605    }
1606
1607    pub(crate) fn ordinary_callback(
1608        &self,
1609        token: &CallbackToken,
1610    ) -> Result<OrdinaryCallback, RegistryError> {
1611        let entry = self
1612            .entries
1613            .get(token.identity())
1614            .ok_or(RegistryError::StaleCallback)?;
1615        if token.role != CallbackRole::OrdinaryWork
1616            || token.claim_generation != entry.claim_generation
1617            || !matches!(
1618                entry.control,
1619                EntryControl::Ordinary {
1620                    ref control,
1621                    ..
1622                } if matches!(
1623                    control.registration(),
1624                    crate::TimerRegistration::Running { generation }
1625                        if generation == token.callback_generation
1626                )
1627            )
1628        {
1629            return Err(RegistryError::StaleCallback);
1630        }
1631        match &entry.callback {
1632            EntryCallback::Ordinary(callback) => Ok(Rc::clone(callback)),
1633            EntryCallback::Watchdog(_) => Err(RegistryError::MissingCallback),
1634            #[cfg(test)]
1635            EntryCallback::None => Err(RegistryError::MissingCallback),
1636        }
1637    }
1638
1639    pub(crate) fn watchdog_callback(
1640        &self,
1641        token: &CallbackToken,
1642    ) -> Result<WatchdogCallback, RegistryError> {
1643        let entry = self
1644            .entries
1645            .get(token.identity())
1646            .ok_or(RegistryError::StaleCallback)?;
1647        if token.role != CallbackRole::WatchdogWork
1648            || token.claim_generation != entry.claim_generation
1649            || !matches!(
1650                entry.control,
1651                EntryControl::Watchdog(WatchdogControl {
1652                    state: WatchdogState::AwaitingWork {
1653                        attempt_generation,
1654                        attempt_status: WatchdogAttemptStatus::Running,
1655                        ..
1656                    },
1657                    ..
1658                }) if attempt_generation == token.callback_generation
1659            )
1660        {
1661            return Err(RegistryError::StaleCallback);
1662        }
1663        match &entry.callback {
1664            EntryCallback::Watchdog(callback) => Ok(Rc::clone(callback)),
1665            EntryCallback::Ordinary(_) => Err(RegistryError::MissingCallback),
1666            #[cfg(test)]
1667            EntryCallback::None => Err(RegistryError::MissingCallback),
1668        }
1669    }
1670
1671    pub(crate) fn install_provider_handle(
1672        &mut self,
1673        token: &CallbackToken,
1674        handle: TimerHandle,
1675    ) -> Result<(), (RegistryError, TimerHandle)> {
1676        let entry = match self.entry_by_token_mut(token, token.role) {
1677            Ok(entry) => entry,
1678            Err(error) => return Err((error, handle)),
1679        };
1680        let valid = match (&entry.control, token.role) {
1681            (EntryControl::Ordinary { control, .. }, CallbackRole::OrdinaryWork) => matches!(
1682                control.registration(),
1683                crate::TimerRegistration::Scheduled { generation, .. }
1684                    if generation == token.callback_generation
1685            ),
1686            (EntryControl::Watchdog(control), CallbackRole::WatchdogScheduler) => {
1687                matches!(
1688                    control.state,
1689                    WatchdogState::Scheduled {
1690                        scheduler_generation,
1691                        ..
1692                    } if scheduler_generation == token.callback_generation
1693                ) || matches!(
1694                    control.state,
1695                    WatchdogState::AwaitingWork {
1696                        successor_generation,
1697                        ..
1698                    } if successor_generation == token.callback_generation
1699                )
1700            }
1701            (EntryControl::Watchdog(control), CallbackRole::WatchdogWork) => matches!(
1702                control.state,
1703                WatchdogState::AwaitingWork {
1704                    attempt_generation,
1705                    attempt_status: WatchdogAttemptStatus::Dispatched,
1706                    ..
1707                } if attempt_generation == token.callback_generation
1708            ),
1709            (
1710                EntryControl::Ordinary { .. },
1711                CallbackRole::WatchdogScheduler | CallbackRole::WatchdogWork,
1712            )
1713            | (EntryControl::Watchdog(_), CallbackRole::OrdinaryWork) => false,
1714        };
1715        if !valid {
1716            return Err((RegistryError::StaleCallback, handle));
1717        }
1718        let slot = match token.role {
1719            CallbackRole::OrdinaryWork | CallbackRole::WatchdogScheduler => &mut entry.wakeup,
1720            CallbackRole::WatchdogWork => &mut entry.work,
1721        };
1722        if slot.is_some() {
1723            return Err((RegistryError::ProviderHandleAlreadyOwned, handle));
1724        }
1725        *slot = Some(OwnedProviderHandle {
1726            callback_generation: token.callback_generation,
1727            role: token.role,
1728            handle,
1729        });
1730        Ok(())
1731    }
1732
1733    pub(crate) fn take_wakeup_handle(
1734        &mut self,
1735        identity: &TimerIdentity,
1736    ) -> Option<ProviderHandle> {
1737        let entry = self.entries.get_mut(identity)?;
1738        let owned = entry.wakeup.take()?;
1739        Some(detach_provider_handle(
1740            identity,
1741            entry.claim_generation,
1742            owned,
1743        ))
1744    }
1745
1746    pub(crate) fn take_work_handle(&mut self, identity: &TimerIdentity) -> Option<ProviderHandle> {
1747        let entry = self.entries.get_mut(identity)?;
1748        let owned = entry.work.take()?;
1749        Some(detach_provider_handle(
1750            identity,
1751            entry.claim_generation,
1752            owned,
1753        ))
1754    }
1755
1756    pub(crate) fn take_provider_handles_for_claim(
1757        &mut self,
1758        claim: &RegistrationClaim,
1759    ) -> Result<ProviderHandles, RegistryError> {
1760        let identity = claim.identity.clone();
1761        let entry = self.entry_mut(claim)?;
1762        Ok(ProviderHandles {
1763            wakeup: entry
1764                .wakeup
1765                .take()
1766                .map(|owned| detach_provider_handle(&identity, entry.claim_generation, owned)),
1767            work: entry
1768                .work
1769                .take()
1770                .map(|owned| detach_provider_handle(&identity, entry.claim_generation, owned)),
1771        })
1772    }
1773
1774    pub(crate) fn consume_provider_handle(&mut self, token: &CallbackToken) {
1775        let Some(entry) = self.entries.get_mut(token.identity()) else {
1776            return;
1777        };
1778        let slot = match token.role {
1779            CallbackRole::OrdinaryWork | CallbackRole::WatchdogScheduler => &mut entry.wakeup,
1780            CallbackRole::WatchdogWork => &mut entry.work,
1781        };
1782        let matches_token = slot.as_ref().is_some_and(|owned| {
1783            owned.callback_generation == token.callback_generation && owned.role == token.role
1784        });
1785        if matches_token {
1786            *slot = None;
1787        }
1788    }
1789
1790    /// Confirm that the platform successfully applied one emitted arm effect.
1791    ///
1792    /// Patch 3 calls this synchronously after owning the returned provider
1793    /// handles. Pure tests use it to distinguish requested effects from actual
1794    /// provider operations.
1795    pub(crate) fn confirm_effect_applied(
1796        &mut self,
1797        effect: &RegistryEffect,
1798    ) -> Result<(), RegistryError> {
1799        match effect {
1800            RegistryEffect::None | RegistryEffect::ClearCallbacks { .. } => Ok(()),
1801            RegistryEffect::ArmWakeup {
1802                token, delay_ns, ..
1803            } => {
1804                let entry = self.entry_by_token_mut(token, token.role)?;
1805                let valid_generation = match (&entry.control, token.role) {
1806                    (EntryControl::Ordinary { control, .. }, CallbackRole::OrdinaryWork) => {
1807                        matches!(
1808                            control.registration(),
1809                            crate::TimerRegistration::Scheduled { generation, .. }
1810                                if generation == token.callback_generation
1811                        )
1812                    }
1813                    (EntryControl::Watchdog(control), CallbackRole::WatchdogScheduler) => matches!(
1814                        control.state,
1815                        WatchdogState::Scheduled {
1816                            scheduler_generation,
1817                            ..
1818                        } if scheduler_generation == token.callback_generation
1819                    ),
1820                    (EntryControl::Ordinary { .. } | EntryControl::Watchdog(_), _) => false,
1821                };
1822                if !valid_generation {
1823                    return Err(RegistryError::StaleCallback);
1824                }
1825                if entry.confirmed_wakeup_generation == Some(token.callback_generation) {
1826                    return Ok(());
1827                }
1828                entry.confirmed_wakeup_generation = Some(token.callback_generation);
1829                entry.latest_armed_delay_ns = Some(*delay_ns);
1830                entry.observability.counters_mut().record_wakeup_armed();
1831                Ok(())
1832            }
1833            RegistryEffect::DispatchWatchdog {
1834                successor,
1835                successor_delay_ns,
1836                work,
1837                ..
1838            } => {
1839                let successor_claim_generation = successor.claim_generation;
1840                let successor_callback_generation = successor.callback_generation;
1841                let successor_role = successor.role;
1842                let entry = self.entry_by_token_mut(successor, successor_role)?;
1843                if successor_role != CallbackRole::WatchdogScheduler
1844                    || work.role != CallbackRole::WatchdogWork
1845                    || work.claim_generation != successor_claim_generation
1846                {
1847                    return Err(RegistryError::StaleCallback);
1848                }
1849                let EntryControl::Watchdog(control) = &entry.control else {
1850                    return Err(RegistryError::StaleCallback);
1851                };
1852                if !matches!(
1853                    control.state,
1854                    WatchdogState::AwaitingWork {
1855                        successor_generation,
1856                        attempt_generation,
1857                        ..
1858                    } if successor_generation == successor_callback_generation
1859                        && attempt_generation == work.callback_generation
1860                ) {
1861                    return Err(RegistryError::StaleCallback);
1862                }
1863                if entry.confirmed_wakeup_generation == Some(successor_callback_generation)
1864                    && entry.confirmed_work_generation == Some(work.callback_generation)
1865                {
1866                    return Ok(());
1867                }
1868                entry.confirmed_wakeup_generation = Some(successor_callback_generation);
1869                entry.confirmed_work_generation = Some(work.callback_generation);
1870                entry.latest_armed_delay_ns = Some(*successor_delay_ns);
1871                entry.observability.counters_mut().record_wakeup_armed();
1872                entry.observability.counters_mut().record_work_dispatched();
1873                Ok(())
1874            }
1875        }
1876    }
1877
1878    fn entry(&self, claim: &RegistrationClaim) -> Result<&Entry, RegistryError> {
1879        let entry = self
1880            .entries
1881            .get(claim.identity())
1882            .ok_or(RegistryError::UnknownRegistration)?;
1883        if entry.claim_generation != claim.claim_generation() {
1884            return Err(RegistryError::StaleRegistration);
1885        }
1886        Ok(entry)
1887    }
1888
1889    fn entry_mut(&mut self, claim: &RegistrationClaim) -> Result<&mut Entry, RegistryError> {
1890        let entry = self
1891            .entries
1892            .get_mut(claim.identity())
1893            .ok_or(RegistryError::UnknownRegistration)?;
1894        if entry.claim_generation != claim.claim_generation() {
1895            return Err(RegistryError::StaleRegistration);
1896        }
1897        Ok(entry)
1898    }
1899
1900    fn entry_by_token_mut(
1901        &mut self,
1902        token: &CallbackToken,
1903        role: CallbackRole,
1904    ) -> Result<&mut Entry, RegistryError> {
1905        let entry = self
1906            .entries
1907            .get_mut(token.identity())
1908            .ok_or(RegistryError::StaleCallback)?;
1909        if entry.claim_generation != token.claim_generation || token.role != role {
1910            return Err(RegistryError::StaleCallback);
1911        }
1912        Ok(entry)
1913    }
1914}
1915
1916fn token_for(
1917    claim: &RegistrationClaim,
1918    callback_generation: u64,
1919    role: CallbackRole,
1920) -> CallbackToken {
1921    CallbackToken::new(
1922        claim.identity.clone(),
1923        claim.claim_generation,
1924        callback_generation,
1925        role,
1926    )
1927}
1928
1929fn detach_provider_handle(
1930    identity: &TimerIdentity,
1931    claim_generation: u64,
1932    owned: OwnedProviderHandle,
1933) -> ProviderHandle {
1934    ProviderHandle {
1935        token: CallbackToken::new(
1936            identity.clone(),
1937            claim_generation,
1938            owned.callback_generation,
1939            owned.role,
1940        ),
1941        handle: owned.handle,
1942    }
1943}
1944
1945const fn clear_callbacks(
1946    identity: TimerIdentity,
1947    clear_wakeup: bool,
1948    clear_work: bool,
1949) -> RegistryEffect {
1950    RegistryEffect::ClearCallbacks {
1951        identity,
1952        clear_wakeup,
1953        clear_work,
1954    }
1955}
1956
1957fn apply_ordinary_action(
1958    entry: &mut Entry,
1959    identity: TimerIdentity,
1960    now_ns: u64,
1961    action: TimerControlAction,
1962    requested: PendingSchedule,
1963) -> RegistryTransition {
1964    match action {
1965        TimerControlAction::Arm {
1966            generation,
1967            deadline_ns,
1968        }
1969        | TimerControlAction::Replace {
1970            generation,
1971            deadline_ns,
1972        } => {
1973            let replace = matches!(action, TimerControlAction::Replace { .. });
1974            entry.scheduling_mode = requested.mode;
1975            RegistryTransition::normal(RegistryEffect::ArmWakeup {
1976                token: CallbackToken::new(
1977                    identity,
1978                    entry.claim_generation,
1979                    generation,
1980                    CallbackRole::OrdinaryWork,
1981                ),
1982                deadline_ns,
1983                delay_ns: deadline_ns.saturating_sub(now_ns),
1984                replace,
1985            })
1986        }
1987        TimerControlAction::None => {
1988            entry.observability.counters_mut().record_coalesced();
1989            RegistryTransition::normal(RegistryEffect::None)
1990        }
1991        TimerControlAction::Clear | TimerControlAction::Disarm { .. } => {
1992            terminal_ordinary(entry, identity, TimerControlError::StaleCompletion)
1993        }
1994    }
1995}
1996
1997fn terminal_ordinary(
1998    entry: &mut Entry,
1999    identity: TimerIdentity,
2000    error: TimerControlError,
2001) -> RegistryTransition {
2002    let failure = map_control_failure(error);
2003    let EntryControl::Ordinary {
2004        control,
2005        pending,
2006        inactive_reason,
2007    } = &mut entry.control
2008    else {
2009        return RegistryTransition::terminal(RegistryEffect::None, failure);
2010    };
2011    let clear_wakeup = control.terminate();
2012    *pending = None;
2013    *inactive_reason = InactiveReason::ControlFailure(failure);
2014    RegistryTransition::terminal(
2015        RegistryEffect::ClearCallbacks {
2016            identity,
2017            clear_wakeup,
2018            clear_work: false,
2019        },
2020        failure,
2021    )
2022}
2023
2024fn terminal_completion(
2025    entry: &mut Entry,
2026    generation: u64,
2027    work_count: u64,
2028    now_ns: u64,
2029    failure: TimerControlFailure,
2030) -> RegistryTransition {
2031    let EntryControl::Ordinary {
2032        control,
2033        pending,
2034        inactive_reason,
2035    } = &mut entry.control
2036    else {
2037        return RegistryTransition::terminal(RegistryEffect::None, failure);
2038    };
2039    if control.registration() == (crate::TimerRegistration::Running { generation }) {
2040        control.terminate();
2041    }
2042    *pending = None;
2043    *inactive_reason = InactiveReason::ControlFailure(failure);
2044    entry.latest_directive = Some(TimerDirectiveSnapshot::Stop);
2045    entry
2046        .observability
2047        .record_completion(TimerCompletion::invariant_failure(work_count), now_ns);
2048    RegistryTransition::terminal(RegistryEffect::None, failure)
2049}
2050
2051fn invariant_completion(
2052    entry: &mut Entry,
2053    generation: u64,
2054    completion: TimerCompletion,
2055    now_ns: u64,
2056) -> RegistryTransition {
2057    let EntryControl::Ordinary {
2058        control,
2059        pending,
2060        inactive_reason,
2061    } = &mut entry.control
2062    else {
2063        return RegistryTransition::normal(RegistryEffect::None);
2064    };
2065    if control.registration() == (crate::TimerRegistration::Running { generation }) {
2066        control.terminate();
2067    }
2068    *pending = None;
2069    *inactive_reason = InactiveReason::InvariantFailure;
2070    entry.latest_directive = Some(TimerDirectiveSnapshot::Stop);
2071    entry.observability.record_completion(completion, now_ns);
2072    RegistryTransition::normal(RegistryEffect::None)
2073}
2074
2075const fn map_control_failure(error: TimerControlError) -> TimerControlFailure {
2076    match error {
2077        TimerControlError::RequestSequenceExhausted => {
2078            TimerControlFailure::RequestSequenceExhausted
2079        }
2080        TimerControlError::GenerationExhausted => TimerControlFailure::GenerationExhausted,
2081        TimerControlError::StaleCompletion => TimerControlFailure::DirectiveNotAllowed,
2082    }
2083}
2084
2085const fn map_schedule_failure(error: ScheduleError) -> TimerControlFailure {
2086    match error {
2087        ScheduleError::DeadlineOverflow => TimerControlFailure::DeadlineOverflow,
2088        ScheduleError::DelayOutOfRange => TimerControlFailure::DelayOutOfRange,
2089        ScheduleError::ZeroCadence | ScheduleError::MissingCadence => {
2090            TimerControlFailure::DirectiveNotAllowed
2091        }
2092    }
2093}
2094
2095const fn select_completion_schedule(
2096    pending: Option<OrdinaryPending>,
2097    callback: Option<PendingSchedule>,
2098) -> Option<PendingSchedule> {
2099    match pending {
2100        Some(OrdinaryPending::Cancel | OrdinaryPending::Unregister) => None,
2101        Some(OrdinaryPending::Reconcile(pending)) => Some(pending),
2102        Some(OrdinaryPending::Schedule(pending)) => match callback {
2103            Some(callback) if callback.deadline_ns < pending.deadline_ns => Some(callback),
2104            Some(_) | None => Some(pending),
2105        },
2106        None => callback,
2107    }
2108}
2109
2110const fn select_pending_ordinary(
2111    current: Option<OrdinaryPending>,
2112    request: OrdinaryRequest,
2113    requested: PendingSchedule,
2114) -> OrdinaryPending {
2115    if matches!(current, Some(OrdinaryPending::Unregister)) {
2116        return OrdinaryPending::Unregister;
2117    }
2118    match request {
2119        OrdinaryRequest::Reconcile => OrdinaryPending::Reconcile(requested),
2120        OrdinaryRequest::Ensure => match current {
2121            Some(OrdinaryPending::Reconcile(current) | OrdinaryPending::Schedule(current))
2122                if current.deadline_ns <= requested.deadline_ns =>
2123            {
2124                OrdinaryPending::Schedule(current)
2125            }
2126            Some(
2127                OrdinaryPending::Cancel
2128                | OrdinaryPending::Reconcile(_)
2129                | OrdinaryPending::Schedule(_),
2130            )
2131            | None => OrdinaryPending::Schedule(requested),
2132            Some(OrdinaryPending::Unregister) => OrdinaryPending::Unregister,
2133        },
2134    }
2135}
2136
2137fn cancel_watchdog(
2138    control: &mut WatchdogControl,
2139    identity: &TimerIdentity,
2140) -> (RegistryTransition, bool) {
2141    match control.state {
2142        WatchdogState::Inactive => (RegistryTransition::normal(RegistryEffect::None), false),
2143        WatchdogState::AwaitingWork {
2144            attempt_status: WatchdogAttemptStatus::Running,
2145            ..
2146        } => {
2147            let Some(sequence) = control.request_sequence.checked_add(1) else {
2148                control.state = WatchdogState::Inactive;
2149                control.inactive_reason =
2150                    InactiveReason::ControlFailure(TimerControlFailure::RequestSequenceExhausted);
2151                return (
2152                    RegistryTransition::terminal(
2153                        clear_callbacks(identity.clone(), true, false),
2154                        TimerControlFailure::RequestSequenceExhausted,
2155                    ),
2156                    true,
2157                );
2158            };
2159            control.request_sequence = sequence;
2160            if !matches!(control.pending, Some(WatchdogPending::Unregister)) {
2161                control.pending = Some(WatchdogPending::Cancel);
2162            }
2163            (RegistryTransition::normal(RegistryEffect::None), false)
2164        }
2165        WatchdogState::Scheduled { .. }
2166        | WatchdogState::AwaitingWork {
2167            attempt_status: WatchdogAttemptStatus::Dispatched,
2168            ..
2169        } => {
2170            let clear_work = matches!(control.state, WatchdogState::AwaitingWork { .. });
2171            let Some(scheduler_generation) = control.scheduler_generation.checked_add(1) else {
2172                control.state = WatchdogState::Inactive;
2173                control.inactive_reason =
2174                    InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
2175                return (
2176                    RegistryTransition::terminal(
2177                        clear_callbacks(identity.clone(), true, clear_work),
2178                        TimerControlFailure::GenerationExhausted,
2179                    ),
2180                    true,
2181                );
2182            };
2183            let Some(attempt_generation) = control.attempt_generation.checked_add(1) else {
2184                control.state = WatchdogState::Inactive;
2185                control.inactive_reason =
2186                    InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
2187                return (
2188                    RegistryTransition::terminal(
2189                        clear_callbacks(identity.clone(), true, clear_work),
2190                        TimerControlFailure::GenerationExhausted,
2191                    ),
2192                    true,
2193                );
2194            };
2195            control.scheduler_generation = scheduler_generation;
2196            control.attempt_generation = attempt_generation;
2197            control.state = WatchdogState::Inactive;
2198            control.pending = None;
2199            control.inactive_reason = InactiveReason::Cancelled;
2200            (
2201                RegistryTransition::normal(clear_callbacks(identity.clone(), true, clear_work)),
2202                true,
2203            )
2204        }
2205    }
2206}
2207
2208fn remove_after(
2209    entries: &mut BTreeMap<TimerIdentity, Entry>,
2210    identity: &TimerIdentity,
2211    transition: RegistryTransition,
2212    remove: bool,
2213) -> RegistryTransition {
2214    if remove {
2215        entries.remove(identity);
2216    }
2217    transition
2218}
2219
2220#[cfg(test)]
2221mod tests;