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.running_work_entry(token)?;
1612        match &entry.callback {
1613            EntryCallback::Ordinary(callback) => Ok(Rc::clone(callback)),
1614            EntryCallback::Watchdog(_) => Err(RegistryError::MissingCallback),
1615            #[cfg(test)]
1616            EntryCallback::None => Err(RegistryError::MissingCallback),
1617        }
1618    }
1619
1620    pub(crate) fn watchdog_callback(
1621        &self,
1622        token: &CallbackToken,
1623    ) -> Result<WatchdogCallback, RegistryError> {
1624        let entry = self.running_work_entry(token)?;
1625        match &entry.callback {
1626            EntryCallback::Watchdog(callback) => Ok(Rc::clone(callback)),
1627            EntryCallback::Ordinary(_) => Err(RegistryError::MissingCallback),
1628            #[cfg(test)]
1629            EntryCallback::None => Err(RegistryError::MissingCallback),
1630        }
1631    }
1632
1633    pub(crate) fn validate_running_context(
1634        &self,
1635        token: &CallbackToken,
1636    ) -> Result<(), RegistryError> {
1637        self.running_work_entry(token).map(|_| ())
1638    }
1639
1640    pub(crate) fn install_provider_handle(
1641        &mut self,
1642        token: &CallbackToken,
1643        handle: TimerHandle,
1644    ) -> Result<(), (RegistryError, TimerHandle)> {
1645        let entry = match self.entry_by_token_mut(token, token.role) {
1646            Ok(entry) => entry,
1647            Err(error) => return Err((error, handle)),
1648        };
1649        let valid = match (&entry.control, token.role) {
1650            (EntryControl::Ordinary { control, .. }, CallbackRole::OrdinaryWork) => matches!(
1651                control.registration(),
1652                crate::TimerRegistration::Scheduled { generation, .. }
1653                    if generation == token.callback_generation
1654            ),
1655            (EntryControl::Watchdog(control), CallbackRole::WatchdogScheduler) => {
1656                matches!(
1657                    control.state,
1658                    WatchdogState::Scheduled {
1659                        scheduler_generation,
1660                        ..
1661                    } if scheduler_generation == token.callback_generation
1662                ) || matches!(
1663                    control.state,
1664                    WatchdogState::AwaitingWork {
1665                        successor_generation,
1666                        ..
1667                    } if successor_generation == token.callback_generation
1668                )
1669            }
1670            (EntryControl::Watchdog(control), CallbackRole::WatchdogWork) => matches!(
1671                control.state,
1672                WatchdogState::AwaitingWork {
1673                    attempt_generation,
1674                    attempt_status: WatchdogAttemptStatus::Dispatched,
1675                    ..
1676                } if attempt_generation == token.callback_generation
1677            ),
1678            (
1679                EntryControl::Ordinary { .. },
1680                CallbackRole::WatchdogScheduler | CallbackRole::WatchdogWork,
1681            )
1682            | (EntryControl::Watchdog(_), CallbackRole::OrdinaryWork) => false,
1683        };
1684        if !valid {
1685            return Err((RegistryError::StaleCallback, handle));
1686        }
1687        let slot = match token.role {
1688            CallbackRole::OrdinaryWork | CallbackRole::WatchdogScheduler => &mut entry.wakeup,
1689            CallbackRole::WatchdogWork => &mut entry.work,
1690        };
1691        if slot.is_some() {
1692            return Err((RegistryError::ProviderHandleAlreadyOwned, handle));
1693        }
1694        *slot = Some(OwnedProviderHandle {
1695            callback_generation: token.callback_generation,
1696            role: token.role,
1697            handle,
1698        });
1699        Ok(())
1700    }
1701
1702    pub(crate) fn take_wakeup_handle(
1703        &mut self,
1704        identity: &TimerIdentity,
1705    ) -> Option<ProviderHandle> {
1706        let entry = self.entries.get_mut(identity)?;
1707        let owned = entry.wakeup.take()?;
1708        Some(detach_provider_handle(
1709            identity,
1710            entry.claim_generation,
1711            owned,
1712        ))
1713    }
1714
1715    pub(crate) fn take_work_handle(&mut self, identity: &TimerIdentity) -> Option<ProviderHandle> {
1716        let entry = self.entries.get_mut(identity)?;
1717        let owned = entry.work.take()?;
1718        Some(detach_provider_handle(
1719            identity,
1720            entry.claim_generation,
1721            owned,
1722        ))
1723    }
1724
1725    pub(crate) fn take_provider_handles_for_claim(
1726        &mut self,
1727        claim: &RegistrationClaim,
1728    ) -> Result<ProviderHandles, RegistryError> {
1729        let identity = claim.identity.clone();
1730        let entry = self.entry_mut(claim)?;
1731        Ok(ProviderHandles {
1732            wakeup: entry
1733                .wakeup
1734                .take()
1735                .map(|owned| detach_provider_handle(&identity, entry.claim_generation, owned)),
1736            work: entry
1737                .work
1738                .take()
1739                .map(|owned| detach_provider_handle(&identity, entry.claim_generation, owned)),
1740        })
1741    }
1742
1743    pub(crate) fn consume_provider_handle(&mut self, token: &CallbackToken) {
1744        let Some(entry) = self.entries.get_mut(token.identity()) else {
1745            return;
1746        };
1747        let slot = match token.role {
1748            CallbackRole::OrdinaryWork | CallbackRole::WatchdogScheduler => &mut entry.wakeup,
1749            CallbackRole::WatchdogWork => &mut entry.work,
1750        };
1751        let matches_token = slot.as_ref().is_some_and(|owned| {
1752            owned.callback_generation == token.callback_generation && owned.role == token.role
1753        });
1754        if matches_token {
1755            *slot = None;
1756        }
1757    }
1758
1759    /// Confirm that the platform successfully applied one emitted arm effect.
1760    ///
1761    /// Patch 3 calls this synchronously after owning the returned provider
1762    /// handles. Pure tests use it to distinguish requested effects from actual
1763    /// provider operations.
1764    pub(crate) fn confirm_effect_applied(
1765        &mut self,
1766        effect: &RegistryEffect,
1767    ) -> Result<(), RegistryError> {
1768        match effect {
1769            RegistryEffect::None | RegistryEffect::ClearCallbacks { .. } => Ok(()),
1770            RegistryEffect::ArmWakeup {
1771                token, delay_ns, ..
1772            } => {
1773                let entry = self.entry_by_token_mut(token, token.role)?;
1774                let valid_generation = match (&entry.control, token.role) {
1775                    (EntryControl::Ordinary { control, .. }, CallbackRole::OrdinaryWork) => {
1776                        matches!(
1777                            control.registration(),
1778                            crate::TimerRegistration::Scheduled { generation, .. }
1779                                if generation == token.callback_generation
1780                        )
1781                    }
1782                    (EntryControl::Watchdog(control), CallbackRole::WatchdogScheduler) => matches!(
1783                        control.state,
1784                        WatchdogState::Scheduled {
1785                            scheduler_generation,
1786                            ..
1787                        } if scheduler_generation == token.callback_generation
1788                    ),
1789                    (EntryControl::Ordinary { .. } | EntryControl::Watchdog(_), _) => false,
1790                };
1791                if !valid_generation {
1792                    return Err(RegistryError::StaleCallback);
1793                }
1794                if entry.confirmed_wakeup_generation == Some(token.callback_generation) {
1795                    return Ok(());
1796                }
1797                entry.confirmed_wakeup_generation = Some(token.callback_generation);
1798                entry.latest_armed_delay_ns = Some(*delay_ns);
1799                entry.observability.counters_mut().record_wakeup_armed();
1800                Ok(())
1801            }
1802            RegistryEffect::DispatchWatchdog {
1803                successor,
1804                successor_delay_ns,
1805                work,
1806                ..
1807            } => {
1808                let successor_claim_generation = successor.claim_generation;
1809                let successor_callback_generation = successor.callback_generation;
1810                let successor_role = successor.role;
1811                let entry = self.entry_by_token_mut(successor, successor_role)?;
1812                if successor_role != CallbackRole::WatchdogScheduler
1813                    || work.role != CallbackRole::WatchdogWork
1814                    || work.claim_generation != successor_claim_generation
1815                {
1816                    return Err(RegistryError::StaleCallback);
1817                }
1818                let EntryControl::Watchdog(control) = &entry.control else {
1819                    return Err(RegistryError::StaleCallback);
1820                };
1821                if !matches!(
1822                    control.state,
1823                    WatchdogState::AwaitingWork {
1824                        successor_generation,
1825                        attempt_generation,
1826                        ..
1827                    } if successor_generation == successor_callback_generation
1828                        && attempt_generation == work.callback_generation
1829                ) {
1830                    return Err(RegistryError::StaleCallback);
1831                }
1832                if entry.confirmed_wakeup_generation == Some(successor_callback_generation)
1833                    && entry.confirmed_work_generation == Some(work.callback_generation)
1834                {
1835                    return Ok(());
1836                }
1837                entry.confirmed_wakeup_generation = Some(successor_callback_generation);
1838                entry.confirmed_work_generation = Some(work.callback_generation);
1839                entry.latest_armed_delay_ns = Some(*successor_delay_ns);
1840                entry.observability.counters_mut().record_wakeup_armed();
1841                entry.observability.counters_mut().record_work_dispatched();
1842                Ok(())
1843            }
1844        }
1845    }
1846
1847    fn entry(&self, claim: &RegistrationClaim) -> Result<&Entry, RegistryError> {
1848        let entry = self
1849            .entries
1850            .get(claim.identity())
1851            .ok_or(RegistryError::UnknownRegistration)?;
1852        if entry.claim_generation != claim.claim_generation() {
1853            return Err(RegistryError::StaleRegistration);
1854        }
1855        Ok(entry)
1856    }
1857
1858    fn entry_mut(&mut self, claim: &RegistrationClaim) -> Result<&mut Entry, RegistryError> {
1859        let entry = self
1860            .entries
1861            .get_mut(claim.identity())
1862            .ok_or(RegistryError::UnknownRegistration)?;
1863        if entry.claim_generation != claim.claim_generation() {
1864            return Err(RegistryError::StaleRegistration);
1865        }
1866        Ok(entry)
1867    }
1868
1869    fn entry_by_token_mut(
1870        &mut self,
1871        token: &CallbackToken,
1872        role: CallbackRole,
1873    ) -> Result<&mut Entry, RegistryError> {
1874        let entry = self
1875            .entries
1876            .get_mut(token.identity())
1877            .ok_or(RegistryError::StaleCallback)?;
1878        if entry.claim_generation != token.claim_generation || token.role != role {
1879            return Err(RegistryError::StaleCallback);
1880        }
1881        Ok(entry)
1882    }
1883
1884    fn running_work_entry(&self, token: &CallbackToken) -> Result<&Entry, RegistryError> {
1885        let entry = self
1886            .entries
1887            .get(token.identity())
1888            .ok_or(RegistryError::StaleCallback)?;
1889        if token.claim_generation != entry.claim_generation {
1890            return Err(RegistryError::StaleCallback);
1891        }
1892        let active = match (&entry.control, token.role) {
1893            (EntryControl::Ordinary { control, .. }, CallbackRole::OrdinaryWork) => matches!(
1894                control.registration(),
1895                crate::TimerRegistration::Running { generation }
1896                    if generation == token.callback_generation
1897            ),
1898            (EntryControl::Watchdog(control), CallbackRole::WatchdogWork) => matches!(
1899                control.state,
1900                WatchdogState::AwaitingWork {
1901                    attempt_generation,
1902                    attempt_status: WatchdogAttemptStatus::Running,
1903                    ..
1904                } if attempt_generation == token.callback_generation
1905            ),
1906            (
1907                EntryControl::Ordinary { .. },
1908                CallbackRole::WatchdogScheduler | CallbackRole::WatchdogWork,
1909            )
1910            | (
1911                EntryControl::Watchdog(_),
1912                CallbackRole::OrdinaryWork | CallbackRole::WatchdogScheduler,
1913            ) => false,
1914        };
1915        if active {
1916            Ok(entry)
1917        } else {
1918            Err(RegistryError::StaleCallback)
1919        }
1920    }
1921}
1922
1923fn token_for(
1924    claim: &RegistrationClaim,
1925    callback_generation: u64,
1926    role: CallbackRole,
1927) -> CallbackToken {
1928    CallbackToken::new(
1929        claim.identity.clone(),
1930        claim.claim_generation,
1931        callback_generation,
1932        role,
1933    )
1934}
1935
1936fn detach_provider_handle(
1937    identity: &TimerIdentity,
1938    claim_generation: u64,
1939    owned: OwnedProviderHandle,
1940) -> ProviderHandle {
1941    ProviderHandle {
1942        token: CallbackToken::new(
1943            identity.clone(),
1944            claim_generation,
1945            owned.callback_generation,
1946            owned.role,
1947        ),
1948        handle: owned.handle,
1949    }
1950}
1951
1952const fn clear_callbacks(
1953    identity: TimerIdentity,
1954    clear_wakeup: bool,
1955    clear_work: bool,
1956) -> RegistryEffect {
1957    RegistryEffect::ClearCallbacks {
1958        identity,
1959        clear_wakeup,
1960        clear_work,
1961    }
1962}
1963
1964fn apply_ordinary_action(
1965    entry: &mut Entry,
1966    identity: TimerIdentity,
1967    now_ns: u64,
1968    action: TimerControlAction,
1969    requested: PendingSchedule,
1970) -> RegistryTransition {
1971    match action {
1972        TimerControlAction::Arm {
1973            generation,
1974            deadline_ns,
1975        }
1976        | TimerControlAction::Replace {
1977            generation,
1978            deadline_ns,
1979        } => {
1980            let replace = matches!(action, TimerControlAction::Replace { .. });
1981            entry.scheduling_mode = requested.mode;
1982            RegistryTransition::normal(RegistryEffect::ArmWakeup {
1983                token: CallbackToken::new(
1984                    identity,
1985                    entry.claim_generation,
1986                    generation,
1987                    CallbackRole::OrdinaryWork,
1988                ),
1989                deadline_ns,
1990                delay_ns: deadline_ns.saturating_sub(now_ns),
1991                replace,
1992            })
1993        }
1994        TimerControlAction::None => {
1995            entry.observability.counters_mut().record_coalesced();
1996            RegistryTransition::normal(RegistryEffect::None)
1997        }
1998        TimerControlAction::Clear | TimerControlAction::Disarm { .. } => {
1999            terminal_ordinary(entry, identity, TimerControlError::StaleCompletion)
2000        }
2001    }
2002}
2003
2004fn terminal_ordinary(
2005    entry: &mut Entry,
2006    identity: TimerIdentity,
2007    error: TimerControlError,
2008) -> RegistryTransition {
2009    let failure = map_control_failure(error);
2010    let EntryControl::Ordinary {
2011        control,
2012        pending,
2013        inactive_reason,
2014    } = &mut entry.control
2015    else {
2016        return RegistryTransition::terminal(RegistryEffect::None, failure);
2017    };
2018    let clear_wakeup = control.terminate();
2019    *pending = None;
2020    *inactive_reason = InactiveReason::ControlFailure(failure);
2021    RegistryTransition::terminal(
2022        RegistryEffect::ClearCallbacks {
2023            identity,
2024            clear_wakeup,
2025            clear_work: false,
2026        },
2027        failure,
2028    )
2029}
2030
2031fn terminal_completion(
2032    entry: &mut Entry,
2033    generation: u64,
2034    work_count: u64,
2035    now_ns: u64,
2036    failure: TimerControlFailure,
2037) -> RegistryTransition {
2038    let EntryControl::Ordinary {
2039        control,
2040        pending,
2041        inactive_reason,
2042    } = &mut entry.control
2043    else {
2044        return RegistryTransition::terminal(RegistryEffect::None, failure);
2045    };
2046    if control.registration() == (crate::TimerRegistration::Running { generation }) {
2047        control.terminate();
2048    }
2049    *pending = None;
2050    *inactive_reason = InactiveReason::ControlFailure(failure);
2051    entry.latest_directive = Some(TimerDirectiveSnapshot::Stop);
2052    entry
2053        .observability
2054        .record_completion(TimerCompletion::invariant_failure(work_count), now_ns);
2055    RegistryTransition::terminal(RegistryEffect::None, failure)
2056}
2057
2058fn invariant_completion(
2059    entry: &mut Entry,
2060    generation: u64,
2061    completion: TimerCompletion,
2062    now_ns: u64,
2063) -> RegistryTransition {
2064    let EntryControl::Ordinary {
2065        control,
2066        pending,
2067        inactive_reason,
2068    } = &mut entry.control
2069    else {
2070        return RegistryTransition::normal(RegistryEffect::None);
2071    };
2072    if control.registration() == (crate::TimerRegistration::Running { generation }) {
2073        control.terminate();
2074    }
2075    *pending = None;
2076    *inactive_reason = InactiveReason::InvariantFailure;
2077    entry.latest_directive = Some(TimerDirectiveSnapshot::Stop);
2078    entry.observability.record_completion(completion, now_ns);
2079    RegistryTransition::normal(RegistryEffect::None)
2080}
2081
2082const fn map_control_failure(error: TimerControlError) -> TimerControlFailure {
2083    match error {
2084        TimerControlError::RequestSequenceExhausted => {
2085            TimerControlFailure::RequestSequenceExhausted
2086        }
2087        TimerControlError::GenerationExhausted => TimerControlFailure::GenerationExhausted,
2088        TimerControlError::StaleCompletion => TimerControlFailure::DirectiveNotAllowed,
2089    }
2090}
2091
2092const fn map_schedule_failure(error: ScheduleError) -> TimerControlFailure {
2093    match error {
2094        ScheduleError::DeadlineOverflow => TimerControlFailure::DeadlineOverflow,
2095        ScheduleError::DelayOutOfRange => TimerControlFailure::DelayOutOfRange,
2096        ScheduleError::ZeroCadence | ScheduleError::MissingCadence => {
2097            TimerControlFailure::DirectiveNotAllowed
2098        }
2099    }
2100}
2101
2102const fn select_completion_schedule(
2103    pending: Option<OrdinaryPending>,
2104    callback: Option<PendingSchedule>,
2105) -> Option<PendingSchedule> {
2106    match pending {
2107        Some(OrdinaryPending::Cancel | OrdinaryPending::Unregister) => None,
2108        Some(OrdinaryPending::Reconcile(pending)) => Some(pending),
2109        Some(OrdinaryPending::Schedule(pending)) => match callback {
2110            Some(callback) if callback.deadline_ns < pending.deadline_ns => Some(callback),
2111            Some(_) | None => Some(pending),
2112        },
2113        None => callback,
2114    }
2115}
2116
2117const fn select_pending_ordinary(
2118    current: Option<OrdinaryPending>,
2119    request: OrdinaryRequest,
2120    requested: PendingSchedule,
2121) -> OrdinaryPending {
2122    if matches!(current, Some(OrdinaryPending::Unregister)) {
2123        return OrdinaryPending::Unregister;
2124    }
2125    match request {
2126        OrdinaryRequest::Reconcile => OrdinaryPending::Reconcile(requested),
2127        OrdinaryRequest::Ensure => match current {
2128            Some(OrdinaryPending::Reconcile(current) | OrdinaryPending::Schedule(current))
2129                if current.deadline_ns <= requested.deadline_ns =>
2130            {
2131                OrdinaryPending::Schedule(current)
2132            }
2133            Some(
2134                OrdinaryPending::Cancel
2135                | OrdinaryPending::Reconcile(_)
2136                | OrdinaryPending::Schedule(_),
2137            )
2138            | None => OrdinaryPending::Schedule(requested),
2139            Some(OrdinaryPending::Unregister) => OrdinaryPending::Unregister,
2140        },
2141    }
2142}
2143
2144fn cancel_watchdog(
2145    control: &mut WatchdogControl,
2146    identity: &TimerIdentity,
2147) -> (RegistryTransition, bool) {
2148    match control.state {
2149        WatchdogState::Inactive => (RegistryTransition::normal(RegistryEffect::None), false),
2150        WatchdogState::AwaitingWork {
2151            attempt_status: WatchdogAttemptStatus::Running,
2152            ..
2153        } => {
2154            let Some(sequence) = control.request_sequence.checked_add(1) else {
2155                control.state = WatchdogState::Inactive;
2156                control.inactive_reason =
2157                    InactiveReason::ControlFailure(TimerControlFailure::RequestSequenceExhausted);
2158                return (
2159                    RegistryTransition::terminal(
2160                        clear_callbacks(identity.clone(), true, false),
2161                        TimerControlFailure::RequestSequenceExhausted,
2162                    ),
2163                    true,
2164                );
2165            };
2166            control.request_sequence = sequence;
2167            if !matches!(control.pending, Some(WatchdogPending::Unregister)) {
2168                control.pending = Some(WatchdogPending::Cancel);
2169            }
2170            (RegistryTransition::normal(RegistryEffect::None), false)
2171        }
2172        WatchdogState::Scheduled { .. }
2173        | WatchdogState::AwaitingWork {
2174            attempt_status: WatchdogAttemptStatus::Dispatched,
2175            ..
2176        } => {
2177            let clear_work = matches!(control.state, WatchdogState::AwaitingWork { .. });
2178            let Some(scheduler_generation) = control.scheduler_generation.checked_add(1) else {
2179                control.state = WatchdogState::Inactive;
2180                control.inactive_reason =
2181                    InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
2182                return (
2183                    RegistryTransition::terminal(
2184                        clear_callbacks(identity.clone(), true, clear_work),
2185                        TimerControlFailure::GenerationExhausted,
2186                    ),
2187                    true,
2188                );
2189            };
2190            let Some(attempt_generation) = control.attempt_generation.checked_add(1) else {
2191                control.state = WatchdogState::Inactive;
2192                control.inactive_reason =
2193                    InactiveReason::ControlFailure(TimerControlFailure::GenerationExhausted);
2194                return (
2195                    RegistryTransition::terminal(
2196                        clear_callbacks(identity.clone(), true, clear_work),
2197                        TimerControlFailure::GenerationExhausted,
2198                    ),
2199                    true,
2200                );
2201            };
2202            control.scheduler_generation = scheduler_generation;
2203            control.attempt_generation = attempt_generation;
2204            control.state = WatchdogState::Inactive;
2205            control.pending = None;
2206            control.inactive_reason = InactiveReason::Cancelled;
2207            (
2208                RegistryTransition::normal(clear_callbacks(identity.clone(), true, clear_work)),
2209                true,
2210            )
2211        }
2212    }
2213}
2214
2215fn remove_after(
2216    entries: &mut BTreeMap<TimerIdentity, Entry>,
2217    identity: &TimerIdentity,
2218    transition: RegistryTransition,
2219    remove: bool,
2220) -> RegistryTransition {
2221    if remove {
2222        entries.remove(identity);
2223    }
2224    transition
2225}
2226
2227#[cfg(test)]
2228mod tests;