Skip to main content

ic_timers/runtime/
mod.rs

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