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