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