Skip to main content

ic_timers/runtime/
mod.rs

1//! Canister-local owner and live timer execution.
2
3use crate::{
4    platform::{self, TimerHandle},
5    registry::{
6        CallbackAcceptance, CallbackRole, CallbackToken, OrdinaryCallback, ProviderHandle,
7        ProviderHandles, RegisterError, RegistrationClaim, RegistryEffect, RegistryError,
8        RegistryTransition, TimerRegistry, WatchdogCallback,
9    },
10    schedule::{ScheduleError, TimerCadence, TimerDirective, TimerSchedule},
11    snapshot::{
12        DeclarationLifetime, TimerCompletion, TimerControlFailure, TimerEpoch, TimerIdentity,
13        TimerInventorySnapshot, TimerPolicy, TimerRunResult, TimerSnapshot, WatchdogRunResult,
14    },
15};
16use std::{cell::RefCell, future::Future, rc::Rc, time::Duration};
17use thiserror::Error;
18
19thread_local! {
20    static RUNTIME: RefCell<Option<TimerRegistry>> = const { RefCell::new(None) };
21}
22
23/// Failure from the canister-local timer runtime API.
24#[non_exhaustive]
25#[derive(Debug, Error)]
26pub enum TimerError {
27    /// The lifecycle owner has not initialized the volatile runtime.
28    #[error("timer runtime is not initialized")]
29    NotInitialized,
30    /// A nested internal borrow indicates unsupported re-entrancy.
31    #[error("timer runtime is already borrowed")]
32    RuntimeBusy,
33    /// Claiming one canonical timer identity failed.
34    #[error(transparent)]
35    Register(#[from] RegisterError),
36    /// Cadence or deadline validation failed.
37    #[error(transparent)]
38    Schedule(#[from] ScheduleError),
39    /// The logical registration was removed or superseded.
40    #[error("timer registration is no longer authoritative")]
41    RegistrationExpired,
42    /// Pure checked control reached a terminal failure after effects were applied.
43    #[error("timer control failed: {0:?}")]
44    ControlFailure(TimerControlFailure),
45    /// Canonical callback or provider-handle ownership was internally inconsistent.
46    #[error("timer runtime ownership invariant failed")]
47    OwnershipInvariant,
48    /// A retained lifecycle claim no longer matches its canonical declaration.
49    #[error("timer lifecycle reconciliation conflicts with the canonical declaration")]
50    ReconciliationConflict,
51}
52
53impl From<RegistryError> for TimerError {
54    fn from(value: RegistryError) -> Self {
55        match value {
56            RegistryError::UnknownRegistration
57            | RegistryError::StaleRegistration
58            | RegistryError::StaleCallback => Self::RegistrationExpired,
59            RegistryError::PolicyMismatch { .. } => Self::OwnershipInvariant,
60            RegistryError::Schedule(error) => Self::Schedule(error),
61            RegistryError::MissingCallback | RegistryError::ProviderHandleAlreadyOwned => {
62                Self::OwnershipInvariant
63            }
64        }
65    }
66}
67
68/// Initialize the volatile canister-local runtime once for this Wasm instance.
69///
70/// Repeated calls are idempotent and return the original epoch. This function
71/// exports no lifecycle hook; the canister's existing lifecycle owner calls it.
72pub fn initialize_runtime() -> Result<TimerEpoch, TimerError> {
73    let epoch = TimerEpoch::new(platform::canister_version(), platform::time_ns());
74    RUNTIME.with(|runtime| {
75        let mut runtime = runtime
76            .try_borrow_mut()
77            .map_err(|_| TimerError::RuntimeBusy)?;
78        if let Some(registry) = runtime.as_ref() {
79            return Ok(registry.epoch());
80        }
81        *runtime = Some(TimerRegistry::new(epoch));
82        Ok(epoch)
83    })
84}
85
86struct CallbackContext {
87    token: CallbackToken,
88}
89
90impl CallbackContext {
91    const fn new(token: CallbackToken) -> Self {
92        Self { token }
93    }
94
95    fn claim(&self) -> RegistrationClaim {
96        RegistrationClaim::from_callback(&self.token)
97    }
98
99    const fn identity(&self) -> &TimerIdentity {
100        self.token.identity()
101    }
102
103    fn cancel(&self) -> Result<(), TimerError> {
104        cancel_claim(&self.claim(), Some(&self.token))
105    }
106
107    fn schedule_once(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
108        ensure_once_claim(&self.claim(), Some(&self.token), schedule)
109    }
110
111    fn schedule_recurring(&self) -> Result<(), TimerError> {
112        ensure_recurring_claim(&self.claim(), Some(&self.token))
113    }
114
115    fn schedule_watchdog_immediately(&self) -> Result<(), TimerError> {
116        ensure_watchdog_immediately_claim(&self.claim(), Some(&self.token))
117    }
118
119    fn reconcile_ordinary(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
120        reconcile_ordinary_claim(&self.claim(), Some(&self.token), schedule)
121    }
122}
123
124/// Delegated control capability scoped to one exact `Once` work attempt.
125///
126/// Identity remains inspectable after work returns, but mutation methods then
127/// return [`TimerError::RegistrationExpired`]. Retain the
128/// [`OnceRegistration`] for longer-lived ownership.
129pub struct OnceContext {
130    inner: CallbackContext,
131}
132
133impl OnceContext {
134    const fn new(token: CallbackToken) -> Self {
135        Self {
136            inner: CallbackContext::new(token),
137        }
138    }
139
140    /// Return the logical timer identity executing this work.
141    #[must_use]
142    pub const fn identity(&self) -> &TimerIdentity {
143        self.inner.identity()
144    }
145
146    /// Schedule a `Once` declaration while this exact work attempt is active.
147    ///
148    /// A context retained after its callback completes is expired and returns
149    /// [`TimerError::RegistrationExpired`].
150    pub fn ensure_scheduled(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
151        self.inner.schedule_once(schedule)
152    }
153
154    /// Reconcile the executing declaration to one exact schedule.
155    ///
156    /// `None` requests inactive state at normal completion. Retained callback
157    /// authority remains; a remove-on-stop declaration is removed when that
158    /// cancellation wins arbitration. A stored context cannot mutate the
159    /// registration after its exact work attempt ends.
160    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
161        self.inner.reconcile_ordinary(schedule)
162    }
163
164    /// Request cancellation while this exact work attempt is active.
165    ///
166    /// Cancellation does not interrupt the current invocation; normal
167    /// completion applies it before any callback successor is retained.
168    /// A retained declaration becomes inactive. A remove-on-stop declaration
169    /// is removed when cancellation wins arbitration.
170    pub fn cancel(&self) -> Result<(), TimerError> {
171        self.inner.cancel()
172    }
173}
174
175/// Delegated control capability scoped to one exact after-completion work
176/// attempt.
177///
178/// Mutation authority expires when the callback completes. Retain the
179/// [`AfterCompletionRegistration`] for longer-lived ownership.
180pub struct AfterCompletionContext {
181    inner: CallbackContext,
182}
183
184impl AfterCompletionContext {
185    const fn new(token: CallbackToken) -> Self {
186        Self {
187            inner: CallbackContext::new(token),
188        }
189    }
190
191    /// Return the logical timer identity executing this work.
192    #[must_use]
193    pub const fn identity(&self) -> &TimerIdentity {
194        self.inner.identity()
195    }
196
197    /// Request configured recurrence after this exact work attempt.
198    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
199        self.inner.schedule_recurring()
200    }
201
202    /// Reconcile the executing declaration to one exact schedule without
203    /// changing its configured after-completion cadence.
204    ///
205    /// `None` requests inactive state at normal completion. A retained
206    /// declaration keeps its callback authority; a remove-on-stop declaration
207    /// is removed when cancellation wins arbitration. A stored context cannot
208    /// mutate the registration after its exact work attempt ends.
209    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
210        self.inner.reconcile_ordinary(schedule)
211    }
212
213    /// Request cancellation while this exact work attempt is active.
214    ///
215    /// Cancellation does not interrupt the current invocation; normal
216    /// completion applies it before any callback successor is retained. A
217    /// retained declaration becomes inactive; a remove-on-stop declaration is
218    /// removed when cancellation wins arbitration.
219    pub fn cancel(&self) -> Result<(), TimerError> {
220        self.inner.cancel()
221    }
222}
223
224/// Delegated control capability scoped to one exact watchdog work attempt.
225///
226/// Mutation authority expires when the callback completes. Retain the
227/// [`WatchdogRegistration`] for longer-lived ownership.
228pub struct WatchdogContext {
229    inner: CallbackContext,
230}
231
232impl WatchdogContext {
233    const fn new(token: CallbackToken) -> Self {
234        Self {
235            inner: CallbackContext::new(token),
236        }
237    }
238
239    /// Return the logical timer identity executing this work.
240    #[must_use]
241    pub const fn identity(&self) -> &TimerIdentity {
242        self.inner.identity()
243    }
244
245    /// Request that the pre-armed cadence successor remain scheduled.
246    ///
247    /// This is idempotent unless it supersedes a nested cancellation request.
248    /// It never arms an additional Watchdog successor and cannot move a
249    /// pending immediate successor later.
250    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
251        self.inner.schedule_recurring()
252    }
253
254    /// Request that this work attempt's pre-armed successor run immediately.
255    ///
256    /// Repeated requests coalesce, and a cadence ensure cannot move the
257    /// pending immediate deadline later. Normal completion replaces the exact
258    /// cadence successor with a zero-delay scheduler callback. Consumer work
259    /// still runs only in the scheduler's separately queued later message.
260    pub fn ensure_scheduled_immediately(&self) -> Result<(), TimerError> {
261        self.inner.schedule_watchdog_immediately()
262    }
263
264    /// Request cancellation while this exact work attempt is active.
265    ///
266    /// Cancellation does not interrupt the current invocation; normal
267    /// completion clears the already-armed successor when cancellation wins.
268    /// A retained declaration becomes inactive; a remove-on-stop declaration
269    /// is removed.
270    pub fn cancel(&self) -> Result<(), TimerError> {
271        self.inner.cancel()
272    }
273}
274
275/// Opaque non-clone claim for one registered `Once` callback.
276#[must_use = "retain the registration claim so the timer remains controllable"]
277pub struct OnceRegistration {
278    claim: RegistrationClaim,
279}
280
281impl OnceRegistration {
282    /// Return the claimed logical identity.
283    #[must_use]
284    pub const fn identity(&self) -> &TimerIdentity {
285        self.claim.identity()
286    }
287
288    /// Return whether this exact claim currently owns an armed provider wake-up.
289    ///
290    /// This is a volatile observation, not durable scheduling authority or a
291    /// delivery guarantee. Call [`Self::ensure_scheduled`] unconditionally when
292    /// a wake-up is required rather than using this value as a scheduling guard.
293    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
294        has_armed_wakeup_claim(&self.claim)
295    }
296
297    /// Ensure one invocation is armed or retained as the running work's successor.
298    pub fn ensure_scheduled(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
299        ensure_once_claim(&self.claim, None, schedule)
300    }
301
302    /// Reconcile to one exact desired schedule, replacing a later or earlier
303    /// live deadline as necessary.
304    ///
305    /// `None` leaves a retained declaration inactive after any running work
306    /// completes. A remove-on-stop declaration is removed and this claim
307    /// expires when the transition finalizes.
308    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
309        reconcile_ordinary_claim(&self.claim, None, schedule)
310    }
311
312    /// Cancel the armed callback or the running work's successor.
313    ///
314    /// Consumer work already running is not interrupted.
315    /// A retained declaration keeps callback authority. A remove-on-stop
316    /// declaration and this claim expire when cancellation finalizes.
317    pub fn cancel(&self) -> Result<(), TimerError> {
318        cancel_claim(&self.claim, None)
319    }
320
321    /// Consume the claim and unregister its callback authority.
322    ///
323    /// When called from running work, removal is deferred until that invocation
324    /// completes normally.
325    pub fn unregister(self) -> Result<(), TimerError> {
326        unregister_claim(&self.claim)
327    }
328}
329
330/// Opaque non-clone claim for one callback with configured
331/// after-completion recurrence.
332#[must_use = "retain the registration claim so the timer remains controllable"]
333pub struct AfterCompletionRegistration {
334    claim: RegistrationClaim,
335}
336
337/// Opaque non-clone claim for one pre-armed watchdog callback.
338#[must_use = "retain the registration claim so the timer remains controllable"]
339pub struct WatchdogRegistration {
340    claim: RegistrationClaim,
341}
342
343trait RegistrationClaimOwner {
344    fn registration_claim(&self) -> &RegistrationClaim;
345}
346
347impl RegistrationClaimOwner for OnceRegistration {
348    fn registration_claim(&self) -> &RegistrationClaim {
349        &self.claim
350    }
351}
352
353impl RegistrationClaimOwner for AfterCompletionRegistration {
354    fn registration_claim(&self) -> &RegistrationClaim {
355        &self.claim
356    }
357}
358
359impl RegistrationClaimOwner for WatchdogRegistration {
360    fn registration_claim(&self) -> &RegistrationClaim {
361        &self.claim
362    }
363}
364
365/// Desired volatile after-completion state during lifecycle reconciliation.
366#[derive(Clone, Copy, Debug, Eq, PartialEq)]
367pub enum TimerReconcileState {
368    /// Request inactive state immediately unless consumer work is running.
369    Inactive,
370    /// Preserve scheduling demand now or through the running work's successor.
371    Scheduled,
372}
373
374/// Desired volatile watchdog state during synchronous lifecycle reconciliation.
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub enum WatchdogReconcileState {
377    /// Request inactive state immediately unless consumer work is running.
378    Inactive,
379    /// Preserve or arm one scheduler wake-up at the configured cadence.
380    Scheduled,
381    /// Preserve an earlier wake-up or arm/move one scheduler wake-up to now.
382    ///
383    /// The zero-delay provider callback executes as a later replicated
384    /// scheduler message and does not invoke consumer work synchronously.
385    ScheduledImmediately,
386}
387
388impl WatchdogRegistration {
389    /// Return the claimed logical identity.
390    #[must_use]
391    pub const fn identity(&self) -> &TimerIdentity {
392        self.claim.identity()
393    }
394
395    /// Return whether this exact claim currently owns an armed scheduler wake-up.
396    ///
397    /// The separately queued work callback is not itself a wake-up. During
398    /// watchdog work this returns `true` only because the scheduler has already
399    /// committed and installed the cadence successor. This is a volatile
400    /// observation, not a delivery guarantee.
401    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
402        has_armed_wakeup_claim(&self.claim)
403    }
404
405    /// Synchronously ensure one watchdog scheduler wake-up is authoritative.
406    ///
407    /// An existing earlier immediate deadline is retained and never delayed.
408    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
409        ensure_recurring_claim(&self.claim, None)
410    }
411
412    /// Synchronously ensure one watchdog scheduler wake-up is due now.
413    ///
414    /// An inactive declaration arms one zero-delay scheduler. A later cadence
415    /// deadline is replaced in place; an already earlier or equivalent
416    /// deadline and repeated immediate requests coalesce. If work is already
417    /// dispatched, that zero-delay work satisfies the request. If work is
418    /// running, the request applies to that attempt's pre-armed successor.
419    pub fn ensure_scheduled_immediately(&self) -> Result<(), TimerError> {
420        ensure_watchdog_immediately_claim(&self.claim, None)
421    }
422
423    /// Cancel the scheduler and any work callback that has not started.
424    ///
425    /// Consumer work already running is not interrupted; normal completion
426    /// clears its pre-armed successor.
427    /// A retained declaration keeps callback authority. A remove-on-stop
428    /// declaration and this claim expire when cancellation finalizes.
429    pub fn cancel(&self) -> Result<(), TimerError> {
430        cancel_claim(&self.claim, None)
431    }
432
433    /// Consume the claim and unregister its callback authority.
434    ///
435    /// When called from running work, removal is deferred until that invocation
436    /// completes normally.
437    pub fn unregister(self) -> Result<(), TimerError> {
438        unregister_claim(&self.claim)
439    }
440}
441
442impl AfterCompletionRegistration {
443    /// Return the claimed logical identity.
444    #[must_use]
445    pub const fn identity(&self) -> &TimerIdentity {
446        self.claim.identity()
447    }
448
449    /// Return whether this exact claim currently owns an armed provider wake-up.
450    ///
451    /// A callback currently running without an installed successor returns
452    /// `false`. This is a volatile observation, not durable scheduling authority
453    /// or a delivery guarantee.
454    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
455        has_armed_wakeup_claim(&self.claim)
456    }
457
458    /// Ensure configured recurrence is armed or retained as the running work's
459    /// successor.
460    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
461        ensure_recurring_claim(&self.claim, None)
462    }
463
464    /// Reconcile to one exact desired schedule without changing the configured
465    /// after-completion cadence. `None` makes a retained declaration inactive
466    /// after any running work completes; it removes a remove-on-stop
467    /// declaration and expires this claim when the transition finalizes.
468    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
469        reconcile_ordinary_claim(&self.claim, None, schedule)
470    }
471
472    /// Cancel the armed callback or the running work's successor.
473    ///
474    /// Consumer work already running is not interrupted.
475    /// A retained declaration keeps callback authority. A remove-on-stop
476    /// declaration and this claim expire when cancellation finalizes.
477    pub fn cancel(&self) -> Result<(), TimerError> {
478        cancel_claim(&self.claim, None)
479    }
480
481    /// Consume the claim and unregister its callback authority.
482    ///
483    /// When called from running work, removal is deferred until that invocation
484    /// completes normally.
485    pub fn unregister(self) -> Result<(), TimerError> {
486        unregister_claim(&self.claim)
487    }
488}
489
490/// Register one asynchronous `Once` callback without scheduling it.
491pub fn register_once<F, Fut>(
492    identity: TimerIdentity,
493    lifetime: DeclarationLifetime,
494    callback: F,
495) -> Result<OnceRegistration, TimerError>
496where
497    F: FnMut(OnceContext) -> Fut + 'static,
498    Fut: Future<Output = TimerRunResult> + 'static,
499{
500    let callback = erase_ordinary_callback(callback, OnceContext::new);
501    let claim = with_registry_mut(|registry| {
502        registry
503            .register_once_with_callback(identity, lifetime, callback)
504            .map_err(TimerError::from)
505    })?;
506    Ok(OnceRegistration { claim })
507}
508
509/// Register one asynchronous callback with configured after-completion recurrence.
510pub fn register_after_completion<F, Fut>(
511    identity: TimerIdentity,
512    cadence: TimerCadence,
513    lifetime: DeclarationLifetime,
514    callback: F,
515) -> Result<AfterCompletionRegistration, TimerError>
516where
517    F: FnMut(AfterCompletionContext) -> Fut + 'static,
518    Fut: Future<Output = TimerRunResult> + 'static,
519{
520    let callback = erase_ordinary_callback(callback, AfterCompletionContext::new);
521    let claim = with_registry_mut(|registry| {
522        registry
523            .register_after_completion_with_callback(identity, cadence, lifetime, callback)
524            .map_err(TimerError::from)
525    })?;
526    Ok(AfterCompletionRegistration { claim })
527}
528
529/// Register one synchronous pre-armed watchdog callback without scheduling it.
530///
531/// The callback cannot be async: it runs only in the work message after a
532/// separate scheduler message has armed the next cadence successor. Its
533/// `WatchdogDecision` retains the cadence successor, replaces it with a
534/// deadline of now, or clears it.
535pub fn register_watchdog<F>(
536    identity: TimerIdentity,
537    cadence: TimerCadence,
538    lifetime: DeclarationLifetime,
539    callback: F,
540) -> Result<WatchdogRegistration, TimerError>
541where
542    F: FnMut(WatchdogContext) -> WatchdogRunResult + 'static,
543{
544    let mut callback = callback;
545    let callback: WatchdogCallback = Rc::new(RefCell::new(Box::new(move |token| {
546        callback(WatchdogContext::new(token))
547    })));
548    let claim = with_registry_mut(|registry| {
549        registry
550            .register_watchdog_with_callback(identity, cadence, lifetime, callback)
551            .map_err(TimerError::from)
552    })?;
553    Ok(WatchdogRegistration { claim })
554}
555
556/// Reconstruct or reconcile one `Once` declaration synchronously.
557///
558/// `Some(schedule)` is authoritative and may move an existing deadline in
559/// either direction. `None` retains an inactive declaration in the canonical
560/// inventory, including on a fresh heap. Lifecycle reconciliation always owns
561/// a [`DeclarationLifetime::Retained`] declaration; transient
562/// `RemoveWhenStopped` callbacks use [`register_once`] directly.
563pub fn reconcile_once<F, Fut>(
564    registration: &mut Option<OnceRegistration>,
565    identity: &TimerIdentity,
566    desired: Option<TimerSchedule>,
567    callback: F,
568) -> Result<(), TimerError>
569where
570    F: FnMut(OnceContext) -> Fut + 'static,
571    Fut: Future<Output = TimerRunResult> + 'static,
572{
573    let registration = reconcile_registration(registration, identity, TimerPolicy::Once, || {
574        register_once(identity.clone(), DeclarationLifetime::Retained, callback)
575    })?;
576    registration.reconcile_schedule(desired)
577}
578
579/// Reconstruct or reconcile one after-completion declaration synchronously.
580///
581/// The consumer owns `registration` in volatile state. A fresh Wasm heap has
582/// `None`, so this function installs callback authority before reconciling it
583/// active or inactive. A repeated call reuses the exact claim and does not
584/// replace its callback. The installed declaration is always retained;
585/// transient `RemoveWhenStopped` recurrence uses
586/// [`register_after_completion`] directly.
587pub fn reconcile_after_completion<F, Fut>(
588    registration: &mut Option<AfterCompletionRegistration>,
589    identity: &TimerIdentity,
590    cadence: TimerCadence,
591    desired: TimerReconcileState,
592    callback: F,
593) -> Result<(), TimerError>
594where
595    F: FnMut(AfterCompletionContext) -> Fut + 'static,
596    Fut: Future<Output = TimerRunResult> + 'static,
597{
598    let registration = reconcile_registration(
599        registration,
600        identity,
601        TimerPolicy::AfterCompletion { cadence },
602        || {
603            register_after_completion(
604                identity.clone(),
605                cadence,
606                DeclarationLifetime::Retained,
607                callback,
608            )
609        },
610    )?;
611    match desired {
612        TimerReconcileState::Inactive => registration.cancel(),
613        TimerReconcileState::Scheduled => registration.ensure_scheduled(),
614    }
615}
616
617/// Reconstruct or reconcile one watchdog declaration synchronously.
618///
619/// Durable readiness remains consumer-owned. Fresh inactive authority still
620/// installs an observable retained declaration. `ScheduledImmediately` arms
621/// its first scheduler at deadline now without synchronous consumer work.
622/// This helper owns no lifecycle export and persists no policy, generation,
623/// provider handle, or callback.
624/// Transient `RemoveWhenStopped` watchdogs use [`register_watchdog`] directly.
625pub fn reconcile_watchdog<F>(
626    registration: &mut Option<WatchdogRegistration>,
627    identity: &TimerIdentity,
628    cadence: TimerCadence,
629    desired: WatchdogReconcileState,
630    callback: F,
631) -> Result<(), TimerError>
632where
633    F: FnMut(WatchdogContext) -> WatchdogRunResult + 'static,
634{
635    let registration = reconcile_registration(
636        registration,
637        identity,
638        TimerPolicy::Watchdog { cadence },
639        || {
640            register_watchdog(
641                identity.clone(),
642                cadence,
643                DeclarationLifetime::Retained,
644                callback,
645            )
646        },
647    )?;
648    match desired {
649        WatchdogReconcileState::Inactive => registration.cancel(),
650        WatchdogReconcileState::Scheduled => registration.ensure_scheduled(),
651        WatchdogReconcileState::ScheduledImmediately => registration.ensure_scheduled_immediately(),
652    }
653}
654
655fn reconcile_registration<'a, Registration>(
656    registration: &'a mut Option<Registration>,
657    identity: &TimerIdentity,
658    policy: TimerPolicy,
659    register: impl FnOnce() -> Result<Registration, TimerError>,
660) -> Result<&'a Registration, TimerError>
661where
662    Registration: RegistrationClaimOwner,
663{
664    if registration.is_none() {
665        *registration = Some(register()?);
666    }
667    let registration = registration
668        .as_ref()
669        .ok_or(TimerError::ReconciliationConflict)?;
670    verify_declaration(registration.registration_claim(), identity, policy)?;
671    Ok(registration)
672}
673
674fn verify_declaration(
675    claim: &RegistrationClaim,
676    identity: &TimerIdentity,
677    policy: TimerPolicy,
678) -> Result<(), TimerError> {
679    if claim.identity() != identity {
680        return Err(TimerError::ReconciliationConflict);
681    }
682    with_registry(|registry| {
683        registry
684            .declaration_matches(claim, policy, DeclarationLifetime::Retained)
685            .map_err(TimerError::from)?
686            .then_some(())
687            .ok_or(TimerError::ReconciliationConflict)
688    })
689}
690
691/// Return one coherent inert snapshot by identity.
692pub fn timer_snapshot(identity: &TimerIdentity) -> Result<Option<TimerSnapshot>, TimerError> {
693    with_registry(|registry| Ok(registry.snapshot(identity)))
694}
695
696/// Return one atomic bounded inventory with its volatile runtime epoch.
697///
698/// Timer snapshots are ordered deterministically by identity. An initialized
699/// empty registry still returns its epoch and an empty timer slice.
700pub fn timer_inventory() -> Result<TimerInventorySnapshot, TimerError> {
701    with_registry(|registry| Ok(registry.inventory()))
702}
703
704/// Return functional expected-failure state by identity without a full inventory.
705pub fn consecutive_expected_failures(identity: &TimerIdentity) -> Result<Option<u64>, TimerError> {
706    with_registry(|registry| Ok(registry.consecutive_expected_failures(identity)))
707}
708
709fn has_armed_wakeup_claim(claim: &RegistrationClaim) -> Result<bool, TimerError> {
710    with_registry(|registry| registry.has_armed_wakeup(claim).map_err(TimerError::from))
711}
712
713fn erase_ordinary_callback<Context: 'static, F, Fut>(
714    mut callback: F,
715    context: fn(CallbackToken) -> Context,
716) -> OrdinaryCallback
717where
718    F: FnMut(Context) -> Fut + 'static,
719    Fut: Future<Output = TimerRunResult> + 'static,
720{
721    Rc::new(RefCell::new(Box::new(move |token| {
722        Box::pin(callback(context(token)))
723    })))
724}
725
726fn apply_claim_transition(
727    claim: &RegistrationClaim,
728    context: Option<&CallbackToken>,
729    operation: impl FnOnce(&mut TimerRegistry) -> Result<RegistryTransition, RegistryError>,
730) -> Result<(), TimerError> {
731    let transition = with_registry_mut(|registry| {
732        validate_context(registry, context)?;
733        operation(registry).map_err(TimerError::from)
734    })?;
735    finish_claim_transition(claim, transition, ProviderHandles::default())
736}
737
738fn ensure_once_claim(
739    claim: &RegistrationClaim,
740    context: Option<&CallbackToken>,
741    schedule: TimerSchedule,
742) -> Result<(), TimerError> {
743    apply_claim_transition(claim, context, |registry| {
744        registry.ensure_once(claim, platform::time_ns(), schedule)
745    })
746}
747
748fn reconcile_ordinary_claim(
749    claim: &RegistrationClaim,
750    context: Option<&CallbackToken>,
751    schedule: Option<TimerSchedule>,
752) -> Result<(), TimerError> {
753    if schedule.is_none() {
754        let (handles, transition) = with_registry_mut(|registry| {
755            validate_context(registry, context)?;
756            registry
757                .validate_ordinary_claim(claim)
758                .map_err(TimerError::from)?;
759            let handles = registry
760                .take_provider_handles_for_claim(claim)
761                .map_err(TimerError::from)?;
762            let transition = registry
763                .reconcile_ordinary(claim, platform::time_ns(), None)
764                .map_err(TimerError::from);
765            Ok((handles, transition))
766        })?;
767        return finish_detached_claim_transition(claim, handles, transition);
768    }
769    apply_claim_transition(claim, context, |registry| {
770        registry.reconcile_ordinary(claim, platform::time_ns(), schedule)
771    })
772}
773
774fn ensure_recurring_claim(
775    claim: &RegistrationClaim,
776    context: Option<&CallbackToken>,
777) -> Result<(), TimerError> {
778    apply_claim_transition(claim, context, |registry| {
779        registry.ensure_recurring(claim, platform::time_ns())
780    })
781}
782
783fn ensure_watchdog_immediately_claim(
784    claim: &RegistrationClaim,
785    context: Option<&CallbackToken>,
786) -> Result<(), TimerError> {
787    apply_claim_transition(claim, context, |registry| {
788        registry.ensure_watchdog_immediately(claim, platform::time_ns())
789    })
790}
791
792fn cancel_claim(
793    claim: &RegistrationClaim,
794    context: Option<&CallbackToken>,
795) -> Result<(), TimerError> {
796    apply_detached_claim_transition(claim, context, |registry| registry.cancel(claim))
797}
798
799fn validate_context(
800    registry: &TimerRegistry,
801    context: Option<&CallbackToken>,
802) -> Result<(), TimerError> {
803    context.map_or(Ok(()), |token| {
804        registry
805            .validate_running_context(token)
806            .map_err(TimerError::from)
807    })
808}
809
810fn unregister_claim(claim: &RegistrationClaim) -> Result<(), TimerError> {
811    apply_detached_claim_transition(claim, None, |registry| registry.unregister(claim))
812}
813
814fn apply_detached_claim_transition(
815    claim: &RegistrationClaim,
816    context: Option<&CallbackToken>,
817    operation: impl FnOnce(&mut TimerRegistry) -> Result<RegistryTransition, RegistryError>,
818) -> Result<(), TimerError> {
819    let (handles, transition) = with_registry_mut(|registry| {
820        validate_context(registry, context)?;
821        let handles = registry
822            .take_provider_handles_for_claim(claim)
823            .map_err(TimerError::from)?;
824        let transition = operation(registry).map_err(TimerError::from);
825        Ok((handles, transition))
826    })?;
827    finish_detached_claim_transition(claim, handles, transition)
828}
829
830fn finish_detached_claim_transition(
831    claim: &RegistrationClaim,
832    handles: ProviderHandles,
833    transition: Result<RegistryTransition, TimerError>,
834) -> Result<(), TimerError> {
835    match transition {
836        Ok(transition) => finish_claim_transition(claim, transition, handles),
837        Err(error) => match restore_provider_handles(handles) {
838            Ok(()) => Err(error),
839            Err(restoration_error) => retire_failed_claim(claim, restoration_error),
840        },
841    }
842}
843
844fn finish_claim_transition(
845    claim: &RegistrationClaim,
846    transition: RegistryTransition,
847    handles: ProviderHandles,
848) -> Result<(), TimerError> {
849    match finish_transition(transition, handles) {
850        result @ (Ok(()) | Err(TimerError::ControlFailure(_))) => result,
851        Err(error) => retire_failed_claim(claim, error),
852    }
853}
854
855fn retire_failed_claim(claim: &RegistrationClaim, error: TimerError) -> Result<(), TimerError> {
856    match fail_claim_provider_binding(claim) {
857        Ok(()) | Err(TimerError::RegistrationExpired) => Err(error),
858        Err(cleanup_error) => Err(cleanup_error),
859    }
860}
861
862fn finish_transition(
863    transition: RegistryTransition,
864    handles: ProviderHandles,
865) -> Result<(), TimerError> {
866    let failure = transition.failure();
867    let effect = transition.into_effect();
868    apply_effect(&effect, handles)?;
869    failure.map_or(Ok(()), |failure| Err(TimerError::ControlFailure(failure)))
870}
871
872fn apply_effect(effect: &RegistryEffect, mut handles: ProviderHandles) -> Result<(), TimerError> {
873    if !effect.has_valid_shape() {
874        clear_provider_handles(handles);
875        return Err(TimerError::OwnershipInvariant);
876    }
877    match effect {
878        RegistryEffect::None => restore_provider_handles(handles),
879        RegistryEffect::ArmWakeup { token, arm, .. } => {
880            if arm.replaces_existing() {
881                let replaced = take_detached_or_owned_handle(handles.take_wakeup(), |registry| {
882                    registry.take_wakeup_handle(token.identity())
883                })?;
884                if let Some(replaced) = replaced {
885                    clear_provider_handle(replaced);
886                }
887            }
888            restore_provider_handles(handles)?;
889            arm_wakeup(effect)
890        }
891        RegistryEffect::ClearCallbacks {
892            identity,
893            handles: selected,
894        } => {
895            if selected.includes_wakeup() {
896                let wakeup = take_detached_or_owned_handle(handles.take_wakeup(), |registry| {
897                    registry.take_wakeup_handle(identity)
898                })?;
899                if let Some(wakeup) = wakeup {
900                    clear_provider_handle(wakeup);
901                }
902            }
903            if selected.includes_work() {
904                let work = take_detached_or_owned_handle(handles.take_work(), |registry| {
905                    registry.take_work_handle(identity)
906                })?;
907                if let Some(work) = work {
908                    clear_provider_handle(work);
909                }
910            }
911            restore_provider_handles(handles)
912        }
913        RegistryEffect::DispatchWatchdog { successor, .. } => {
914            if let Some(wakeup) = handles.take_wakeup() {
915                clear_provider_handle(wakeup);
916            }
917            let replaced_work = take_detached_or_owned_handle(handles.take_work(), |registry| {
918                registry.take_work_handle(successor.identity())
919            })?;
920            if let Some(replaced_work) = replaced_work {
921                clear_provider_handle(replaced_work);
922            }
923            dispatch_watchdog_effect(effect)
924        }
925    }
926}
927
928fn take_detached_or_owned_handle(
929    detached: Option<ProviderHandle>,
930    take_owned: impl FnOnce(&mut TimerRegistry) -> Option<ProviderHandle>,
931) -> Result<Option<ProviderHandle>, TimerError> {
932    detached.map_or_else(
933        || with_registry_mut(|registry| Ok(take_owned(registry))),
934        |handle| Ok(Some(handle)),
935    )
936}
937
938fn arm_wakeup(effect: &RegistryEffect) -> Result<(), TimerError> {
939    let RegistryEffect::ArmWakeup {
940        token, delay_ns, ..
941    } = effect
942    else {
943        return Err(TimerError::OwnershipInvariant);
944    };
945    let task_token = token.clone();
946    let handle = platform::set_timer(Duration::from_nanos(*delay_ns), async move {
947        dispatch_wakeup(task_token).await;
948    });
949    if let Err((error, handle)) = install_provider_handle(token, handle) {
950        platform::clear_timer(handle);
951        return Err(error);
952    }
953    if let Err(error) = confirm_effect(effect) {
954        let handle = with_registry_mut(|registry| {
955            registry
956                .take_wakeup_handle(token.identity())
957                .ok_or(TimerError::OwnershipInvariant)
958        })?;
959        clear_provider_handle(handle);
960        return Err(error);
961    }
962    Ok(())
963}
964
965fn dispatch_watchdog_effect(effect: &RegistryEffect) -> Result<(), TimerError> {
966    let RegistryEffect::DispatchWatchdog {
967        successor,
968        successor_delay_ns,
969        work,
970        ..
971    } = effect
972    else {
973        return Err(TimerError::OwnershipInvariant);
974    };
975    let successor_token = successor.clone();
976    let successor_handle =
977        platform::set_timer(Duration::from_nanos(*successor_delay_ns), async move {
978            dispatch_watchdog_scheduler(&successor_token);
979        });
980    if let Err((error, handle)) = install_provider_handle(successor, successor_handle) {
981        platform::clear_timer(handle);
982        return Err(error);
983    }
984
985    let work_token = work.clone();
986    let work_handle = platform::set_timer(Duration::ZERO, async move {
987        dispatch_watchdog_work(&work_token);
988    });
989    if let Err((error, handle)) = install_provider_handle(work, work_handle) {
990        platform::clear_timer(handle);
991        clear_entry_provider_handles(successor.identity())?;
992        return Err(error);
993    }
994    if let Err(error) = confirm_effect(effect) {
995        clear_entry_provider_handles(successor.identity())?;
996        return Err(error);
997    }
998    Ok(())
999}
1000
1001fn install_provider_handle(
1002    token: &CallbackToken,
1003    handle: TimerHandle,
1004) -> Result<(), (TimerError, TimerHandle)> {
1005    #[cfg(test)]
1006    if take_provider_install_fault() {
1007        return Err((TimerError::OwnershipInvariant, handle));
1008    }
1009    RUNTIME.with(|runtime| {
1010        let Ok(mut runtime) = runtime.try_borrow_mut() else {
1011            return Err((TimerError::RuntimeBusy, handle));
1012        };
1013        let Some(registry) = runtime.as_mut() else {
1014            return Err((TimerError::NotInitialized, handle));
1015        };
1016        match registry.install_provider_handle(token, handle) {
1017            Ok(()) => Ok(()),
1018            Err((error, handle)) => Err((TimerError::from(error), handle)),
1019        }
1020    })
1021}
1022
1023fn confirm_effect(effect: &RegistryEffect) -> Result<(), TimerError> {
1024    #[cfg(test)]
1025    if take_provider_confirmation_fault() {
1026        return Err(TimerError::OwnershipInvariant);
1027    }
1028    with_registry_mut(|registry| {
1029        registry
1030            .confirm_effect_applied(effect)
1031            .map_err(TimerError::from)
1032    })
1033}
1034
1035fn restore_provider_handles(mut handles: ProviderHandles) -> Result<(), TimerError> {
1036    // Every detached linear capability must be restored or cleared even when
1037    // restoring an earlier handle fails.
1038    let wakeup_failure = handles
1039        .take_wakeup()
1040        .and_then(|handle| restore_provider_handle(handle).err());
1041    let work_failure = handles
1042        .take_work()
1043        .and_then(|handle| restore_provider_handle(handle).err());
1044    wakeup_failure.or(work_failure).map_or(Ok(()), Err)
1045}
1046
1047fn restore_provider_handle(handle: ProviderHandle) -> Result<(), TimerError> {
1048    let (token, handle) = handle.into_parts();
1049    match install_provider_handle(&token, handle) {
1050        Ok(()) => Ok(()),
1051        Err((error, handle)) => {
1052            platform::clear_timer(handle);
1053            Err(error)
1054        }
1055    }
1056}
1057
1058fn clear_provider_handle(handle: ProviderHandle) {
1059    let (_, handle) = handle.into_parts();
1060    platform::clear_timer(handle);
1061}
1062
1063fn clear_provider_handles(mut handles: ProviderHandles) {
1064    if let Some(wakeup) = handles.take_wakeup() {
1065        clear_provider_handle(wakeup);
1066    }
1067    if let Some(work) = handles.take_work() {
1068        clear_provider_handle(work);
1069    }
1070}
1071
1072fn clear_entry_provider_handles(identity: &TimerIdentity) -> Result<(), TimerError> {
1073    let handles = with_registry_mut(|registry| {
1074        Ok(ProviderHandles::from_parts(
1075            registry.take_wakeup_handle(identity),
1076            registry.take_work_handle(identity),
1077        ))
1078    })?;
1079    clear_provider_handles(handles);
1080    Ok(())
1081}
1082
1083#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
1084async fn dispatch_wakeup(token: CallbackToken) {
1085    match token.role() {
1086        CallbackRole::OrdinaryWork => dispatch_ordinary(token).await,
1087        CallbackRole::WatchdogScheduler => dispatch_watchdog_scheduler(&token),
1088        CallbackRole::WatchdogWork => {}
1089    }
1090}
1091
1092#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
1093async fn dispatch_ordinary(token: CallbackToken) {
1094    let measurement = CallbackMeasurementStart::capture();
1095    let accepted = with_registry_mut(|registry| {
1096        registry.consume_provider_handle(&token);
1097        Ok(registry.begin_ordinary(&token))
1098    });
1099    match accepted {
1100        Ok(CallbackAcceptance::Accepted) => {}
1101        Ok(CallbackAcceptance::Stale) => return,
1102        Err(error) => trap_callback_failure("ordinary callback acceptance", &error),
1103    }
1104
1105    let callback = match with_registry(|registry| {
1106        registry.ordinary_callback(&token).map_err(TimerError::from)
1107    }) {
1108        Ok(callback) => callback,
1109        Err(TimerError::OwnershipInvariant) => {
1110            fail_ordinary_dispatch(&token);
1111            return;
1112        }
1113        Err(error) => trap_callback_failure("ordinary callback lookup", &error),
1114    };
1115    let future = {
1116        let Ok(mut callback) = callback.try_borrow_mut() else {
1117            fail_ordinary_dispatch(&token);
1118            return;
1119        };
1120        callback(token.clone())
1121    };
1122    let result = future.await;
1123    let transition = with_registry_mut(|registry| {
1124        registry
1125            .complete_ordinary(&token, platform::time_ns(), result)
1126            .map_err(TimerError::from)
1127    });
1128    let transition = transition
1129        .unwrap_or_else(|error| trap_callback_failure("ordinary callback completion", &error));
1130    finish_callback_transition(&token, transition, ProviderHandles::default());
1131    record_callback_measurements(&token, measurement.finish());
1132}
1133
1134fn fail_ordinary_dispatch(token: &CallbackToken) {
1135    let transition = with_registry_mut(|registry| {
1136        registry
1137            .complete_ordinary(
1138                token,
1139                platform::time_ns(),
1140                TimerRunResult::new(TimerCompletion::invariant_failure(0), TimerDirective::Stop),
1141            )
1142            .map_err(TimerError::from)
1143    });
1144    let transition = transition.unwrap_or_else(|error| {
1145        trap_callback_failure("ordinary invariant-failure completion", &error)
1146    });
1147    finish_callback_transition(token, transition, ProviderHandles::default());
1148}
1149
1150fn dispatch_watchdog_scheduler(token: &CallbackToken) {
1151    let measurement = CallbackMeasurementStart::capture();
1152    let transition = with_registry_mut(|registry| {
1153        registry.consume_provider_handle(token);
1154        Ok(registry.begin_watchdog_scheduler(token, platform::time_ns()))
1155    });
1156    let transition = transition
1157        .unwrap_or_else(|error| trap_callback_failure("watchdog scheduler transition", &error));
1158    let accepted = !matches!(transition.effect(), RegistryEffect::None);
1159    finish_callback_transition(token, transition, ProviderHandles::default());
1160    if accepted {
1161        record_callback_measurements(token, measurement.finish());
1162    }
1163}
1164
1165fn dispatch_watchdog_work(token: &CallbackToken) {
1166    let measurement = CallbackMeasurementStart::capture();
1167    let accepted = with_registry_mut(|registry| {
1168        registry.consume_provider_handle(token);
1169        Ok(registry.begin_watchdog_work(token))
1170    });
1171    match accepted {
1172        Ok(CallbackAcceptance::Accepted) => {}
1173        Ok(CallbackAcceptance::Stale) => return,
1174        Err(error) => trap_callback_failure("watchdog work acceptance", &error),
1175    }
1176
1177    let callback =
1178        match with_registry(|registry| registry.watchdog_callback(token).map_err(TimerError::from))
1179        {
1180            Ok(callback) => callback,
1181            Err(error) => trap_callback_failure("watchdog callback lookup", &error),
1182        };
1183    let result = {
1184        let Ok(mut callback) = callback.try_borrow_mut() else {
1185            trap_callback_failure(
1186                "watchdog callback ownership",
1187                &TimerError::OwnershipInvariant,
1188            );
1189        };
1190        callback(token.clone())
1191    };
1192    finish_watchdog_dispatch(token, result);
1193    record_callback_measurements(token, measurement.finish());
1194}
1195
1196fn finish_watchdog_dispatch(token: &CallbackToken, result: WatchdogRunResult) {
1197    let claim = RegistrationClaim::from_callback(token);
1198    // Unlike synchronous public control, an unexpected callback-completion
1199    // failure must trap. IC message rollback restores these temporarily
1200    // detached heap capabilities while the previously committed successor
1201    // remains armed by the scheduler message.
1202    let completed = with_registry_mut(|registry| {
1203        let handles = registry
1204            .take_provider_handles_for_claim(&claim)
1205            .map_err(TimerError::from)?;
1206        #[cfg(test)]
1207        {
1208            if take_watchdog_completion_fault() {
1209                return Err(TimerError::OwnershipInvariant);
1210            }
1211        }
1212        let transition = registry
1213            .complete_watchdog_work(token, platform::time_ns(), result)
1214            .map_err(TimerError::from)?;
1215        Ok((transition, handles))
1216    });
1217    let (transition, handles) =
1218        completed.unwrap_or_else(|error| trap_callback_failure("watchdog work completion", &error));
1219    finish_callback_transition(token, transition, handles);
1220}
1221
1222fn finish_callback_transition(
1223    token: &CallbackToken,
1224    transition: RegistryTransition,
1225    handles: ProviderHandles,
1226) {
1227    match finish_transition(transition, handles) {
1228        Ok(()) | Err(TimerError::ControlFailure(_)) => {}
1229        Err(
1230            error @ (TimerError::NotInitialized
1231            | TimerError::RuntimeBusy
1232            | TimerError::Register(_)
1233            | TimerError::Schedule(_)
1234            | TimerError::RegistrationExpired
1235            | TimerError::OwnershipInvariant
1236            | TimerError::ReconciliationConflict),
1237        ) => {
1238            if token.role() == CallbackRole::WatchdogWork {
1239                trap_callback_failure("watchdog provider-handle completion", &error);
1240            }
1241            fail_provider_binding(token).unwrap_or_else(|binding_error| {
1242                trap_callback_failure("provider-binding failure cleanup", &binding_error)
1243            });
1244        }
1245    }
1246}
1247
1248fn fail_provider_binding(token: &CallbackToken) -> Result<(), TimerError> {
1249    let claim = RegistrationClaim::from_callback(token);
1250    fail_claim_provider_binding(&claim)
1251}
1252
1253fn fail_claim_provider_binding(claim: &RegistrationClaim) -> Result<(), TimerError> {
1254    let failed = with_registry_mut(|registry| {
1255        registry
1256            .fail_registration(claim, TimerControlFailure::ProviderBindingFailed)
1257            .map_err(TimerError::from)
1258    });
1259    clear_provider_handles(failed?);
1260    Ok(())
1261}
1262
1263#[derive(Clone, Copy)]
1264struct CallbackMeasurementStart {
1265    instructions_before: u64,
1266    memory_start: platform::MemoryPages,
1267}
1268
1269impl CallbackMeasurementStart {
1270    fn capture() -> Self {
1271        // Keep page observation outside the established instruction interval.
1272        let memory_start = platform::memory_pages();
1273        let instructions_before = platform::instruction_counter();
1274        Self {
1275            instructions_before,
1276            memory_start,
1277        }
1278    }
1279
1280    fn finish(self) -> CallbackMeasurement {
1281        // Close the instruction interval before taking its paired end extent.
1282        let instructions = platform::instruction_counter().saturating_sub(self.instructions_before);
1283        let memory_end = platform::memory_pages();
1284        CallbackMeasurement {
1285            instructions,
1286            memory_start: self.memory_start,
1287            memory_end,
1288        }
1289    }
1290}
1291
1292#[derive(Clone, Copy)]
1293struct CallbackMeasurement {
1294    instructions: u64,
1295    memory_start: platform::MemoryPages,
1296    memory_end: platform::MemoryPages,
1297}
1298
1299fn record_callback_measurements(token: &CallbackToken, measurement: CallbackMeasurement) {
1300    with_registry_mut(|registry| {
1301        registry
1302            .record_callback_measurements(
1303                token,
1304                measurement.instructions,
1305                measurement.memory_start,
1306                measurement.memory_end,
1307            )
1308            .map_err(TimerError::from)
1309    })
1310    .unwrap_or_else(|error| trap_callback_failure("callback measurement accounting", &error));
1311}
1312
1313fn trap_callback_failure(context: &str, error: &TimerError) -> ! {
1314    platform::trap(&format!("ic-timers {context} failed: {error}"))
1315}
1316
1317fn with_registry<T>(
1318    operation: impl FnOnce(&TimerRegistry) -> Result<T, TimerError>,
1319) -> Result<T, TimerError> {
1320    RUNTIME.with(|runtime| {
1321        let runtime = runtime.try_borrow().map_err(|_| TimerError::RuntimeBusy)?;
1322        let registry = runtime.as_ref().ok_or(TimerError::NotInitialized)?;
1323        operation(registry)
1324    })
1325}
1326
1327fn with_registry_mut<T>(
1328    operation: impl FnOnce(&mut TimerRegistry) -> Result<T, TimerError>,
1329) -> Result<T, TimerError> {
1330    RUNTIME.with(|runtime| {
1331        let mut runtime = runtime
1332            .try_borrow_mut()
1333            .map_err(|_| TimerError::RuntimeBusy)?;
1334        let registry = runtime.as_mut().ok_or(TimerError::NotInitialized)?;
1335        operation(registry)
1336    })
1337}
1338
1339#[cfg(test)]
1340fn reset_for_test(now_ns: u64, canister_version: u64) {
1341    platform::reset(now_ns, canister_version);
1342    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(false));
1343    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(None));
1344    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.set(false));
1345    RUNTIME.with(|runtime| {
1346        *runtime.borrow_mut() = None;
1347    });
1348}
1349
1350#[cfg(test)]
1351thread_local! {
1352    static WATCHDOG_COMPLETION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1353    static PROVIDER_INSTALL_FAULT_AFTER: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1354    static PROVIDER_CONFIRMATION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1355}
1356
1357#[cfg(test)]
1358fn inject_watchdog_completion_fault() {
1359    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(true));
1360}
1361
1362#[cfg(test)]
1363fn take_watchdog_completion_fault() -> bool {
1364    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.replace(false))
1365}
1366
1367#[cfg(test)]
1368fn inject_provider_install_fault() {
1369    inject_provider_install_fault_after(0);
1370}
1371
1372#[cfg(test)]
1373fn inject_provider_install_fault_after(successful_installs: u64) {
1374    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(Some(successful_installs)));
1375}
1376
1377#[cfg(test)]
1378fn take_provider_install_fault() -> bool {
1379    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| match fault.get() {
1380        Some(0) => {
1381            fault.set(None);
1382            true
1383        }
1384        Some(remaining) => {
1385            fault.set(Some(remaining - 1));
1386            false
1387        }
1388        None => false,
1389    })
1390}
1391
1392#[cfg(test)]
1393fn inject_provider_confirmation_fault() {
1394    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.set(true));
1395}
1396
1397#[cfg(test)]
1398fn take_provider_confirmation_fault() -> bool {
1399    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.replace(false))
1400}
1401
1402#[cfg(test)]
1403mod tests;