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