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