Skip to main content

ic_timers/runtime/
mod.rs

1//! Canister-local owner and live timer execution.
2
3use crate::{
4    callback::{AfterCompletionRunResult, OnceRunResult, OrdinaryRunResult},
5    platform::{self, TimerHandle},
6    registry::{
7        CallbackRole, CallbackToken, OrdinaryCallback, ProviderHandle, ProviderHandles,
8        RegisterError, RegistrationClaim, RegistryEffect, RegistryError, RegistryTransition,
9        TimerRegistry, WakeupToken, WatchdogCallback,
10    },
11    schedule::{OrdinaryDirective, ScheduleError, TimerCadence, TimerSchedule},
12    snapshot::{
13        DeclarationLifetime, MemoryPageExtent, TimerCompletion, TimerControlFailure, TimerEpoch,
14        TimerIdentity, TimerInventorySnapshot, TimerPolicy, TimerSnapshot, WatchdogRunResult,
15    },
16};
17use std::{cell::RefCell, future::Future, rc::Rc, time::Duration};
18use thiserror::Error;
19
20thread_local! {
21    static RUNTIME: RefCell<Option<TimerRegistry>> = const { RefCell::new(None) };
22}
23
24/// Initial schedule and declaration retention for a direct registration.
25///
26/// Retained declarations may start inactive. A declaration removed when stopped
27/// always starts with a schedule; all three registration policies use this same
28/// configuration. Durations and deadlines are checked when registering.
29///
30/// A remove-on-stop declaration cannot start inactive:
31///
32/// ```compile_fail,E0308
33/// use ic_timers::TimerStart;
34/// let start = TimerStart::RemoveWhenStopped { initial: None };
35/// ```
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum TimerStart {
38    /// Keep callback authority after stopping, optionally starting scheduled.
39    Retained {
40        /// Initial wake-up, or `None` for an inactive declaration.
41        initial: Option<TimerSchedule>,
42    },
43    /// Start scheduled and remove callback authority after stopping.
44    RemoveWhenStopped {
45        /// Required initial wake-up.
46        initial: TimerSchedule,
47    },
48}
49
50impl TimerStart {
51    const fn into_parts(self) -> (DeclarationLifetime, Option<TimerSchedule>) {
52        match self {
53            Self::Retained { initial } => (DeclarationLifetime::Retained, initial),
54            Self::RemoveWhenStopped { initial } => {
55                (DeclarationLifetime::RemoveWhenStopped, Some(initial))
56            }
57        }
58    }
59}
60
61/// Failure from the canister-local timer runtime API.
62#[non_exhaustive]
63#[derive(Debug, Error)]
64pub enum TimerError {
65    /// The lifecycle owner has not initialized the volatile runtime.
66    #[error("timer runtime is not initialized")]
67    NotInitialized,
68    /// A nested internal borrow indicates unsupported re-entrancy.
69    #[error("timer runtime is already borrowed")]
70    RuntimeBusy,
71    /// Claiming one canonical timer identity failed.
72    #[error(transparent)]
73    Register(#[from] RegisterError),
74    /// Cadence or deadline validation failed.
75    #[error(transparent)]
76    Schedule(#[from] ScheduleError),
77    /// The logical registration was removed or superseded.
78    #[error("timer registration is no longer authoritative")]
79    RegistrationExpired,
80    /// Pure checked control reached a terminal failure after effects were applied.
81    #[error("timer control failed: {0:?}")]
82    ControlFailure(TimerControlFailure),
83    /// Canonical callback or provider-handle ownership was internally inconsistent.
84    #[error("timer runtime ownership invariant failed")]
85    OwnershipInvariant,
86    /// The bounded pending-callback store is full.
87    #[error("timer backend pending capacity exhausted")]
88    BackendCapacity,
89    /// The backend cannot allocate another non-reusable callback identity.
90    #[error("timer backend callback identities exhausted")]
91    BackendIdentityExhausted,
92    /// A retained lifecycle claim no longer matches its canonical declaration.
93    #[error("timer lifecycle reconciliation conflicts with the canonical declaration")]
94    ReconciliationConflict,
95}
96
97impl From<platform::AdmissionError> for TimerError {
98    fn from(error: platform::AdmissionError) -> Self {
99        match error {
100            platform::AdmissionError::Capacity => Self::BackendCapacity,
101            platform::AdmissionError::IdentityExhausted => Self::BackendIdentityExhausted,
102            platform::AdmissionError::DeadlineOverflow => {
103                Self::Schedule(ScheduleError::DeadlineOverflow)
104            }
105        }
106    }
107}
108
109impl From<RegistryError> for TimerError {
110    fn from(value: RegistryError) -> Self {
111        match value {
112            RegistryError::UnknownRegistration
113            | RegistryError::StaleRegistration
114            | RegistryError::StaleCallback => Self::RegistrationExpired,
115            RegistryError::PolicyMismatch { .. } | RegistryError::ProviderHandleAlreadyOwned => {
116                Self::OwnershipInvariant
117            }
118            RegistryError::Schedule(error) => Self::Schedule(error),
119        }
120    }
121}
122
123/// Initialize the volatile canister-local runtime once for this Wasm instance.
124///
125/// Repeated calls are idempotent and return the original epoch. This function
126/// exports no lifecycle hook; the canister's existing lifecycle owner calls it.
127pub fn initialize_runtime() -> Result<TimerEpoch, TimerError> {
128    RUNTIME.with(|runtime| {
129        let mut runtime = runtime
130            .try_borrow_mut()
131            .map_err(|_| TimerError::RuntimeBusy)?;
132        let registry = runtime.get_or_insert_with(|| {
133            TimerRegistry::new(TimerEpoch::new(
134                platform::canister_version(),
135                platform::time_ns(),
136            ))
137        });
138        Ok(registry.epoch())
139    })
140}
141
142/// Delegated control capability scoped to one exact `Once` work attempt.
143///
144/// Identity remains inspectable after work returns or is abandoned, but mutation
145/// methods then return [`TimerError::RegistrationExpired`]. Retain the
146/// [`OnceRegistration`] for longer-lived ownership.
147pub struct OnceContext {
148    token: CallbackToken,
149}
150
151impl OnceContext {
152    const fn new(token: CallbackToken) -> Self {
153        Self { token }
154    }
155
156    /// Return the logical timer identity executing this work.
157    #[must_use]
158    pub const fn identity(&self) -> &TimerIdentity {
159        self.token.identity()
160    }
161
162    /// Schedule a `Once` declaration while this exact work attempt is active.
163    ///
164    /// A context retained after completion or delivery abandonment is expired
165    /// and returns [`TimerError::RegistrationExpired`].
166    pub fn ensure_scheduled(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
167        ensure_once_claim(self.token.claim(), Some(&self.token), schedule)
168    }
169
170    /// Reconcile the executing declaration to one exact schedule.
171    ///
172    /// A winning exact schedule replaces the returned scheduling proposal
173    /// before validation. Explicit consumer invariant failure still stops.
174    ///
175    /// `None` requests inactive state at normal completion. Retained callback
176    /// authority remains; a remove-on-stop declaration is removed when that
177    /// cancellation wins arbitration. A stored context cannot mutate the
178    /// registration after its exact work attempt ends.
179    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
180        reconcile_ordinary_claim(self.token.claim(), Some(&self.token), schedule)
181    }
182
183    /// Request cancellation while this exact work attempt is active.
184    /// Cancellation allocates no callback generation.
185    ///
186    /// Cancellation does not interrupt the current invocation; normal
187    /// completion applies it before any callback successor is retained.
188    /// A retained declaration becomes inactive. A remove-on-stop declaration
189    /// is removed when cancellation wins arbitration.
190    pub fn cancel(&self) -> Result<(), TimerError> {
191        cancel_claim(self.token.claim(), Some(&self.token))
192    }
193}
194
195/// Delegated control capability scoped to one exact after-completion work
196/// attempt.
197///
198/// Mutation authority expires when the callback completes or its delivery is
199/// abandoned. Retain the [`AfterCompletionRegistration`] for longer-lived
200/// ownership.
201pub struct AfterCompletionContext {
202    token: CallbackToken,
203}
204
205impl AfterCompletionContext {
206    const fn new(token: CallbackToken) -> Self {
207        Self { token }
208    }
209
210    /// Return the logical timer identity executing this work.
211    #[must_use]
212    pub const fn identity(&self) -> &TimerIdentity {
213        self.token.identity()
214    }
215
216    /// Request configured recurrence after this exact work attempt.
217    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
218        ensure_recurring_claim(self.token.claim(), Some(&self.token))
219    }
220
221    /// Reconcile the executing declaration to one exact schedule without
222    /// changing its configured after-completion cadence.
223    ///
224    /// A winning exact schedule replaces the returned scheduling proposal
225    /// before validation. Explicit consumer invariant failure still stops.
226    ///
227    /// `None` requests inactive state at normal completion. A retained
228    /// declaration keeps its callback authority; a remove-on-stop declaration
229    /// is removed when cancellation wins arbitration. A stored context cannot
230    /// mutate the registration after its exact work attempt ends.
231    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
232        reconcile_ordinary_claim(self.token.claim(), Some(&self.token), schedule)
233    }
234
235    /// Request cancellation while this exact work attempt is active.
236    /// Cancellation allocates no callback generation.
237    ///
238    /// Cancellation does not interrupt the current invocation; normal
239    /// completion applies it before any callback successor is retained. A
240    /// retained declaration becomes inactive; a remove-on-stop declaration is
241    /// removed when cancellation wins arbitration.
242    pub fn cancel(&self) -> Result<(), TimerError> {
243        cancel_claim(self.token.claim(), Some(&self.token))
244    }
245}
246
247/// Delegated control capability scoped to one exact watchdog work attempt.
248///
249/// Mutation authority expires when the callback completes. Retain the
250/// [`WatchdogRegistration`] for longer-lived ownership.
251pub struct WatchdogContext {
252    token: CallbackToken,
253}
254
255impl WatchdogContext {
256    const fn new(token: CallbackToken) -> Self {
257        Self { token }
258    }
259
260    /// Return the logical timer identity executing this work.
261    #[must_use]
262    pub const fn identity(&self) -> &TimerIdentity {
263        self.token.identity()
264    }
265
266    /// Request that the pre-armed cadence successor remain scheduled.
267    ///
268    /// This is idempotent unless it supersedes a nested cancellation request.
269    /// It never arms an additional Watchdog successor and cannot move a
270    /// pending immediate successor later.
271    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
272        ensure_recurring_claim(self.token.claim(), Some(&self.token))
273    }
274
275    /// Request that this work attempt's pre-armed successor run immediately.
276    ///
277    /// Repeated requests coalesce, and a cadence ensure cannot move the
278    /// pending immediate deadline later. Normal completion replaces the exact
279    /// cadence successor with a zero-delay scheduler callback. Consumer work
280    /// still runs only in the scheduler's separately queued later message.
281    pub fn ensure_scheduled_immediately(&self) -> Result<(), TimerError> {
282        ensure_watchdog_immediately_claim(self.token.claim(), Some(&self.token))
283    }
284
285    /// Replace this attempt's successor with an exact schedule, or cancel it.
286    ///
287    /// Applied on normal completion; a trap preserves the committed cadence
288    /// successor. The latest reconciliation wins over prior scheduling demand,
289    /// but unregistration and invariant failure remain terminal.
290    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
291        reconcile_watchdog_claim(self.token.claim(), Some(&self.token), schedule)
292    }
293
294    /// Request cancellation while this exact work attempt is active.
295    /// Cancellation allocates no callback generation.
296    ///
297    /// Cancellation does not interrupt the current invocation; normal
298    /// completion clears the already-armed successor when cancellation wins.
299    /// A retained declaration becomes inactive; a remove-on-stop declaration
300    /// is removed.
301    pub fn cancel(&self) -> Result<(), TimerError> {
302        cancel_claim(self.token.claim(), Some(&self.token))
303    }
304}
305
306/// Opaque non-clone claim for one registered `Once` callback.
307#[must_use = "retain the registration claim so the timer remains controllable"]
308pub struct OnceRegistration {
309    claim: RegistrationClaim,
310}
311
312impl OnceRegistration {
313    /// Return the claimed logical identity.
314    #[must_use]
315    pub const fn identity(&self) -> &TimerIdentity {
316        self.claim.identity()
317    }
318
319    /// Return whether this exact claim currently owns an armed provider wake-up.
320    ///
321    /// This is a volatile observation, not durable scheduling authority or a
322    /// delivery guarantee. Call [`Self::ensure_scheduled`] unconditionally when
323    /// a wake-up is required rather than using this value as a scheduling guard.
324    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
325        has_armed_wakeup_claim(&self.claim)
326    }
327
328    /// Ensure one invocation is armed or retained as the running work's successor.
329    pub fn ensure_scheduled(&self, schedule: TimerSchedule) -> Result<(), TimerError> {
330        ensure_once_claim(&self.claim, None, schedule)
331    }
332
333    /// Reconcile to one exact desired schedule, replacing a later or earlier
334    /// live deadline as necessary.
335    ///
336    /// During running work, a winning exact schedule replaces the callback
337    /// scheduling proposal before validation; explicit invariant failure stops.
338    ///
339    /// `None` leaves a retained declaration inactive after any running work
340    /// completes or is abandoned. A remove-on-stop declaration is removed and
341    /// this claim expires when the transition finalizes.
342    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
343        reconcile_ordinary_claim(&self.claim, None, schedule)
344    }
345
346    /// Cancel the armed callback or the running work's successor.
347    /// Cancellation allocates no callback generation, even at the counter limit.
348    ///
349    /// Consumer work already running is not interrupted.
350    /// A retained declaration keeps callback authority. A remove-on-stop
351    /// declaration and this claim expire when cancellation finalizes.
352    pub fn cancel(&self) -> Result<(), TimerError> {
353        cancel_claim(&self.claim, None)
354    }
355
356    /// Consume the claim and unregister its callback authority.
357    ///
358    /// When called from running work, removal is deferred until that invocation
359    /// completes normally or its confirmed delivery is abandoned.
360    pub fn unregister(self) -> Result<(), TimerError> {
361        unregister_claim(&self.claim)
362    }
363}
364
365/// Opaque non-clone claim for one callback with configured
366/// after-completion recurrence.
367#[must_use = "retain the registration claim so the timer remains controllable"]
368pub struct AfterCompletionRegistration {
369    claim: RegistrationClaim,
370}
371
372/// Opaque non-clone claim for one pre-armed watchdog callback.
373#[must_use = "retain the registration claim so the timer remains controllable"]
374pub struct WatchdogRegistration {
375    claim: RegistrationClaim,
376}
377
378trait RegistrationClaimOwner {
379    fn registration_claim(&self) -> &RegistrationClaim;
380}
381
382impl RegistrationClaimOwner for OnceRegistration {
383    fn registration_claim(&self) -> &RegistrationClaim {
384        &self.claim
385    }
386}
387
388impl RegistrationClaimOwner for AfterCompletionRegistration {
389    fn registration_claim(&self) -> &RegistrationClaim {
390        &self.claim
391    }
392}
393
394impl RegistrationClaimOwner for WatchdogRegistration {
395    fn registration_claim(&self) -> &RegistrationClaim {
396        &self.claim
397    }
398}
399
400/// Desired volatile after-completion state during lifecycle reconciliation.
401#[derive(Clone, Copy, Debug, Eq, PartialEq)]
402pub enum TimerReconcileState {
403    /// Request inactive state immediately unless consumer work is running.
404    Inactive,
405    /// Preserve scheduling demand now or through the running work's successor.
406    Scheduled,
407}
408
409/// Desired volatile watchdog state during synchronous lifecycle reconciliation.
410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
411pub enum WatchdogReconcileState {
412    /// Request inactive state immediately unless consumer work is running.
413    Inactive,
414    /// Preserve or arm one scheduler wake-up at the configured cadence.
415    Scheduled,
416    /// Preserve an earlier wake-up or arm/move one scheduler wake-up to now.
417    ///
418    /// The zero-delay provider callback executes as a later replicated
419    /// scheduler message and does not invoke consumer work synchronously.
420    ScheduledImmediately,
421    /// Reconcile the scheduler to an exact absolute IC timestamp.
422    ScheduledAt(u64),
423}
424
425impl WatchdogRegistration {
426    /// Return the claimed logical identity.
427    #[must_use]
428    pub const fn identity(&self) -> &TimerIdentity {
429        self.claim.identity()
430    }
431
432    /// Return whether this exact claim currently owns an armed scheduler wake-up.
433    ///
434    /// The separately queued work callback is not itself a wake-up. During
435    /// watchdog work this returns `true` only because the scheduler has already
436    /// committed and installed the cadence successor. This is a volatile
437    /// observation, not a delivery guarantee.
438    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
439        has_armed_wakeup_claim(&self.claim)
440    }
441
442    /// Synchronously ensure one watchdog scheduler wake-up is authoritative.
443    ///
444    /// An existing earlier immediate deadline is retained and never delayed.
445    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
446        ensure_recurring_claim(&self.claim, None)
447    }
448
449    /// Synchronously ensure one watchdog scheduler wake-up is due now.
450    ///
451    /// An inactive declaration arms one zero-delay scheduler. A later cadence
452    /// deadline is replaced in place; an already earlier or equivalent
453    /// deadline and repeated immediate requests coalesce. If work is already
454    /// dispatched, that zero-delay work satisfies the request. If work is
455    /// running, the request applies to that attempt's pre-armed successor.
456    pub fn ensure_scheduled_immediately(&self) -> Result<(), TimerError> {
457        ensure_watchdog_immediately_claim(&self.claim, None)
458    }
459
460    /// Reconcile to one exact scheduler deadline, or cancel with `None`.
461    ///
462    /// Unlike ensure, this can move a wake-up later. Identical deadlines
463    /// coalesce. Dispatched or running work is not postponed: reconciliation
464    /// selects its successor on normal completion. Until then, the committed
465    /// cadence successor remains the recovery wake-up. A subsequent recovery
466    /// attempt retires the interrupted attempt's pending proposal.
467    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
468        reconcile_watchdog_claim(&self.claim, None, schedule)
469    }
470
471    /// Cancel the scheduler and any work callback that has not started.
472    /// Cancellation allocates no callback generation, even at the counter limits.
473    ///
474    /// Consumer work already running is not interrupted; normal completion
475    /// clears its pre-armed successor.
476    /// A retained declaration keeps callback authority. A remove-on-stop
477    /// declaration and this claim expire when cancellation finalizes.
478    pub fn cancel(&self) -> Result<(), TimerError> {
479        cancel_claim(&self.claim, None)
480    }
481
482    /// Consume the claim and unregister its callback authority.
483    ///
484    /// When called from running work, removal is deferred until that invocation
485    /// completes normally.
486    pub fn unregister(self) -> Result<(), TimerError> {
487        unregister_claim(&self.claim)
488    }
489}
490
491impl AfterCompletionRegistration {
492    /// Return the claimed logical identity.
493    #[must_use]
494    pub const fn identity(&self) -> &TimerIdentity {
495        self.claim.identity()
496    }
497
498    /// Return whether this exact claim currently owns an armed provider wake-up.
499    ///
500    /// A callback currently running without an installed successor returns
501    /// `false`. This is a volatile observation, not durable scheduling authority
502    /// or a delivery guarantee.
503    pub fn has_armed_wakeup(&self) -> Result<bool, TimerError> {
504        has_armed_wakeup_claim(&self.claim)
505    }
506
507    /// Ensure configured recurrence is armed or retained as the running work's
508    /// successor.
509    pub fn ensure_scheduled(&self) -> Result<(), TimerError> {
510        ensure_recurring_claim(&self.claim, None)
511    }
512
513    /// Reconcile to one exact desired schedule without changing the configured
514    /// after-completion cadence. `None` makes a retained declaration inactive
515    /// after any running work completes or is abandoned; it removes a
516    /// remove-on-stop declaration and expires this claim when the transition
517    /// finalizes.
518    ///
519    /// During running work, a winning exact schedule replaces the callback
520    /// scheduling proposal before validation; explicit invariant failure stops.
521    pub fn reconcile_schedule(&self, schedule: Option<TimerSchedule>) -> Result<(), TimerError> {
522        reconcile_ordinary_claim(&self.claim, None, schedule)
523    }
524
525    /// Cancel the armed callback or the running work's successor.
526    /// Cancellation allocates no callback generation, even at the counter limit.
527    ///
528    /// Consumer work already running is not interrupted.
529    /// A retained declaration keeps callback authority. A remove-on-stop
530    /// declaration and this claim expire when cancellation finalizes.
531    pub fn cancel(&self) -> Result<(), TimerError> {
532        cancel_claim(&self.claim, None)
533    }
534
535    /// Consume the claim and unregister its callback authority.
536    ///
537    /// When called from running work, removal is deferred until that invocation
538    /// completes normally or its confirmed delivery is abandoned.
539    pub fn unregister(self) -> Result<(), TimerError> {
540        unregister_claim(&self.claim)
541    }
542}
543
544/// Register one asynchronous `Once` callback with its initial schedule.
545///
546/// [`TimerStart::Retained`] permits an inactive declaration;
547/// [`TimerStart::RemoveWhenStopped`] always supplies its initial schedule.
548/// Success returns installed callback authority and,
549/// when requested, a confirmed provider wakeup. A returned error retires this
550/// creation's declaration and handles before releasing callback captures.
551///
552/// The future must return [`OnceRunResult`]; a recurring policy's result cannot
553/// be used here, even when its decision is Stop:
554///
555/// ```compile_fail,E0271
556/// use ic_timers::{register_once, TimerIdentity, TimerStart,
557///     TimerCompletion, AfterCompletionDecision, AfterCompletionRunResult};
558/// let identity = TimerIdentity::try_new("app", "jobs", "once").unwrap();
559/// let _ = register_once(identity, TimerStart::Retained { initial: None }, |_| async {
560///     AfterCompletionRunResult::new(TimerCompletion::no_work(), AfterCompletionDecision::Stop)
561/// });
562/// ```
563pub fn register_once<F, Fut>(
564    identity: TimerIdentity,
565    start: TimerStart,
566    callback: F,
567) -> Result<OnceRegistration, TimerError>
568where
569    F: FnMut(OnceContext) -> Fut + 'static,
570    Fut: Future<Output = OnceRunResult> + 'static,
571{
572    let callback = erase_ordinary_callback(callback, OnceContext::new, OrdinaryRunResult::from);
573    let claim = register_initial(start, |registry, lifetime| {
574        registry.register_once_with_callback(identity, lifetime, Rc::clone(&callback))
575    });
576    // Release factory captures only after initial registry/provider cleanup.
577    drop(callback);
578    Ok(OnceRegistration { claim: claim? })
579}
580
581/// Register one asynchronous callback with configured after-completion recurrence.
582///
583/// The explicit initial schedule selects the first deadline independently of
584/// the recurrence cadence. [`TimerStart::Retained`] may start inactive.
585/// Initial scheduling failure removes the new declaration and provider handles.
586///
587/// The future must return [`AfterCompletionRunResult`]:
588///
589/// ```compile_fail,E0271
590/// use ic_timers::{register_after_completion, TimerIdentity, TimerCadence,
591///     TimerStart, TimerCompletion, OnceDecision, OnceRunResult};
592/// let identity = TimerIdentity::try_new("app", "jobs", "recurring").unwrap();
593/// let cadence = TimerCadence::from_nanos(5).unwrap();
594/// let _ = register_after_completion(identity, cadence, TimerStart::Retained { initial: None }, |_| async {
595///     OnceRunResult::new(TimerCompletion::no_work(), OnceDecision::Stop)
596/// });
597/// ```
598pub fn register_after_completion<F, Fut>(
599    identity: TimerIdentity,
600    cadence: TimerCadence,
601    start: TimerStart,
602    callback: F,
603) -> Result<AfterCompletionRegistration, TimerError>
604where
605    F: FnMut(AfterCompletionContext) -> Fut + 'static,
606    Fut: Future<Output = AfterCompletionRunResult> + 'static,
607{
608    let callback = erase_ordinary_callback(
609        callback,
610        AfterCompletionContext::new,
611        OrdinaryRunResult::from,
612    );
613    let claim = register_initial(start, |registry, lifetime| {
614        registry.register_after_completion_with_callback(
615            identity,
616            cadence,
617            lifetime,
618            Rc::clone(&callback),
619        )
620    });
621    // Release factory captures only after initial registry/provider cleanup.
622    drop(callback);
623    Ok(AfterCompletionRegistration { claim: claim? })
624}
625
626/// Register one synchronous pre-armed watchdog callback with its initial schedule.
627///
628/// The initial schedule selects the first scheduler message, never synchronous
629/// consumer work. [`TimerStart::Retained`] may start inactive. Initial
630/// scheduling failure removes the new declaration and provider handles.
631///
632/// The callback cannot be async: it runs only in the work message after a
633/// separate scheduler message has armed the next cadence successor. Its
634/// `WatchdogDecision` retains the cadence successor, replaces it with a
635/// deadline of now or an exact timestamp, or clears it.
636pub fn register_watchdog<F>(
637    identity: TimerIdentity,
638    cadence: TimerCadence,
639    start: TimerStart,
640    callback: F,
641) -> Result<WatchdogRegistration, TimerError>
642where
643    F: FnMut(WatchdogContext) -> WatchdogRunResult + 'static,
644{
645    let mut callback = callback;
646    let callback: WatchdogCallback = Rc::new(RefCell::new(move |token| {
647        callback(WatchdogContext::new(token))
648    }));
649    let claim = register_initial(start, |registry, lifetime| {
650        registry.register_watchdog_with_callback(identity, cadence, lifetime, Rc::clone(&callback))
651    });
652    // Release factory captures only after initial registry/provider cleanup.
653    drop(callback);
654    Ok(WatchdogRegistration { claim: claim? })
655}
656
657/// Finish creation before exposing a claim. The callback factory retains its
658/// captures until this returns, including through rejected provider binding.
659fn register_initial(
660    start: TimerStart,
661    register: impl FnOnce(
662        &mut TimerRegistry,
663        DeclarationLifetime,
664    ) -> Result<RegistrationClaim, RegisterError>,
665) -> Result<RegistrationClaim, TimerError> {
666    let (lifetime, initial) = start.into_parts();
667    register_initialized(
668        |registry| {
669            let now_ns = platform::time_ns();
670            let requested = initial
671                .map(|schedule| schedule.resolve(now_ns))
672                .transpose()?;
673            Ok((register(registry, lifetime)?, (requested, now_ns)))
674        },
675        |registry, claim, (requested, now_ns)| {
676            requested
677                .map(|schedule| {
678                    registry
679                        .transition_claim(claim, |registry| {
680                            registry.reconcile_initial(claim, now_ns, schedule)
681                        })
682                        .map_err(TimerError::from)
683                })
684                .transpose()
685        },
686    )
687}
688
689/// Allocate and initialize under one borrow; publish authority only after effects.
690/// Fresh initialization failures share direct registration's retirement owner.
691fn register_initialized<Prepared>(
692    register: impl FnOnce(&mut TimerRegistry) -> Result<(RegistrationClaim, Prepared), TimerError>,
693    initialize: impl FnOnce(
694        &mut TimerRegistry,
695        &RegistrationClaim,
696        Prepared,
697    ) -> Result<Option<(RegistryTransition, ProviderHandles)>, TimerError>,
698) -> Result<RegistrationClaim, TimerError> {
699    let (claim, initialized) = with_registry_mut(|registry| {
700        let (claim, prepared) = register(registry)?;
701        let initialized = initialize(registry, &claim, prepared);
702        Ok((claim, initialized))
703    })?;
704    let applied = initialized.and_then(|transition| {
705        transition.map_or(Ok(()), |(transition, handles)| {
706            finish_transition(transition, handles)
707        })
708    });
709    applied.map_err(|error| cleanup_creation_error(&claim, error))?;
710    Ok(claim)
711}
712
713fn cleanup_creation_error(claim: &RegistrationClaim, error: TimerError) -> TimerError {
714    match unregister_claim(claim) {
715        Ok(()) | Err(TimerError::RegistrationExpired) => error,
716        Err(cleanup_error) => cleanup_error,
717    }
718}
719
720/// Reconstruct or reconcile one `Once` declaration synchronously.
721///
722/// `Some(schedule)` is authoritative and may move an existing deadline in
723/// either direction. `None` retains an inactive declaration in the canonical
724/// inventory, including on a fresh heap. Lifecycle reconciliation always owns
725/// a [`TimerStart::Retained`] declaration; transient
726/// `RemoveWhenStopped` callbacks use [`register_once`] directly.
727/// An empty owner slot is filled only after initial reconciliation succeeds.
728/// A returned error cleans up the new declaration and captures, leaving that
729/// slot empty. Later errors preserve an existing registration's authority.
730///
731/// Lifecycle reconstruction retains the same policy-specific result boundary:
732///
733/// ```compile_fail,E0271
734/// use ic_timers::{reconcile_once, TimerIdentity, TimerCompletion,
735///     AfterCompletionDecision, AfterCompletionRunResult};
736/// let identity = TimerIdentity::try_new("app", "jobs", "once").unwrap();
737/// let mut registration = None;
738/// let _ = reconcile_once(&mut registration, &identity, None, |_| async {
739///     AfterCompletionRunResult::new(TimerCompletion::no_work(), AfterCompletionDecision::Stop)
740/// });
741/// ```
742pub fn reconcile_once<F, Fut>(
743    registration: &mut Option<OnceRegistration>,
744    identity: &TimerIdentity,
745    desired: Option<TimerSchedule>,
746    callback: F,
747) -> Result<(), TimerError>
748where
749    F: FnMut(OnceContext) -> Fut + 'static,
750    Fut: Future<Output = OnceRunResult> + 'static,
751{
752    reconcile_registration(
753        registration,
754        identity,
755        TimerPolicy::Once,
756        || {
757            let callback =
758                erase_ordinary_callback(callback, OnceContext::new, OrdinaryRunResult::from);
759            let claim = register_initialized(
760                |registry| {
761                    Ok((
762                        registry.register_once_with_callback(
763                            identity.clone(),
764                            DeclarationLifetime::Retained,
765                            Rc::clone(&callback),
766                        )?,
767                        (),
768                    ))
769                },
770                |registry, claim, ()| {
771                    registry
772                        .transition_claim(claim, |registry| {
773                            registry.reconcile_ordinary(claim, platform::time_ns(), desired)
774                        })
775                        .map(Some)
776                        .map_err(TimerError::from)
777                },
778            );
779            drop(callback);
780            Ok(OnceRegistration { claim: claim? })
781        },
782        |registration| registration.reconcile_schedule(desired),
783    )
784}
785
786/// Reconstruct or reconcile one after-completion declaration synchronously.
787///
788/// The consumer owns `registration` in volatile state. A fresh Wasm heap has
789/// `None`, so this function creates callback authority and publishes it only
790/// after initial active or inactive reconciliation succeeds. A returned error
791/// cleans up a fresh declaration, leaving the slot empty. Later errors retain
792/// existing authority. A repeated call reuses the exact claim and does not
793/// replace its callback. The installed declaration is always retained;
794/// transient `RemoveWhenStopped` recurrence uses
795/// [`register_after_completion`] directly.
796///
797/// Lifecycle reconstruction also requires [`AfterCompletionRunResult`]:
798///
799/// ```compile_fail,E0271
800/// use ic_timers::{reconcile_after_completion, TimerIdentity, TimerCadence,
801///     TimerReconcileState, TimerCompletion, OnceDecision, OnceRunResult};
802/// let identity = TimerIdentity::try_new("app", "jobs", "recurring").unwrap();
803/// let cadence = TimerCadence::from_nanos(5).unwrap();
804/// let mut registration = None;
805/// let _ = reconcile_after_completion(&mut registration, &identity, cadence,
806///     TimerReconcileState::Inactive, |_| async {
807///         OnceRunResult::new(TimerCompletion::no_work(), OnceDecision::Stop)
808///     });
809/// ```
810pub fn reconcile_after_completion<F, Fut>(
811    registration: &mut Option<AfterCompletionRegistration>,
812    identity: &TimerIdentity,
813    cadence: TimerCadence,
814    desired: TimerReconcileState,
815    callback: F,
816) -> Result<(), TimerError>
817where
818    F: FnMut(AfterCompletionContext) -> Fut + 'static,
819    Fut: Future<Output = AfterCompletionRunResult> + 'static,
820{
821    reconcile_registration(
822        registration,
823        identity,
824        TimerPolicy::AfterCompletion { cadence },
825        || {
826            let callback = erase_ordinary_callback(
827                callback,
828                AfterCompletionContext::new,
829                OrdinaryRunResult::from,
830            );
831            let claim = register_initialized(
832                |registry| {
833                    Ok((
834                        registry.register_after_completion_with_callback(
835                            identity.clone(),
836                            cadence,
837                            DeclarationLifetime::Retained,
838                            Rc::clone(&callback),
839                        )?,
840                        (),
841                    ))
842                },
843                |registry, claim, ()| {
844                    registry
845                        .transition_claim(claim, |registry| match desired {
846                            TimerReconcileState::Inactive => registry.cancel(claim),
847                            TimerReconcileState::Scheduled => {
848                                registry.ensure_recurring(claim, platform::time_ns())
849                            }
850                        })
851                        .map(Some)
852                        .map_err(TimerError::from)
853                },
854            );
855            drop(callback);
856            Ok(AfterCompletionRegistration { claim: claim? })
857        },
858        |registration| match desired {
859            TimerReconcileState::Inactive => registration.cancel(),
860            TimerReconcileState::Scheduled => registration.ensure_scheduled(),
861        },
862    )
863}
864
865/// Reconstruct or reconcile one watchdog declaration synchronously.
866///
867/// Durable readiness remains consumer-owned. Fresh inactive authority still
868/// installs an observable retained declaration. `ScheduledImmediately` arms
869/// its first scheduler at deadline now without synchronous consumer work.
870/// `ScheduledAt` reconciles an exact deadline, including moving it later.
871/// This helper owns no lifecycle export and persists no policy, generation,
872/// provider handle, or callback.
873/// Transient `RemoveWhenStopped` watchdogs use [`register_watchdog`] directly.
874/// An empty owner slot is filled only after its initial state succeeds. A
875/// returned error cleans up the new declaration and captures, leaving the slot
876/// empty. Later errors retain existing authority and its failure observations.
877pub fn reconcile_watchdog<F>(
878    registration: &mut Option<WatchdogRegistration>,
879    identity: &TimerIdentity,
880    cadence: TimerCadence,
881    desired: WatchdogReconcileState,
882    callback: F,
883) -> Result<(), TimerError>
884where
885    F: FnMut(WatchdogContext) -> WatchdogRunResult + 'static,
886{
887    reconcile_registration(
888        registration,
889        identity,
890        TimerPolicy::Watchdog { cadence },
891        || {
892            let mut callback = callback;
893            let callback: WatchdogCallback = Rc::new(RefCell::new(move |token| {
894                callback(WatchdogContext::new(token))
895            }));
896            let claim = register_initialized(
897                |registry| {
898                    Ok((
899                        registry.register_watchdog_with_callback(
900                            identity.clone(),
901                            cadence,
902                            DeclarationLifetime::Retained,
903                            Rc::clone(&callback),
904                        )?,
905                        (),
906                    ))
907                },
908                |registry, claim, ()| {
909                    registry
910                        .transition_claim(claim, |registry| match desired {
911                            WatchdogReconcileState::Inactive => registry.cancel(claim),
912                            WatchdogReconcileState::Scheduled => {
913                                registry.ensure_recurring(claim, platform::time_ns())
914                            }
915                            WatchdogReconcileState::ScheduledImmediately => {
916                                registry.ensure_watchdog_immediately(claim, platform::time_ns())
917                            }
918                            WatchdogReconcileState::ScheduledAt(deadline_ns) => registry
919                                .reconcile_watchdog_schedule(
920                                    claim,
921                                    platform::time_ns(),
922                                    Some(TimerSchedule::At(deadline_ns)),
923                                ),
924                        })
925                        .map(Some)
926                        .map_err(TimerError::from)
927                },
928            );
929            drop(callback);
930            Ok(WatchdogRegistration { claim: claim? })
931        },
932        |registration| match desired {
933            WatchdogReconcileState::Inactive => registration.cancel(),
934            WatchdogReconcileState::Scheduled => registration.ensure_scheduled(),
935            WatchdogReconcileState::ScheduledImmediately => {
936                registration.ensure_scheduled_immediately()
937            }
938            WatchdogReconcileState::ScheduledAt(deadline_ns) => {
939                registration.reconcile_schedule(Some(TimerSchedule::At(deadline_ns)))
940            }
941        },
942    )
943}
944
945fn reconcile_registration<Registration>(
946    registration: &mut Option<Registration>,
947    identity: &TimerIdentity,
948    policy: TimerPolicy,
949    register: impl FnOnce() -> Result<Registration, TimerError>,
950    reconcile: impl FnOnce(&Registration) -> Result<(), TimerError>,
951) -> Result<(), TimerError>
952where
953    Registration: RegistrationClaimOwner,
954{
955    let Some(current) = registration.as_ref() else {
956        *registration = Some(register()?);
957        return Ok(());
958    };
959    let claim = current.registration_claim();
960    if claim.identity() != identity {
961        return Err(TimerError::ReconciliationConflict);
962    }
963    with_registry(|registry| {
964        registry
965            .declaration_matches(claim, policy, DeclarationLifetime::Retained)
966            .map_err(TimerError::from)?
967            .then_some(())
968            .ok_or(TimerError::ReconciliationConflict)
969    })?;
970    // Repeated calls retain the original callback. Release the unused factory
971    // outside the registry borrow, before applying control as in prior calls.
972    drop(register);
973    reconcile(current)
974}
975
976/// Return one coherent inert snapshot by identity.
977pub fn timer_snapshot(identity: &TimerIdentity) -> Result<Option<TimerSnapshot>, TimerError> {
978    with_registry(|registry| Ok(registry.snapshot(identity)))
979}
980
981/// Return one atomic bounded inventory with its volatile runtime epoch.
982///
983/// Timer snapshots are ordered deterministically by identity. An initialized
984/// empty registry still returns its epoch and an empty timer slice.
985pub fn timer_inventory() -> Result<TimerInventorySnapshot, TimerError> {
986    with_registry(|registry| Ok(registry.inventory()))
987}
988
989/// Return functional expected-failure state by identity without a full inventory.
990pub fn consecutive_expected_failures(identity: &TimerIdentity) -> Result<Option<u64>, TimerError> {
991    with_registry(|registry| Ok(registry.consecutive_expected_failures(identity)))
992}
993
994fn has_armed_wakeup_claim(claim: &RegistrationClaim) -> Result<bool, TimerError> {
995    with_registry(|registry| registry.has_armed_wakeup(claim).map_err(TimerError::from))
996}
997
998fn erase_ordinary_callback<Context: 'static, Output: 'static, F, Fut>(
999    mut callback: F,
1000    context: fn(CallbackToken) -> Context,
1001    erase_result: fn(Output) -> OrdinaryRunResult,
1002) -> OrdinaryCallback
1003where
1004    F: FnMut(Context) -> Fut + 'static,
1005    Fut: Future<Output = Output> + 'static,
1006{
1007    Rc::new(RefCell::new(move |token| {
1008        let future = callback(context(token));
1009        Box::pin(async move { erase_result(future.await) }) as _
1010    }))
1011}
1012
1013fn apply_claim_transition(
1014    claim: &RegistrationClaim,
1015    context: Option<&CallbackToken>,
1016    operation: impl FnOnce(&mut TimerRegistry) -> Result<RegistryTransition, RegistryError>,
1017) -> Result<(), TimerError> {
1018    let (transition, handles) = with_registry_mut(|registry| {
1019        if let Some(token) = context {
1020            registry
1021                .validate_running_context(token)
1022                .map_err(TimerError::from)?;
1023        }
1024        registry
1025            .transition_claim(claim, operation)
1026            .map_err(TimerError::from)
1027    })?;
1028    match finish_transition(transition, handles) {
1029        result @ (Ok(()) | Err(TimerError::ControlFailure(_))) => result,
1030        Err(error) => retire_failed_claim(claim, error),
1031    }
1032}
1033
1034fn ensure_once_claim(
1035    claim: &RegistrationClaim,
1036    context: Option<&CallbackToken>,
1037    schedule: TimerSchedule,
1038) -> Result<(), TimerError> {
1039    apply_claim_transition(claim, context, |registry| {
1040        registry.ensure_once(claim, platform::time_ns(), schedule)
1041    })
1042}
1043
1044fn reconcile_ordinary_claim(
1045    claim: &RegistrationClaim,
1046    context: Option<&CallbackToken>,
1047    schedule: Option<TimerSchedule>,
1048) -> Result<(), TimerError> {
1049    apply_claim_transition(claim, context, |registry| {
1050        registry.reconcile_ordinary(claim, platform::time_ns(), schedule)
1051    })
1052}
1053
1054fn ensure_recurring_claim(
1055    claim: &RegistrationClaim,
1056    context: Option<&CallbackToken>,
1057) -> Result<(), TimerError> {
1058    apply_claim_transition(claim, context, |registry| {
1059        registry.ensure_recurring(claim, platform::time_ns())
1060    })
1061}
1062
1063fn ensure_watchdog_immediately_claim(
1064    claim: &RegistrationClaim,
1065    context: Option<&CallbackToken>,
1066) -> Result<(), TimerError> {
1067    apply_claim_transition(claim, context, |registry| {
1068        registry.ensure_watchdog_immediately(claim, platform::time_ns())
1069    })
1070}
1071
1072fn reconcile_watchdog_claim(
1073    claim: &RegistrationClaim,
1074    context: Option<&CallbackToken>,
1075    schedule: Option<TimerSchedule>,
1076) -> Result<(), TimerError> {
1077    apply_claim_transition(claim, context, |registry| {
1078        registry.reconcile_watchdog_schedule(claim, platform::time_ns(), schedule)
1079    })
1080}
1081
1082fn cancel_claim(
1083    claim: &RegistrationClaim,
1084    context: Option<&CallbackToken>,
1085) -> Result<(), TimerError> {
1086    apply_claim_transition(claim, context, |registry| registry.cancel(claim))
1087}
1088
1089fn unregister_claim(claim: &RegistrationClaim) -> Result<(), TimerError> {
1090    apply_claim_transition(claim, None, |registry| registry.unregister(claim))
1091}
1092
1093fn retire_failed_claim(claim: &RegistrationClaim, error: TimerError) -> Result<(), TimerError> {
1094    match fail_claim_provider_binding(claim) {
1095        Ok(()) | Err(TimerError::RegistrationExpired) => Err(error),
1096        Err(cleanup_error) => Err(cleanup_error),
1097    }
1098}
1099
1100fn finish_transition(
1101    transition: RegistryTransition,
1102    handles: ProviderHandles,
1103) -> Result<(), TimerError> {
1104    let failure = transition.failure();
1105    let applied = apply_effect(transition.effect(), handles);
1106    // Callback capture destructors may inspect or control other timers. Drop
1107    // them after both the registry borrow and provider cleanup have ended.
1108    drop(transition);
1109    applied?;
1110    failure.map_or(Ok(()), |failure| Err(TimerError::ControlFailure(failure)))
1111}
1112
1113fn apply_effect(effect: &RegistryEffect, mut handles: ProviderHandles) -> Result<(), TimerError> {
1114    match effect {
1115        RegistryEffect::None => restore_provider_handles(handles),
1116        RegistryEffect::ArmWakeup {
1117            token,
1118            delay_ns,
1119            arm,
1120        } => {
1121            if arm.replaces_existing()
1122                && let Some(replaced) = handles.take_wakeup()
1123            {
1124                clear_provider_handle(replaced);
1125            }
1126            restore_provider_handles(handles)?;
1127            let handle = platform::try_set_timer(
1128                Duration::from_nanos(*delay_ns),
1129                dispatch_wakeup(token.clone()),
1130            )?;
1131            bind_provider_handle(token.token(), handle, Some(*delay_ns))
1132        }
1133        RegistryEffect::ClearCallbacks { handles: selected } => {
1134            if selected.includes_wakeup()
1135                && let Some(wakeup) = handles.take_wakeup()
1136            {
1137                clear_provider_handle(wakeup);
1138            }
1139            if selected.includes_work()
1140                && let Some(work) = handles.take_work()
1141            {
1142                clear_provider_handle(work);
1143            }
1144            restore_provider_handles(handles)
1145        }
1146        RegistryEffect::DispatchWatchdog {
1147            dispatch,
1148            successor_delay_ns,
1149        } => {
1150            let successor = dispatch.successor();
1151            let work = dispatch.work();
1152            if let Some(wakeup) = handles.take_wakeup() {
1153                clear_provider_handle(wakeup);
1154            }
1155            if let Some(replaced_work) = handles.take_work() {
1156                clear_provider_handle(replaced_work);
1157            }
1158            let successor_token = successor.clone();
1159            let successor_handle =
1160                platform::try_set_timer(Duration::from_nanos(*successor_delay_ns), async move {
1161                    dispatch_watchdog_scheduler(&successor_token);
1162                })?;
1163            bind_provider_handle(successor, successor_handle, None)?;
1164
1165            let work_token = work.clone();
1166            let work_handle = platform::try_set_timer(Duration::ZERO, async move {
1167                dispatch_watchdog_work(&work_token);
1168            })?;
1169            if let Err(error) = bind_provider_handle(&work, work_handle, Some(*successor_delay_ns))
1170            {
1171                clear_entry_provider_handles(successor.identity())?;
1172                return Err(error);
1173            }
1174            Ok(())
1175        }
1176    }
1177}
1178
1179/// Consume a provider handle by installing it or clearing it on rejection.
1180fn bind_provider_handle(
1181    token: &CallbackToken,
1182    handle: TimerHandle,
1183    armed_delay_ns: Option<u64>,
1184) -> Result<(), TimerError> {
1185    RUNTIME
1186        .with(|runtime| {
1187            #[cfg(test)]
1188            if take_provider_install_fault() {
1189                return Err((TimerError::OwnershipInvariant, handle));
1190            }
1191            let Ok(mut runtime) = runtime.try_borrow_mut() else {
1192                return Err((TimerError::RuntimeBusy, handle));
1193            };
1194            let Some(registry) = runtime.as_mut() else {
1195                return Err((TimerError::NotInitialized, handle));
1196            };
1197            match registry.install_provider_handle(token, handle, armed_delay_ns) {
1198                Ok(()) => Ok(()),
1199                Err((error, handle)) => Err((TimerError::from(error), handle)),
1200            }
1201        })
1202        .map_err(|(error, handle)| {
1203            // Release the registry borrow before calling the provider.
1204            platform::clear_timer(handle);
1205            error
1206        })
1207}
1208
1209fn restore_provider_handles(mut handles: ProviderHandles) -> Result<(), TimerError> {
1210    // Every detached linear capability must be restored or cleared even when
1211    // restoring an earlier handle fails.
1212    let wakeup_failure = handles
1213        .take_wakeup()
1214        .and_then(|handle| restore_provider_handle(handle).err());
1215    let work_failure = handles
1216        .take_work()
1217        .and_then(|handle| restore_provider_handle(handle).err());
1218    wakeup_failure.or(work_failure).map_or(Ok(()), Err)
1219}
1220
1221fn restore_provider_handle(handle: ProviderHandle) -> Result<(), TimerError> {
1222    let (token, handle) = handle.into_parts();
1223    bind_provider_handle(&token, handle, None)
1224}
1225
1226fn clear_provider_handle(handle: ProviderHandle) {
1227    let (_, handle) = handle.into_parts();
1228    platform::clear_timer(handle);
1229}
1230
1231fn clear_provider_handles(mut handles: ProviderHandles) {
1232    if let Some(wakeup) = handles.take_wakeup() {
1233        clear_provider_handle(wakeup);
1234    }
1235    if let Some(work) = handles.take_work() {
1236        clear_provider_handle(work);
1237    }
1238}
1239
1240fn clear_entry_provider_handles(identity: &TimerIdentity) -> Result<(), TimerError> {
1241    let handles = with_registry_mut(|registry| Ok(registry.take_provider_handles(identity)))?;
1242    clear_provider_handles(handles);
1243    Ok(())
1244}
1245
1246#[expect(
1247    clippy::future_not_send,
1248    reason = "IC callbacks and canister-local state are single-threaded."
1249)]
1250fn dispatch_wakeup(token: WakeupToken) -> impl Future<Output = ()> {
1251    // Construct outside the async body: a provider rejection can discard the
1252    // future before its first poll, after rolling registry acceptance back.
1253    let delivery = match token {
1254        WakeupToken::Ordinary(token) => WakeupDelivery::Ordinary(OrdinaryDelivery {
1255            token,
1256            completed: false,
1257        }),
1258        WakeupToken::Scheduler(token) => WakeupDelivery::Scheduler(token),
1259    };
1260    async move {
1261        match delivery {
1262            WakeupDelivery::Ordinary(mut delivery) => {
1263                dispatch_ordinary(&delivery.token).await;
1264                delivery.completed = true;
1265                // Capture the whole guard until normal completion.
1266                drop(delivery);
1267            }
1268            WakeupDelivery::Scheduler(token) => dispatch_watchdog_scheduler(&token),
1269        }
1270    }
1271}
1272
1273enum WakeupDelivery {
1274    Ordinary(OrdinaryDelivery),
1275    Scheduler(CallbackToken),
1276}
1277
1278struct OrdinaryDelivery {
1279    token: CallbackToken,
1280    completed: bool,
1281}
1282
1283impl Drop for OrdinaryDelivery {
1284    fn drop(&mut self) {
1285        if self.completed {
1286            return;
1287        }
1288        let removed = RUNTIME.with(|runtime| {
1289            let Ok(mut runtime) = runtime.try_borrow_mut() else {
1290                // Rejected binding may clear a detached scheduled handle
1291                // while another shared borrow exists. Its transition owns
1292                // cleanup; do not turn that typed failure into a Drop trap.
1293                let runtime = runtime.try_borrow().map_err(|_| TimerError::RuntimeBusy)?;
1294                return if runtime
1295                    .as_ref()
1296                    .is_some_and(|registry| registry.owns_ordinary_delivery(&self.token))
1297                {
1298                    Err(TimerError::RuntimeBusy)
1299                } else {
1300                    Ok(None)
1301                };
1302            };
1303            Ok(runtime
1304                .as_mut()
1305                .and_then(|registry| registry.abandon_ordinary(&self.token, platform::time_ns())))
1306        });
1307        let removed = removed
1308            .unwrap_or_else(|error| trap_callback_failure("ordinary delivery abandonment", &error));
1309        drop(removed);
1310    }
1311}
1312
1313#[expect(
1314    clippy::future_not_send,
1315    reason = "IC callbacks and canister-local state are single-threaded."
1316)]
1317async fn dispatch_ordinary(token: &CallbackToken) {
1318    let measurement = CallbackMeasurementStart::capture();
1319    let callback = with_registry_mut(|registry| Ok(registry.begin_ordinary(token)));
1320    let callback = match callback {
1321        Ok(Some(callback)) => callback,
1322        Ok(None) => return,
1323        Err(error) => trap_callback_failure("ordinary callback acceptance", &error),
1324    };
1325    let future = {
1326        let Ok(mut callback) = callback.try_borrow_mut() else {
1327            finish_ordinary_callback(
1328                token,
1329                OrdinaryRunResult::new(
1330                    TimerCompletion::invariant_failure(0),
1331                    OrdinaryDirective::Stop,
1332                ),
1333            );
1334            return;
1335        };
1336        callback(token.clone())
1337    };
1338    let result = future.await;
1339    finish_ordinary_callback(token, result);
1340    measurement.record(token);
1341}
1342
1343fn finish_ordinary_callback(token: &CallbackToken, result: OrdinaryRunResult) {
1344    let transition = with_registry_mut(|registry| {
1345        registry
1346            .complete_ordinary(token, platform::time_ns(), result)
1347            .map_err(TimerError::from)
1348    });
1349    let transition = transition
1350        .unwrap_or_else(|error| trap_callback_failure("ordinary callback completion", &error));
1351    finish_callback_transition(token, transition, ProviderHandles::default());
1352}
1353
1354fn dispatch_watchdog_scheduler(token: &CallbackToken) {
1355    let measurement = CallbackMeasurementStart::capture();
1356    let dispatched = with_registry_mut(|registry| {
1357        let handles = registry
1358            .take_watchdog_scheduler_handles(token)
1359            .map_err(TimerError::from)?;
1360        Ok((
1361            registry.begin_watchdog_scheduler(token, platform::time_ns()),
1362            handles,
1363        ))
1364    });
1365    let (transition, handles) = dispatched
1366        .unwrap_or_else(|error| trap_callback_failure("watchdog scheduler transition", &error));
1367    let accepted = !matches!(transition.effect(), RegistryEffect::None);
1368    finish_callback_transition(token, transition, handles);
1369    if accepted {
1370        measurement.record(token);
1371    }
1372}
1373
1374fn dispatch_watchdog_work(token: &CallbackToken) {
1375    let measurement = CallbackMeasurementStart::capture();
1376    let callback = with_registry_mut(|registry| Ok(registry.begin_watchdog_work(token)));
1377    let callback = match callback {
1378        Ok(Some(callback)) => callback,
1379        Ok(None) => return,
1380        Err(error) => trap_callback_failure("watchdog work acceptance", &error),
1381    };
1382    let result = {
1383        let Ok(mut callback) = callback.try_borrow_mut() else {
1384            trap_callback_failure(
1385                "watchdog callback ownership",
1386                &TimerError::OwnershipInvariant,
1387            );
1388        };
1389        callback(token.clone())
1390    };
1391    finish_watchdog_dispatch(token, result);
1392    measurement.record(token);
1393}
1394
1395fn finish_watchdog_dispatch(token: &CallbackToken, result: WatchdogRunResult) {
1396    // Unlike synchronous public control, an unexpected callback-completion
1397    // failure must trap. IC message rollback restores these temporarily
1398    // detached heap capabilities while the previously committed successor
1399    // remains armed by the scheduler message.
1400    let completed = with_registry_mut(|registry| {
1401        let handles = registry
1402            .take_provider_handles_for_claim(token.claim())
1403            .map_err(TimerError::from)?;
1404        #[cfg(test)]
1405        {
1406            if take_watchdog_completion_fault() {
1407                return Err(TimerError::OwnershipInvariant);
1408            }
1409        }
1410        let transition = registry
1411            .complete_watchdog_work(token, platform::time_ns(), result)
1412            .map_err(TimerError::from)?;
1413        Ok((transition, handles))
1414    });
1415    let (transition, handles) =
1416        completed.unwrap_or_else(|error| trap_callback_failure("watchdog work completion", &error));
1417    finish_callback_transition(token, transition, handles);
1418}
1419
1420fn finish_callback_transition(
1421    token: &CallbackToken,
1422    transition: RegistryTransition,
1423    handles: ProviderHandles,
1424) {
1425    // Registry control failures are already finalized. Other binding failures
1426    // follow the callback role's cleanup or rollback rule.
1427    match finish_transition(transition, handles) {
1428        Ok(()) | Err(TimerError::ControlFailure(_)) => {}
1429        Err(error) => {
1430            if token.role() == CallbackRole::WatchdogWork {
1431                trap_callback_failure("watchdog provider-handle completion", &error);
1432            }
1433            fail_claim_provider_binding(token.claim()).unwrap_or_else(|binding_error| {
1434                trap_callback_failure("provider-binding failure cleanup", &binding_error)
1435            });
1436        }
1437    }
1438}
1439
1440fn fail_claim_provider_binding(claim: &RegistrationClaim) -> Result<(), TimerError> {
1441    let failed = with_registry_mut(|registry| {
1442        registry
1443            .fail_registration(claim, TimerControlFailure::ProviderBindingFailed)
1444            .map_err(TimerError::from)
1445    });
1446    let (handles, transition) = failed?;
1447    clear_provider_handles(handles);
1448    drop(transition);
1449    Ok(())
1450}
1451
1452#[derive(Clone, Copy)]
1453struct CallbackMeasurementStart {
1454    instructions_before: u64,
1455    memory_start: MemoryPageExtent,
1456}
1457
1458impl CallbackMeasurementStart {
1459    fn capture() -> Self {
1460        // Keep page observation outside the established instruction interval.
1461        let memory_start = platform::memory_pages();
1462        let instructions_before = platform::instruction_counter();
1463        Self {
1464            instructions_before,
1465            memory_start,
1466        }
1467    }
1468
1469    fn record(self, token: &CallbackToken) {
1470        // Close the instruction interval before taking its paired end extent.
1471        let instructions = platform::instruction_counter().saturating_sub(self.instructions_before);
1472        let memory_end = platform::memory_pages();
1473        with_registry_mut(|registry| {
1474            registry
1475                .record_callback_measurements(token, instructions, self.memory_start, memory_end)
1476                .map_err(TimerError::from)
1477        })
1478        .unwrap_or_else(|error| trap_callback_failure("callback measurement accounting", &error));
1479    }
1480}
1481
1482fn trap_callback_failure(context: &str, error: &TimerError) -> ! {
1483    platform::trap(&format!("ic-timers {context} failed: {error}"))
1484}
1485
1486fn with_registry<T>(
1487    operation: impl FnOnce(&TimerRegistry) -> Result<T, TimerError>,
1488) -> Result<T, TimerError> {
1489    RUNTIME.with(|runtime| {
1490        let runtime = runtime.try_borrow().map_err(|_| TimerError::RuntimeBusy)?;
1491        let registry = runtime.as_ref().ok_or(TimerError::NotInitialized)?;
1492        operation(registry)
1493    })
1494}
1495
1496fn with_registry_mut<T>(
1497    operation: impl FnOnce(&mut TimerRegistry) -> Result<T, TimerError>,
1498) -> Result<T, TimerError> {
1499    RUNTIME.with(|runtime| {
1500        let mut runtime = runtime
1501            .try_borrow_mut()
1502            .map_err(|_| TimerError::RuntimeBusy)?;
1503        let registry = runtime.as_mut().ok_or(TimerError::NotInitialized)?;
1504        operation(registry)
1505    })
1506}
1507
1508#[cfg(test)]
1509fn reset_for_test(now_ns: u64, canister_version: u64) {
1510    platform::reset(now_ns, canister_version);
1511    platform::ADMISSION_FAULT.set(None);
1512    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(false));
1513    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(None));
1514    RUNTIME.with(|runtime| {
1515        *runtime.borrow_mut() = None;
1516    });
1517}
1518
1519#[cfg(test)]
1520thread_local! {
1521    static WATCHDOG_COMPLETION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1522    static PROVIDER_INSTALL_FAULT_AFTER: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1523}
1524
1525#[cfg(test)]
1526fn inject_watchdog_completion_fault() {
1527    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(true));
1528}
1529
1530#[cfg(test)]
1531fn take_watchdog_completion_fault() -> bool {
1532    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.replace(false))
1533}
1534
1535#[cfg(test)]
1536fn inject_provider_install_fault() {
1537    inject_provider_install_fault_after(0);
1538}
1539
1540#[cfg(test)]
1541fn inject_provider_install_fault_after(successful_installs: u64) {
1542    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(Some(successful_installs)));
1543}
1544
1545#[cfg(test)]
1546fn take_provider_install_fault() -> bool {
1547    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| match fault.get() {
1548        Some(0) => {
1549            fault.set(None);
1550            true
1551        }
1552        Some(remaining) => {
1553            fault.set(Some(remaining - 1));
1554            false
1555        }
1556        None => false,
1557    })
1558}
1559
1560#[cfg(test)]
1561mod tests;