Skip to main content

ic_timers/registry/
mod.rs

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