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