Skip to main content

ic_timers/runtime/
mod.rs

1//! Canister-local owner and live timer execution.
2
3use crate::{
4    DeclarationLifetime, RegisterError, ScheduleError, TimerCadence, TimerCompletion,
5    TimerControlFailure, TimerDirective, TimerEpoch, TimerIdentity, TimerRunResult, TimerSchedule,
6    TimerSnapshot, WatchdogRunResult,
7    platform::{self, TimerHandle},
8    registry::{
9        CallbackAcceptance, CallbackRole, CallbackToken, OrdinaryCallback, ProviderHandle,
10        ProviderHandles, RegistrationClaim, RegistryEffect, RegistryError, RegistryTransition,
11        TimerRegistry, WatchdogCallback,
12    },
13};
14use std::{cell::RefCell, future::Future, pin::Pin, rc::Rc, time::Duration};
15use thiserror::Error;
16
17/// Erased future returned by an ordinary timer callback.
18pub type TimerFuture = Pin<Box<dyn Future<Output = TimerRunResult>>>;
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    /// Registration failed before any provider callback was armed.
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    /// An operation was attempted through the wrong policy-specific API.
44    #[error("timer operation does not match its registered policy")]
45    WrongPolicy,
46    /// Pure checked control reached a terminal failure after effects were applied.
47    #[error("timer control failed: {0:?}")]
48    ControlFailure(TimerControlFailure),
49    /// Canonical callback or provider-handle ownership was internally inconsistent.
50    #[error("timer runtime ownership invariant failed")]
51    OwnershipInvariant,
52    /// A retained lifecycle claim no longer matches its canonical declaration.
53    #[error("timer lifecycle reconciliation conflicts with the canonical declaration")]
54    ReconciliationConflict,
55}
56
57impl From<RegistryError> for TimerError {
58    fn from(value: RegistryError) -> Self {
59        match value {
60            RegistryError::UnknownRegistration
61            | RegistryError::StaleRegistration
62            | RegistryError::StaleCallback => Self::RegistrationExpired,
63            RegistryError::WrongPolicy { .. } => Self::WrongPolicy,
64            RegistryError::Schedule(error) => Self::Schedule(error),
65            RegistryError::MissingCallback | RegistryError::ProviderHandleAlreadyOwned => {
66                Self::OwnershipInvariant
67            }
68        }
69    }
70}
71
72/// Initialize the volatile canister-local runtime once for this Wasm instance.
73///
74/// Repeated calls are idempotent and return the original epoch. This function
75/// exports no lifecycle hook; the canister's existing lifecycle owner calls it.
76pub fn initialize_runtime() -> Result<TimerEpoch, TimerError> {
77    let epoch = TimerEpoch::new(platform::canister_version(), platform::time_ns());
78    RUNTIME.with(|runtime| {
79        let mut runtime = runtime
80            .try_borrow_mut()
81            .map_err(|_| TimerError::RuntimeBusy)?;
82        if let Some(registry) = runtime.as_ref() {
83            return Ok(registry.epoch());
84        }
85        *runtime = Some(TimerRegistry::new(epoch));
86        Ok(epoch)
87    })
88}
89
90/// Delegated exact registration capability passed to consumer work.
91pub struct TimerContext {
92    identity: TimerIdentity,
93    claim_generation: u64,
94}
95
96impl TimerContext {
97    const fn new(identity: TimerIdentity, claim_generation: u64) -> Self {
98        Self {
99            identity,
100            claim_generation,
101        }
102    }
103
104    fn claim(&self) -> RegistrationClaim {
105        RegistrationClaim::delegated(self.identity.clone(), self.claim_generation)
106    }
107
108    /// Return the logical timer identity executing this work.
109    #[must_use]
110    pub const fn identity(&self) -> &TimerIdentity {
111        &self.identity
112    }
113
114    /// Schedule a `Once` declaration from consumer work.
115    pub fn ensure_once(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
116        ensure_once_claim(&self.claim(), schedule)
117    }
118
119    /// Reconcile the executing ordinary declaration to one exact schedule.
120    ///
121    /// `None` cancels live work while retaining callback authority. Watchdog
122    /// declarations reject this operation.
123    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
124        reconcile_ordinary_claim(&self.claim(), schedule)
125    }
126
127    /// Ensure an after-completion or watchdog declaration has one wake-up.
128    pub fn ensure_recurring(&self) -> Result<(), TimerError> {
129        ensure_recurring_claim(&self.claim())
130    }
131
132    /// Request cancellation using the exact executing registration claim.
133    pub fn cancel(&self) -> Result<(), TimerError> {
134        cancel_claim(&self.claim())
135    }
136}
137
138/// Opaque non-clone claim for one registered `Once` callback.
139#[must_use = "retain the registration claim so the timer remains controllable"]
140pub struct OnceRegistration {
141    claim: RegistrationClaim,
142}
143
144impl OnceRegistration {
145    /// Return the claimed logical identity.
146    #[must_use]
147    pub const fn identity(&self) -> &TimerIdentity {
148        self.claim.identity()
149    }
150
151    /// Synchronously ensure one callback is scheduled.
152    pub fn ensure_scheduled(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
153        ensure_once_claim(&self.claim, schedule)
154    }
155
156    /// Reconcile to one exact desired schedule, replacing a later or earlier
157    /// live deadline as necessary. `None` retains the declaration but cancels
158    /// its live callback.
159    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
160        reconcile_ordinary_claim(&self.claim, schedule)
161    }
162
163    /// Cancel the current schedule while retaining callback authority when configured.
164    pub fn cancel(&self) -> Result<(), TimerError> {
165        cancel_claim(&self.claim)
166    }
167
168    /// Consume the claim and unregister its callback authority.
169    pub fn unregister(self) -> Result<(), TimerError> {
170        unregister_claim(self.claim)
171    }
172}
173
174/// Opaque non-clone claim for one after-completion callback.
175#[must_use = "retain the registration claim so the timer remains controllable"]
176pub struct AfterCompletionRegistration {
177    claim: RegistrationClaim,
178}
179
180/// Opaque non-clone claim for one pre-armed watchdog callback.
181#[must_use = "retain the registration claim so the timer remains controllable"]
182pub struct WatchdogRegistration {
183    claim: RegistrationClaim,
184}
185
186/// Desired volatile scheduling state during synchronous lifecycle reconciliation.
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188pub enum TimerReconcileState {
189    /// Retain an existing declaration but clear its live callbacks.
190    Inactive,
191    /// Ensure one authoritative wake-up exists.
192    Scheduled,
193}
194
195impl WatchdogRegistration {
196    /// Return the claimed logical identity.
197    #[must_use]
198    pub const fn identity(&self) -> &TimerIdentity {
199        self.claim.identity()
200    }
201
202    /// Synchronously ensure one watchdog scheduler wake-up is authoritative.
203    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
204        ensure_recurring_claim(&self.claim)
205    }
206
207    /// Cancel the scheduler and any dispatched work callback.
208    pub fn cancel(&self) -> Result<(), TimerError> {
209        cancel_claim(&self.claim)
210    }
211
212    /// Consume the claim and unregister its callback authority.
213    pub fn unregister(self) -> Result<(), TimerError> {
214        unregister_claim(self.claim)
215    }
216}
217
218impl AfterCompletionRegistration {
219    /// Return the claimed logical identity.
220    #[must_use]
221    pub const fn identity(&self) -> &TimerIdentity {
222        self.claim.identity()
223    }
224
225    /// Synchronously ensure one callback is scheduled at the configured cadence.
226    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
227        ensure_recurring_claim(&self.claim)
228    }
229
230    /// Reconcile to one exact desired schedule without changing the configured
231    /// after-completion cadence. `None` retains the declaration but cancels its
232    /// live callback.
233    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
234        reconcile_ordinary_claim(&self.claim, schedule)
235    }
236
237    /// Cancel the current schedule while retaining callback authority when configured.
238    pub fn cancel(&self) -> Result<(), TimerError> {
239        cancel_claim(&self.claim)
240    }
241
242    /// Consume the claim and unregister its callback authority.
243    pub fn unregister(self) -> Result<(), TimerError> {
244        unregister_claim(self.claim)
245    }
246}
247
248/// Register one asynchronous `Once` callback without scheduling it.
249pub fn register_once<F, Fut>(
250    identity: TimerIdentity,
251    lifetime: DeclarationLifetime,
252    mut callback: F,
253) -> Result<OnceRegistration, TimerError>
254where
255    F: FnMut(TimerContext) -> Fut + 'static,
256    Fut: Future<Output = TimerRunResult> + 'static,
257{
258    let callback: OrdinaryCallback = Rc::new(RefCell::new(Box::new(move |context| {
259        Box::pin(callback(context))
260    })));
261    let claim = with_registry_mut(|registry| {
262        registry
263            .register_once_with_callback(identity, lifetime, callback)
264            .map_err(TimerError::from)
265    })?;
266    Ok(OnceRegistration { claim })
267}
268
269/// Register one asynchronous fixed-cadence after-completion callback.
270pub fn register_after_completion<F, Fut>(
271    identity: TimerIdentity,
272    cadence: TimerCadence,
273    lifetime: DeclarationLifetime,
274    mut callback: F,
275) -> Result<AfterCompletionRegistration, TimerError>
276where
277    F: FnMut(TimerContext) -> Fut + 'static,
278    Fut: Future<Output = TimerRunResult> + 'static,
279{
280    let callback: OrdinaryCallback = Rc::new(RefCell::new(Box::new(move |context| {
281        Box::pin(callback(context))
282    })));
283    let claim = with_registry_mut(|registry| {
284        registry
285            .register_after_completion_with_callback(identity, cadence, lifetime, callback)
286            .map_err(TimerError::from)
287    })?;
288    Ok(AfterCompletionRegistration { claim })
289}
290
291/// Register one synchronous pre-armed watchdog callback without scheduling it.
292///
293/// The callback cannot be async: it runs only in the work message after a
294/// separate scheduler message has armed the next cadence successor. Its
295/// `WatchdogDecision` either retains or clears that committed successor.
296pub fn register_watchdog<F>(
297    identity: TimerIdentity,
298    cadence: TimerCadence,
299    lifetime: DeclarationLifetime,
300    callback: F,
301) -> Result<WatchdogRegistration, TimerError>
302where
303    F: FnMut(TimerContext) -> WatchdogRunResult + 'static,
304{
305    let callback: WatchdogCallback = Rc::new(RefCell::new(Box::new(callback)));
306    let claim = with_registry_mut(|registry| {
307        registry
308            .register_watchdog_with_callback(identity, cadence, lifetime, callback)
309            .map_err(TimerError::from)
310    })?;
311    Ok(WatchdogRegistration { claim })
312}
313
314/// Reconstruct or reconcile one `Once` declaration synchronously.
315///
316/// `Some(schedule)` is authoritative and may move an existing deadline in
317/// either direction. `None` retains an inactive declaration in the canonical
318/// inventory, including on a fresh heap.
319pub fn reconcile_once<F, Fut>(
320    registration: &mut Option<OnceRegistration>,
321    identity: &TimerIdentity,
322    lifetime: DeclarationLifetime,
323    desired: Option<TimerSchedule>,
324    callback: F,
325) -> Result<(), TimerError>
326where
327    F: FnMut(TimerContext) -> Fut + 'static,
328    Fut: Future<Output = TimerRunResult> + 'static,
329{
330    if registration.is_none() {
331        *registration = Some(register_once(identity.clone(), lifetime, callback)?);
332    }
333    verify_declaration(
334        registration.as_ref().map(OnceRegistration::identity),
335        identity,
336        crate::TimerPolicy::Once,
337        lifetime,
338    )?;
339    registration
340        .as_ref()
341        .ok_or(TimerError::ReconciliationConflict)?
342        .reconcile_schedule(desired)
343}
344
345/// Reconstruct or reconcile one after-completion declaration synchronously.
346///
347/// The consumer owns `registration` in volatile state. A fresh Wasm heap has
348/// `None`, so this function installs callback authority before reconciling it
349/// active or inactive. A repeated call reuses the exact claim and does not
350/// replace its callback.
351pub fn reconcile_after_completion<F, Fut>(
352    registration: &mut Option<AfterCompletionRegistration>,
353    identity: &TimerIdentity,
354    cadence: TimerCadence,
355    lifetime: DeclarationLifetime,
356    desired: TimerReconcileState,
357    callback: F,
358) -> Result<(), TimerError>
359where
360    F: FnMut(TimerContext) -> Fut + 'static,
361    Fut: Future<Output = TimerRunResult> + 'static,
362{
363    if registration.is_none() {
364        *registration = Some(register_after_completion(
365            identity.clone(),
366            cadence,
367            lifetime,
368            callback,
369        )?);
370    }
371    verify_declaration(
372        registration
373            .as_ref()
374            .map(AfterCompletionRegistration::identity),
375        identity,
376        crate::TimerPolicy::AfterCompletion { cadence },
377        lifetime,
378    )?;
379    let registration = registration
380        .as_ref()
381        .ok_or(TimerError::ReconciliationConflict)?;
382    match desired {
383        TimerReconcileState::Inactive => registration.cancel(),
384        TimerReconcileState::Scheduled => registration.ensure_scheduled(),
385    }
386}
387
388/// Reconstruct or reconcile one watchdog declaration synchronously.
389///
390/// Durable readiness remains consumer-owned. Fresh inactive authority still
391/// installs an observable retained declaration. This helper owns no lifecycle
392/// export and persists no policy, generation, provider handle, or callback.
393pub fn reconcile_watchdog<F>(
394    registration: &mut Option<WatchdogRegistration>,
395    identity: &TimerIdentity,
396    cadence: TimerCadence,
397    lifetime: DeclarationLifetime,
398    desired: TimerReconcileState,
399    callback: F,
400) -> Result<(), TimerError>
401where
402    F: FnMut(TimerContext) -> WatchdogRunResult + 'static,
403{
404    if registration.is_none() {
405        *registration = Some(register_watchdog(
406            identity.clone(),
407            cadence,
408            lifetime,
409            callback,
410        )?);
411    }
412    verify_declaration(
413        registration.as_ref().map(WatchdogRegistration::identity),
414        identity,
415        crate::TimerPolicy::Watchdog { cadence },
416        lifetime,
417    )?;
418    let registration = registration
419        .as_ref()
420        .ok_or(TimerError::ReconciliationConflict)?;
421    match desired {
422        TimerReconcileState::Inactive => registration.cancel(),
423        TimerReconcileState::Scheduled => registration.ensure_scheduled(),
424    }
425}
426
427fn verify_declaration(
428    claimed_identity: Option<&TimerIdentity>,
429    identity: &TimerIdentity,
430    policy: crate::TimerPolicy,
431    lifetime: DeclarationLifetime,
432) -> Result<(), TimerError> {
433    if claimed_identity != Some(identity) {
434        return Err(TimerError::ReconciliationConflict);
435    }
436    let snapshot = timer_snapshot(identity)?.ok_or(TimerError::RegistrationExpired)?;
437    if snapshot.policy() != policy || snapshot.lifetime() != lifetime {
438        return Err(TimerError::ReconciliationConflict);
439    }
440    Ok(())
441}
442
443/// Return one coherent inert snapshot by identity.
444pub fn timer_snapshot(identity: &TimerIdentity) -> Result<Option<TimerSnapshot>, TimerError> {
445    with_registry(|registry| Ok(registry.snapshot(identity)))
446}
447
448/// Return the complete bounded inventory in deterministic identity order.
449pub fn timer_snapshots() -> Result<Vec<TimerSnapshot>, TimerError> {
450    with_registry(|registry| Ok(registry.snapshots()))
451}
452
453/// Return functional expected-failure state by identity without a full inventory.
454pub fn consecutive_expected_failures(identity: &TimerIdentity) -> Result<Option<u64>, TimerError> {
455    with_registry(|registry| Ok(registry.consecutive_expected_failures(identity)))
456}
457
458fn ensure_once_claim(claim: &RegistrationClaim, schedule: TimerSchedule) -> Result<(), TimerError> {
459    let transition = with_registry_mut(|registry| {
460        registry
461            .ensure_once(claim, platform::time_ns(), schedule)
462            .map_err(TimerError::from)
463    })?;
464    finish_transition(transition, ProviderHandles::default())
465}
466
467fn reconcile_ordinary_claim(
468    claim: &RegistrationClaim,
469    schedule: Option<TimerSchedule>,
470) -> Result<(), TimerError> {
471    if schedule.is_none() {
472        let (handles, transition) = with_registry_mut(|registry| {
473            registry
474                .validate_ordinary_claim(claim)
475                .map_err(TimerError::from)?;
476            let handles = registry
477                .take_provider_handles_for_claim(claim)
478                .map_err(TimerError::from)?;
479            let transition = registry
480                .reconcile_ordinary(claim, platform::time_ns(), None)
481                .map_err(TimerError::from)?;
482            Ok((handles, transition))
483        })?;
484        return finish_transition(transition, handles);
485    }
486    let transition = with_registry_mut(|registry| {
487        registry
488            .reconcile_ordinary(claim, platform::time_ns(), schedule)
489            .map_err(TimerError::from)
490    })?;
491    finish_transition(transition, ProviderHandles::default())
492}
493
494fn ensure_recurring_claim(claim: &RegistrationClaim) -> Result<(), TimerError> {
495    let transition = with_registry_mut(|registry| {
496        registry
497            .ensure_recurring(claim, platform::time_ns())
498            .map_err(TimerError::from)
499    })?;
500    finish_transition(transition, ProviderHandles::default())
501}
502
503fn cancel_claim(claim: &RegistrationClaim) -> Result<(), TimerError> {
504    let (handles, transition) = with_registry_mut(|registry| {
505        let handles = registry
506            .take_provider_handles_for_claim(claim)
507            .map_err(TimerError::from)?;
508        let transition = registry.cancel(claim).map_err(TimerError::from)?;
509        Ok((handles, transition))
510    })?;
511    finish_transition(transition, handles)
512}
513
514fn unregister_claim(claim: RegistrationClaim) -> Result<(), TimerError> {
515    let (handles, transition) = with_registry_mut(|registry| {
516        let handles = registry
517            .take_provider_handles_for_claim(&claim)
518            .map_err(TimerError::from)?;
519        let transition = registry.unregister(claim).map_err(TimerError::from)?;
520        Ok((handles, transition))
521    })?;
522    finish_transition(transition, handles)
523}
524
525fn finish_transition(
526    transition: RegistryTransition,
527    handles: ProviderHandles,
528) -> Result<(), TimerError> {
529    let failure = transition.failure();
530    let effect = transition.into_effect();
531    apply_effect(&effect, handles)?;
532    failure.map_or(Ok(()), |failure| Err(TimerError::ControlFailure(failure)))
533}
534
535fn apply_effect(effect: &RegistryEffect, mut handles: ProviderHandles) -> Result<(), TimerError> {
536    match effect {
537        RegistryEffect::None => restore_provider_handles(handles),
538        RegistryEffect::ArmWakeup {
539            token,
540            delay_ns,
541            replace,
542            ..
543        } => {
544            let detached_wakeup = handles.take_wakeup();
545            if *replace {
546                let replaced = match detached_wakeup {
547                    Some(handle) => Some(handle),
548                    None => with_registry_mut(|registry| {
549                        Ok(registry.take_wakeup_handle(token.identity()))
550                    })?,
551                };
552                if let Some(replaced) = replaced {
553                    clear_provider_handle(replaced);
554                }
555            } else if let Some(detached_wakeup) = detached_wakeup {
556                restore_provider_handle(detached_wakeup)?;
557            }
558            if let Some(work) = handles.take_work() {
559                restore_provider_handle(work)?;
560            }
561            arm_wakeup(token, *delay_ns, effect)
562        }
563        RegistryEffect::ClearCallbacks {
564            identity,
565            clear_wakeup,
566            clear_work,
567        } => {
568            let detached_wakeup = handles.take_wakeup();
569            if *clear_wakeup {
570                let wakeup = match detached_wakeup {
571                    Some(handle) => Some(handle),
572                    None => {
573                        with_registry_mut(|registry| Ok(registry.take_wakeup_handle(identity)))?
574                    }
575                };
576                if let Some(wakeup) = wakeup {
577                    clear_provider_handle(wakeup);
578                }
579            } else if let Some(wakeup) = detached_wakeup {
580                restore_provider_handle(wakeup)?;
581            }
582            let detached_work = handles.take_work();
583            if *clear_work {
584                let work = match detached_work {
585                    Some(handle) => Some(handle),
586                    None => with_registry_mut(|registry| Ok(registry.take_work_handle(identity)))?,
587                };
588                if let Some(work) = work {
589                    clear_provider_handle(work);
590                }
591            } else if let Some(work) = detached_work {
592                restore_provider_handle(work)?;
593            }
594            restore_provider_handles(handles)
595        }
596        RegistryEffect::DispatchWatchdog {
597            successor,
598            successor_delay_ns,
599            work,
600            ..
601        } => {
602            if let Some(wakeup) = handles.take_wakeup() {
603                clear_provider_handle(wakeup);
604            }
605            let replaced_work = handles.take_work().or(with_registry_mut(|registry| {
606                Ok(registry.take_work_handle(successor.identity()))
607            })?);
608            if let Some(replaced_work) = replaced_work {
609                clear_provider_handle(replaced_work);
610            }
611            dispatch_watchdog_effect(successor, *successor_delay_ns, work, effect)
612        }
613    }
614}
615
616fn arm_wakeup(
617    token: &CallbackToken,
618    delay_ns: u64,
619    effect: &RegistryEffect,
620) -> Result<(), TimerError> {
621    let task_token = token.clone();
622    let handle = platform::set_timer(Duration::from_nanos(delay_ns), async move {
623        dispatch_wakeup(task_token).await;
624    });
625    if let Err((error, handle)) = install_provider_handle(token, handle) {
626        platform::clear_timer(handle);
627        return Err(error);
628    }
629    if let Err(error) = confirm_effect(effect) {
630        let handle = with_registry_mut(|registry| {
631            registry
632                .take_wakeup_handle(token.identity())
633                .ok_or(TimerError::OwnershipInvariant)
634        })?;
635        clear_provider_handle(handle);
636        return Err(error);
637    }
638    Ok(())
639}
640
641fn dispatch_watchdog_effect(
642    successor: &CallbackToken,
643    successor_delay_ns: u64,
644    work: &CallbackToken,
645    effect: &RegistryEffect,
646) -> Result<(), TimerError> {
647    let successor_token = successor.clone();
648    let successor_handle =
649        platform::set_timer(Duration::from_nanos(successor_delay_ns), async move {
650            dispatch_watchdog_scheduler(&successor_token);
651        });
652    if let Err((error, handle)) = install_provider_handle(successor, successor_handle) {
653        platform::clear_timer(handle);
654        return Err(error);
655    }
656
657    let work_token = work.clone();
658    let work_handle = platform::set_timer(Duration::ZERO, async move {
659        dispatch_watchdog_work(&work_token);
660    });
661    if let Err((error, handle)) = install_provider_handle(work, work_handle) {
662        platform::clear_timer(handle);
663        clear_entry_provider_handles(successor.identity())?;
664        return Err(error);
665    }
666    if let Err(error) = confirm_effect(effect) {
667        clear_entry_provider_handles(successor.identity())?;
668        return Err(error);
669    }
670    Ok(())
671}
672
673fn install_provider_handle(
674    token: &CallbackToken,
675    handle: TimerHandle,
676) -> Result<(), (TimerError, TimerHandle)> {
677    RUNTIME.with(|runtime| {
678        let Ok(mut runtime) = runtime.try_borrow_mut() else {
679            return Err((TimerError::RuntimeBusy, handle));
680        };
681        let Some(registry) = runtime.as_mut() else {
682            return Err((TimerError::NotInitialized, handle));
683        };
684        match registry.install_provider_handle(token, handle) {
685            Ok(()) => Ok(()),
686            Err((error, handle)) => Err((TimerError::from(error), handle)),
687        }
688    })
689}
690
691fn confirm_effect(effect: &RegistryEffect) -> Result<(), TimerError> {
692    with_registry_mut(|registry| {
693        registry
694            .confirm_effect_applied(effect)
695            .map_err(TimerError::from)
696    })
697}
698
699fn restore_provider_handles(mut handles: ProviderHandles) -> Result<(), TimerError> {
700    if let Some(wakeup) = handles.take_wakeup() {
701        restore_provider_handle(wakeup)?;
702    }
703    if let Some(work) = handles.take_work() {
704        restore_provider_handle(work)?;
705    }
706    Ok(())
707}
708
709fn restore_provider_handle(handle: ProviderHandle) -> Result<(), TimerError> {
710    let (token, handle) = handle.into_parts();
711    match install_provider_handle(&token, handle) {
712        Ok(()) => Ok(()),
713        Err((error, handle)) => {
714            platform::clear_timer(handle);
715            Err(error)
716        }
717    }
718}
719
720fn clear_provider_handle(handle: ProviderHandle) {
721    let (_, handle) = handle.into_parts();
722    platform::clear_timer(handle);
723}
724
725fn clear_entry_provider_handles(identity: &TimerIdentity) -> Result<(), TimerError> {
726    let mut handles = with_registry_mut(|registry| {
727        Ok(ProviderHandles::from_parts(
728            registry.take_wakeup_handle(identity),
729            registry.take_work_handle(identity),
730        ))
731    })?;
732    if let Some(wakeup) = handles.take_wakeup() {
733        clear_provider_handle(wakeup);
734    }
735    if let Some(work) = handles.take_work() {
736        clear_provider_handle(work);
737    }
738    Ok(())
739}
740
741#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
742async fn dispatch_wakeup(token: CallbackToken) {
743    match token.role() {
744        CallbackRole::OrdinaryWork => dispatch_ordinary(token).await,
745        CallbackRole::WatchdogScheduler => dispatch_watchdog_scheduler(&token),
746        CallbackRole::WatchdogWork => {}
747    }
748}
749
750#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
751async fn dispatch_ordinary(token: CallbackToken) {
752    let instructions_before = platform::instruction_counter();
753    let accepted = with_registry_mut(|registry| {
754        registry.consume_provider_handle(&token);
755        Ok(registry.begin_ordinary(&token))
756    });
757    match accepted {
758        Ok(CallbackAcceptance::Accepted) => {}
759        Ok(CallbackAcceptance::Stale) => return,
760        Err(error) => trap_callback_failure("ordinary callback acceptance", &error),
761    }
762
763    let callback = match with_registry(|registry| {
764        registry.ordinary_callback(&token).map_err(TimerError::from)
765    }) {
766        Ok(callback) => callback,
767        Err(TimerError::OwnershipInvariant) => {
768            fail_ordinary_dispatch(&token);
769            return;
770        }
771        Err(error) => trap_callback_failure("ordinary callback lookup", &error),
772    };
773    let context = TimerContext::new(token.identity().clone(), token.claim_generation());
774    let future = {
775        let Ok(mut callback) = callback.try_borrow_mut() else {
776            fail_ordinary_dispatch(&token);
777            return;
778        };
779        callback(context)
780    };
781    let result = future.await;
782    let transition = with_registry_mut(|registry| {
783        registry
784            .complete_ordinary(&token, platform::time_ns(), result)
785            .map_err(TimerError::from)
786    });
787    let transition = transition
788        .unwrap_or_else(|error| trap_callback_failure("ordinary callback completion", &error));
789    finish_callback_transition(&token, transition, ProviderHandles::default());
790    record_work_instructions(&token, instructions_before);
791}
792
793fn fail_ordinary_dispatch(token: &CallbackToken) {
794    let transition = with_registry_mut(|registry| {
795        registry
796            .complete_ordinary(
797                token,
798                platform::time_ns(),
799                TimerRunResult::new(TimerCompletion::invariant_failure(0), TimerDirective::Stop),
800            )
801            .map_err(TimerError::from)
802    });
803    let transition = transition.unwrap_or_else(|error| {
804        trap_callback_failure("ordinary invariant-failure completion", &error)
805    });
806    finish_callback_transition(token, transition, ProviderHandles::default());
807}
808
809fn dispatch_watchdog_scheduler(token: &CallbackToken) {
810    let instructions_before = platform::instruction_counter();
811    let transition = with_registry_mut(|registry| {
812        registry.consume_provider_handle(token);
813        Ok(registry.begin_watchdog_scheduler(token, platform::time_ns()))
814    });
815    let transition = transition
816        .unwrap_or_else(|error| trap_callback_failure("watchdog scheduler transition", &error));
817    let accepted = !matches!(transition.effect(), RegistryEffect::None);
818    finish_callback_transition(token, transition, ProviderHandles::default());
819    if accepted {
820        let instructions = platform::instruction_counter().saturating_sub(instructions_before);
821        with_registry_mut(|registry| {
822            registry.record_scheduler_instructions(token, instructions);
823            Ok(())
824        })
825        .unwrap_or_else(|error| {
826            trap_callback_failure("watchdog scheduler instruction accounting", &error)
827        });
828    }
829}
830
831fn dispatch_watchdog_work(token: &CallbackToken) {
832    let instructions_before = platform::instruction_counter();
833    let accepted = with_registry_mut(|registry| {
834        registry.consume_provider_handle(token);
835        Ok(registry.begin_watchdog_work(token))
836    });
837    match accepted {
838        Ok(CallbackAcceptance::Accepted) => {}
839        Ok(CallbackAcceptance::Stale) => return,
840        Err(error) => trap_callback_failure("watchdog work acceptance", &error),
841    }
842
843    let callback =
844        match with_registry(|registry| registry.watchdog_callback(token).map_err(TimerError::from))
845        {
846            Ok(callback) => callback,
847            Err(error) => trap_callback_failure("watchdog callback lookup", &error),
848        };
849    let context = TimerContext::new(token.identity().clone(), token.claim_generation());
850    let result = {
851        let Ok(mut callback) = callback.try_borrow_mut() else {
852            trap_callback_failure(
853                "watchdog callback ownership",
854                &TimerError::OwnershipInvariant,
855            );
856        };
857        callback(context)
858    };
859    finish_watchdog_dispatch(token, result);
860    record_work_instructions(token, instructions_before);
861}
862
863fn finish_watchdog_dispatch(token: &CallbackToken, result: WatchdogRunResult) {
864    let claim = RegistrationClaim::delegated(token.identity().clone(), token.claim_generation());
865    let completed = with_registry_mut(|registry| {
866        let handles = registry
867            .take_provider_handles_for_claim(&claim)
868            .map_err(TimerError::from)?;
869        #[cfg(test)]
870        {
871            if take_watchdog_completion_fault() {
872                return Err(TimerError::OwnershipInvariant);
873            }
874        }
875        let transition = registry
876            .complete_watchdog_work(token, platform::time_ns(), result)
877            .map_err(TimerError::from)?;
878        Ok((transition, handles))
879    });
880    let (transition, handles) =
881        completed.unwrap_or_else(|error| trap_callback_failure("watchdog work completion", &error));
882    finish_callback_transition(token, transition, handles);
883}
884
885fn finish_callback_transition(
886    token: &CallbackToken,
887    transition: RegistryTransition,
888    handles: ProviderHandles,
889) {
890    match finish_transition(transition, handles) {
891        Ok(()) | Err(TimerError::ControlFailure(_)) => {}
892        Err(
893            error @ (TimerError::NotInitialized
894            | TimerError::RuntimeBusy
895            | TimerError::Register(_)
896            | TimerError::Schedule(_)
897            | TimerError::RegistrationExpired
898            | TimerError::WrongPolicy
899            | TimerError::OwnershipInvariant
900            | TimerError::ReconciliationConflict),
901        ) => {
902            if token.role() == CallbackRole::WatchdogWork {
903                trap_callback_failure("watchdog provider-handle completion", &error);
904            }
905            fail_provider_binding(token).unwrap_or_else(|binding_error| {
906                trap_callback_failure("provider-binding failure cleanup", &binding_error)
907            });
908        }
909    }
910}
911
912fn fail_provider_binding(token: &CallbackToken) -> Result<(), TimerError> {
913    let claim = RegistrationClaim::delegated(token.identity().clone(), token.claim_generation());
914    let failed = with_registry_mut(|registry| {
915        registry
916            .fail_registration(&claim, TimerControlFailure::ProviderBindingFailed)
917            .map_err(TimerError::from)
918    });
919    let mut handles = failed?;
920    if let Some(wakeup) = handles.take_wakeup() {
921        clear_provider_handle(wakeup);
922    }
923    if let Some(work) = handles.take_work() {
924        clear_provider_handle(work);
925    }
926    Ok(())
927}
928
929fn record_work_instructions(token: &CallbackToken, instructions_before: u64) {
930    let instructions = platform::instruction_counter().saturating_sub(instructions_before);
931    with_registry_mut(|registry| {
932        registry.record_work_instructions(token, instructions);
933        Ok(())
934    })
935    .unwrap_or_else(|error| trap_callback_failure("work instruction accounting", &error));
936}
937
938fn trap_callback_failure(context: &str, error: &TimerError) -> ! {
939    platform::trap(&format!("ic-timers {context} failed: {error}"))
940}
941
942fn with_registry<T>(
943    operation: impl FnOnce(&TimerRegistry) -> Result<T, TimerError>,
944) -> Result<T, TimerError> {
945    RUNTIME.with(|runtime| {
946        let runtime = runtime.try_borrow().map_err(|_| TimerError::RuntimeBusy)?;
947        let registry = runtime.as_ref().ok_or(TimerError::NotInitialized)?;
948        operation(registry)
949    })
950}
951
952fn with_registry_mut<T>(
953    operation: impl FnOnce(&mut TimerRegistry) -> Result<T, TimerError>,
954) -> Result<T, TimerError> {
955    RUNTIME.with(|runtime| {
956        let mut runtime = runtime
957            .try_borrow_mut()
958            .map_err(|_| TimerError::RuntimeBusy)?;
959        let registry = runtime.as_mut().ok_or(TimerError::NotInitialized)?;
960        operation(registry)
961    })
962}
963
964#[cfg(test)]
965fn reset_for_test(now_ns: u64, canister_version: u64) {
966    platform::reset(now_ns, canister_version);
967    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(false));
968    RUNTIME.with(|runtime| {
969        *runtime.borrow_mut() = None;
970    });
971}
972
973#[cfg(test)]
974thread_local! {
975    static WATCHDOG_COMPLETION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
976}
977
978#[cfg(test)]
979fn inject_watchdog_completion_fault() {
980    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(true));
981}
982
983#[cfg(test)]
984fn take_watchdog_completion_fault() -> bool {
985    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.replace(false))
986}
987
988#[cfg(test)]
989mod tests;