Skip to main content

ic_timers/runtime/
mod.rs

1//! Canister-local owner and live timer execution.
2
3use crate::{
4    platform::{self, TimerHandle},
5    registry::{
6        CallbackAcceptance, CallbackRole, CallbackToken, OrdinaryCallback, ProviderHandle,
7        ProviderHandles, RegisterError, RegistrationClaim, RegistryEffect, RegistryError,
8        RegistryTransition, TimerRegistry, WatchdogCallback,
9    },
10    schedule::{ScheduleError, TimerCadence, TimerDirective, TimerSchedule},
11    snapshot::{
12        DeclarationLifetime, TimerCompletion, TimerControlFailure, TimerEpoch, TimerIdentity,
13        TimerPolicy, TimerRunResult, TimerSnapshot, WatchdogRunResult,
14    },
15};
16use std::{cell::RefCell, future::Future, rc::Rc, time::Duration};
17use thiserror::Error;
18
19thread_local! {
20    static RUNTIME: RefCell<Option<TimerRegistry>> = const { RefCell::new(None) };
21}
22
23/// Failure from the canister-local timer runtime API.
24#[non_exhaustive]
25#[derive(Debug, Error)]
26pub enum TimerError {
27    /// The lifecycle owner has not initialized the volatile runtime.
28    #[error("timer runtime is not initialized")]
29    NotInitialized,
30    /// A nested internal borrow indicates unsupported re-entrancy.
31    #[error("timer runtime is already borrowed")]
32    RuntimeBusy,
33    /// Claiming one canonical timer identity failed.
34    #[error(transparent)]
35    Register(#[from] RegisterError),
36    /// Cadence or deadline validation failed.
37    #[error(transparent)]
38    Schedule(#[from] ScheduleError),
39    /// The logical registration was removed or superseded.
40    #[error("timer registration is no longer authoritative")]
41    RegistrationExpired,
42    /// 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 the complete bounded inventory in deterministic identity order.
651pub fn timer_snapshots() -> Result<Vec<TimerSnapshot>, TimerError> {
652    with_registry(|registry| Ok(registry.snapshots()))
653}
654
655/// Return functional expected-failure state by identity without a full inventory.
656pub fn consecutive_expected_failures(identity: &TimerIdentity) -> Result<Option<u64>, TimerError> {
657    with_registry(|registry| Ok(registry.consecutive_expected_failures(identity)))
658}
659
660fn has_armed_wakeup_claim(claim: &RegistrationClaim) -> Result<bool, TimerError> {
661    with_registry(|registry| registry.has_armed_wakeup(claim).map_err(TimerError::from))
662}
663
664fn erase_ordinary_callback<Context: 'static, F, Fut>(
665    mut callback: F,
666    context: fn(CallbackToken) -> Context,
667) -> OrdinaryCallback
668where
669    F: FnMut(Context) -> Fut + 'static,
670    Fut: Future<Output = TimerRunResult> + 'static,
671{
672    Rc::new(RefCell::new(Box::new(move |token| {
673        Box::pin(callback(context(token)))
674    })))
675}
676
677fn apply_claim_transition(
678    claim: &RegistrationClaim,
679    context: Option<&CallbackToken>,
680    operation: impl FnOnce(&mut TimerRegistry) -> Result<RegistryTransition, RegistryError>,
681) -> Result<(), TimerError> {
682    let transition = with_registry_mut(|registry| {
683        validate_context(registry, context)?;
684        operation(registry).map_err(TimerError::from)
685    })?;
686    finish_claim_transition(claim, transition, ProviderHandles::default())
687}
688
689fn ensure_once_claim(
690    claim: &RegistrationClaim,
691    context: Option<&CallbackToken>,
692    schedule: TimerSchedule,
693) -> Result<(), TimerError> {
694    apply_claim_transition(claim, context, |registry| {
695        registry.ensure_once(claim, platform::time_ns(), schedule)
696    })
697}
698
699fn reconcile_ordinary_claim(
700    claim: &RegistrationClaim,
701    context: Option<&CallbackToken>,
702    schedule: Option<TimerSchedule>,
703) -> Result<(), TimerError> {
704    if schedule.is_none() {
705        let (handles, transition) = with_registry_mut(|registry| {
706            validate_context(registry, context)?;
707            registry
708                .validate_ordinary_claim(claim)
709                .map_err(TimerError::from)?;
710            let handles = registry
711                .take_provider_handles_for_claim(claim)
712                .map_err(TimerError::from)?;
713            let transition = registry
714                .reconcile_ordinary(claim, platform::time_ns(), None)
715                .map_err(TimerError::from);
716            Ok((handles, transition))
717        })?;
718        return finish_detached_claim_transition(claim, handles, transition);
719    }
720    apply_claim_transition(claim, context, |registry| {
721        registry.reconcile_ordinary(claim, platform::time_ns(), schedule)
722    })
723}
724
725fn ensure_recurring_claim(
726    claim: &RegistrationClaim,
727    context: Option<&CallbackToken>,
728) -> Result<(), TimerError> {
729    apply_claim_transition(claim, context, |registry| {
730        registry.ensure_recurring(claim, platform::time_ns())
731    })
732}
733
734fn cancel_claim(
735    claim: &RegistrationClaim,
736    context: Option<&CallbackToken>,
737) -> Result<(), TimerError> {
738    apply_detached_claim_transition(claim, context, |registry| registry.cancel(claim))
739}
740
741fn validate_context(
742    registry: &TimerRegistry,
743    context: Option<&CallbackToken>,
744) -> Result<(), TimerError> {
745    context.map_or(Ok(()), |token| {
746        registry
747            .validate_running_context(token)
748            .map_err(TimerError::from)
749    })
750}
751
752fn unregister_claim(claim: &RegistrationClaim) -> Result<(), TimerError> {
753    apply_detached_claim_transition(claim, None, |registry| registry.unregister(claim))
754}
755
756fn apply_detached_claim_transition(
757    claim: &RegistrationClaim,
758    context: Option<&CallbackToken>,
759    operation: impl FnOnce(&mut TimerRegistry) -> Result<RegistryTransition, RegistryError>,
760) -> Result<(), TimerError> {
761    let (handles, transition) = with_registry_mut(|registry| {
762        validate_context(registry, context)?;
763        let handles = registry
764            .take_provider_handles_for_claim(claim)
765            .map_err(TimerError::from)?;
766        let transition = operation(registry).map_err(TimerError::from);
767        Ok((handles, transition))
768    })?;
769    finish_detached_claim_transition(claim, handles, transition)
770}
771
772fn finish_detached_claim_transition(
773    claim: &RegistrationClaim,
774    handles: ProviderHandles,
775    transition: Result<RegistryTransition, TimerError>,
776) -> Result<(), TimerError> {
777    match transition {
778        Ok(transition) => finish_claim_transition(claim, transition, handles),
779        Err(error) => match restore_provider_handles(handles) {
780            Ok(()) => Err(error),
781            Err(restoration_error) => retire_failed_claim(claim, restoration_error),
782        },
783    }
784}
785
786fn finish_claim_transition(
787    claim: &RegistrationClaim,
788    transition: RegistryTransition,
789    handles: ProviderHandles,
790) -> Result<(), TimerError> {
791    match finish_transition(transition, handles) {
792        result @ (Ok(()) | Err(TimerError::ControlFailure(_))) => result,
793        Err(error) => retire_failed_claim(claim, error),
794    }
795}
796
797fn retire_failed_claim(claim: &RegistrationClaim, error: TimerError) -> Result<(), TimerError> {
798    match fail_claim_provider_binding(claim) {
799        Ok(()) | Err(TimerError::RegistrationExpired) => Err(error),
800        Err(cleanup_error) => Err(cleanup_error),
801    }
802}
803
804fn finish_transition(
805    transition: RegistryTransition,
806    handles: ProviderHandles,
807) -> Result<(), TimerError> {
808    let failure = transition.failure();
809    let effect = transition.into_effect();
810    apply_effect(&effect, handles)?;
811    failure.map_or(Ok(()), |failure| Err(TimerError::ControlFailure(failure)))
812}
813
814fn apply_effect(effect: &RegistryEffect, mut handles: ProviderHandles) -> Result<(), TimerError> {
815    if !effect.has_valid_shape() {
816        clear_provider_handles(handles);
817        return Err(TimerError::OwnershipInvariant);
818    }
819    match effect {
820        RegistryEffect::None => restore_provider_handles(handles),
821        RegistryEffect::ArmWakeup { token, arm, .. } => {
822            if arm.replaces_existing() {
823                let replaced = take_detached_or_owned_handle(handles.take_wakeup(), |registry| {
824                    registry.take_wakeup_handle(token.identity())
825                })?;
826                if let Some(replaced) = replaced {
827                    clear_provider_handle(replaced);
828                }
829            }
830            restore_provider_handles(handles)?;
831            arm_wakeup(effect)
832        }
833        RegistryEffect::ClearCallbacks {
834            identity,
835            handles: selected,
836        } => {
837            if selected.includes_wakeup() {
838                let wakeup = take_detached_or_owned_handle(handles.take_wakeup(), |registry| {
839                    registry.take_wakeup_handle(identity)
840                })?;
841                if let Some(wakeup) = wakeup {
842                    clear_provider_handle(wakeup);
843                }
844            }
845            if selected.includes_work() {
846                let work = take_detached_or_owned_handle(handles.take_work(), |registry| {
847                    registry.take_work_handle(identity)
848                })?;
849                if let Some(work) = work {
850                    clear_provider_handle(work);
851                }
852            }
853            restore_provider_handles(handles)
854        }
855        RegistryEffect::DispatchWatchdog { successor, .. } => {
856            if let Some(wakeup) = handles.take_wakeup() {
857                clear_provider_handle(wakeup);
858            }
859            let replaced_work = take_detached_or_owned_handle(handles.take_work(), |registry| {
860                registry.take_work_handle(successor.identity())
861            })?;
862            if let Some(replaced_work) = replaced_work {
863                clear_provider_handle(replaced_work);
864            }
865            dispatch_watchdog_effect(effect)
866        }
867    }
868}
869
870fn take_detached_or_owned_handle(
871    detached: Option<ProviderHandle>,
872    take_owned: impl FnOnce(&mut TimerRegistry) -> Option<ProviderHandle>,
873) -> Result<Option<ProviderHandle>, TimerError> {
874    detached.map_or_else(
875        || with_registry_mut(|registry| Ok(take_owned(registry))),
876        |handle| Ok(Some(handle)),
877    )
878}
879
880fn arm_wakeup(effect: &RegistryEffect) -> Result<(), TimerError> {
881    let RegistryEffect::ArmWakeup {
882        token, delay_ns, ..
883    } = effect
884    else {
885        return Err(TimerError::OwnershipInvariant);
886    };
887    let task_token = token.clone();
888    let handle = platform::set_timer(Duration::from_nanos(*delay_ns), async move {
889        dispatch_wakeup(task_token).await;
890    });
891    if let Err((error, handle)) = install_provider_handle(token, handle) {
892        platform::clear_timer(handle);
893        return Err(error);
894    }
895    if let Err(error) = confirm_effect(effect) {
896        let handle = with_registry_mut(|registry| {
897            registry
898                .take_wakeup_handle(token.identity())
899                .ok_or(TimerError::OwnershipInvariant)
900        })?;
901        clear_provider_handle(handle);
902        return Err(error);
903    }
904    Ok(())
905}
906
907fn dispatch_watchdog_effect(effect: &RegistryEffect) -> Result<(), TimerError> {
908    let RegistryEffect::DispatchWatchdog {
909        successor,
910        successor_delay_ns,
911        work,
912        ..
913    } = effect
914    else {
915        return Err(TimerError::OwnershipInvariant);
916    };
917    let successor_token = successor.clone();
918    let successor_handle =
919        platform::set_timer(Duration::from_nanos(*successor_delay_ns), async move {
920            dispatch_watchdog_scheduler(&successor_token);
921        });
922    if let Err((error, handle)) = install_provider_handle(successor, successor_handle) {
923        platform::clear_timer(handle);
924        return Err(error);
925    }
926
927    let work_token = work.clone();
928    let work_handle = platform::set_timer(Duration::ZERO, async move {
929        dispatch_watchdog_work(&work_token);
930    });
931    if let Err((error, handle)) = install_provider_handle(work, work_handle) {
932        platform::clear_timer(handle);
933        clear_entry_provider_handles(successor.identity())?;
934        return Err(error);
935    }
936    if let Err(error) = confirm_effect(effect) {
937        clear_entry_provider_handles(successor.identity())?;
938        return Err(error);
939    }
940    Ok(())
941}
942
943fn install_provider_handle(
944    token: &CallbackToken,
945    handle: TimerHandle,
946) -> Result<(), (TimerError, TimerHandle)> {
947    #[cfg(test)]
948    if take_provider_install_fault() {
949        return Err((TimerError::OwnershipInvariant, handle));
950    }
951    RUNTIME.with(|runtime| {
952        let Ok(mut runtime) = runtime.try_borrow_mut() else {
953            return Err((TimerError::RuntimeBusy, handle));
954        };
955        let Some(registry) = runtime.as_mut() else {
956            return Err((TimerError::NotInitialized, handle));
957        };
958        match registry.install_provider_handle(token, handle) {
959            Ok(()) => Ok(()),
960            Err((error, handle)) => Err((TimerError::from(error), handle)),
961        }
962    })
963}
964
965fn confirm_effect(effect: &RegistryEffect) -> Result<(), TimerError> {
966    #[cfg(test)]
967    if take_provider_confirmation_fault() {
968        return Err(TimerError::OwnershipInvariant);
969    }
970    with_registry_mut(|registry| {
971        registry
972            .confirm_effect_applied(effect)
973            .map_err(TimerError::from)
974    })
975}
976
977fn restore_provider_handles(mut handles: ProviderHandles) -> Result<(), TimerError> {
978    // Every detached linear capability must be restored or cleared even when
979    // restoring an earlier handle fails.
980    let wakeup_failure = handles
981        .take_wakeup()
982        .and_then(|handle| restore_provider_handle(handle).err());
983    let work_failure = handles
984        .take_work()
985        .and_then(|handle| restore_provider_handle(handle).err());
986    wakeup_failure.or(work_failure).map_or(Ok(()), Err)
987}
988
989fn restore_provider_handle(handle: ProviderHandle) -> Result<(), TimerError> {
990    let (token, handle) = handle.into_parts();
991    match install_provider_handle(&token, handle) {
992        Ok(()) => Ok(()),
993        Err((error, handle)) => {
994            platform::clear_timer(handle);
995            Err(error)
996        }
997    }
998}
999
1000fn clear_provider_handle(handle: ProviderHandle) {
1001    let (_, handle) = handle.into_parts();
1002    platform::clear_timer(handle);
1003}
1004
1005fn clear_provider_handles(mut handles: ProviderHandles) {
1006    if let Some(wakeup) = handles.take_wakeup() {
1007        clear_provider_handle(wakeup);
1008    }
1009    if let Some(work) = handles.take_work() {
1010        clear_provider_handle(work);
1011    }
1012}
1013
1014fn clear_entry_provider_handles(identity: &TimerIdentity) -> Result<(), TimerError> {
1015    let handles = with_registry_mut(|registry| {
1016        Ok(ProviderHandles::from_parts(
1017            registry.take_wakeup_handle(identity),
1018            registry.take_work_handle(identity),
1019        ))
1020    })?;
1021    clear_provider_handles(handles);
1022    Ok(())
1023}
1024
1025#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
1026async fn dispatch_wakeup(token: CallbackToken) {
1027    match token.role() {
1028        CallbackRole::OrdinaryWork => dispatch_ordinary(token).await,
1029        CallbackRole::WatchdogScheduler => dispatch_watchdog_scheduler(&token),
1030        CallbackRole::WatchdogWork => {}
1031    }
1032}
1033
1034#[allow(clippy::future_not_send)] // IC callbacks and canister-local state are single-threaded.
1035async fn dispatch_ordinary(token: CallbackToken) {
1036    let measurement = CallbackMeasurementStart::capture();
1037    let accepted = with_registry_mut(|registry| {
1038        registry.consume_provider_handle(&token);
1039        Ok(registry.begin_ordinary(&token))
1040    });
1041    match accepted {
1042        Ok(CallbackAcceptance::Accepted) => {}
1043        Ok(CallbackAcceptance::Stale) => return,
1044        Err(error) => trap_callback_failure("ordinary callback acceptance", &error),
1045    }
1046
1047    let callback = match with_registry(|registry| {
1048        registry.ordinary_callback(&token).map_err(TimerError::from)
1049    }) {
1050        Ok(callback) => callback,
1051        Err(TimerError::OwnershipInvariant) => {
1052            fail_ordinary_dispatch(&token);
1053            return;
1054        }
1055        Err(error) => trap_callback_failure("ordinary callback lookup", &error),
1056    };
1057    let future = {
1058        let Ok(mut callback) = callback.try_borrow_mut() else {
1059            fail_ordinary_dispatch(&token);
1060            return;
1061        };
1062        callback(token.clone())
1063    };
1064    let result = future.await;
1065    let transition = with_registry_mut(|registry| {
1066        registry
1067            .complete_ordinary(&token, platform::time_ns(), result)
1068            .map_err(TimerError::from)
1069    });
1070    let transition = transition
1071        .unwrap_or_else(|error| trap_callback_failure("ordinary callback completion", &error));
1072    finish_callback_transition(&token, transition, ProviderHandles::default());
1073    record_callback_measurements(&token, measurement.finish());
1074}
1075
1076fn fail_ordinary_dispatch(token: &CallbackToken) {
1077    let transition = with_registry_mut(|registry| {
1078        registry
1079            .complete_ordinary(
1080                token,
1081                platform::time_ns(),
1082                TimerRunResult::new(TimerCompletion::invariant_failure(0), TimerDirective::Stop),
1083            )
1084            .map_err(TimerError::from)
1085    });
1086    let transition = transition.unwrap_or_else(|error| {
1087        trap_callback_failure("ordinary invariant-failure completion", &error)
1088    });
1089    finish_callback_transition(token, transition, ProviderHandles::default());
1090}
1091
1092fn dispatch_watchdog_scheduler(token: &CallbackToken) {
1093    let measurement = CallbackMeasurementStart::capture();
1094    let transition = with_registry_mut(|registry| {
1095        registry.consume_provider_handle(token);
1096        Ok(registry.begin_watchdog_scheduler(token, platform::time_ns()))
1097    });
1098    let transition = transition
1099        .unwrap_or_else(|error| trap_callback_failure("watchdog scheduler transition", &error));
1100    let accepted = !matches!(transition.effect(), RegistryEffect::None);
1101    finish_callback_transition(token, transition, ProviderHandles::default());
1102    if accepted {
1103        record_callback_measurements(token, measurement.finish());
1104    }
1105}
1106
1107fn dispatch_watchdog_work(token: &CallbackToken) {
1108    let measurement = CallbackMeasurementStart::capture();
1109    let accepted = with_registry_mut(|registry| {
1110        registry.consume_provider_handle(token);
1111        Ok(registry.begin_watchdog_work(token))
1112    });
1113    match accepted {
1114        Ok(CallbackAcceptance::Accepted) => {}
1115        Ok(CallbackAcceptance::Stale) => return,
1116        Err(error) => trap_callback_failure("watchdog work acceptance", &error),
1117    }
1118
1119    let callback =
1120        match with_registry(|registry| registry.watchdog_callback(token).map_err(TimerError::from))
1121        {
1122            Ok(callback) => callback,
1123            Err(error) => trap_callback_failure("watchdog callback lookup", &error),
1124        };
1125    let result = {
1126        let Ok(mut callback) = callback.try_borrow_mut() else {
1127            trap_callback_failure(
1128                "watchdog callback ownership",
1129                &TimerError::OwnershipInvariant,
1130            );
1131        };
1132        callback(token.clone())
1133    };
1134    finish_watchdog_dispatch(token, result);
1135    record_callback_measurements(token, measurement.finish());
1136}
1137
1138fn finish_watchdog_dispatch(token: &CallbackToken, result: WatchdogRunResult) {
1139    let claim = RegistrationClaim::from_callback(token);
1140    // Unlike synchronous public control, an unexpected callback-completion
1141    // failure must trap. IC message rollback restores these temporarily
1142    // detached heap capabilities while the previously committed successor
1143    // remains armed by the scheduler message.
1144    let completed = with_registry_mut(|registry| {
1145        let handles = registry
1146            .take_provider_handles_for_claim(&claim)
1147            .map_err(TimerError::from)?;
1148        #[cfg(test)]
1149        {
1150            if take_watchdog_completion_fault() {
1151                return Err(TimerError::OwnershipInvariant);
1152            }
1153        }
1154        let transition = registry
1155            .complete_watchdog_work(token, platform::time_ns(), result)
1156            .map_err(TimerError::from)?;
1157        Ok((transition, handles))
1158    });
1159    let (transition, handles) =
1160        completed.unwrap_or_else(|error| trap_callback_failure("watchdog work completion", &error));
1161    finish_callback_transition(token, transition, handles);
1162}
1163
1164fn finish_callback_transition(
1165    token: &CallbackToken,
1166    transition: RegistryTransition,
1167    handles: ProviderHandles,
1168) {
1169    match finish_transition(transition, handles) {
1170        Ok(()) | Err(TimerError::ControlFailure(_)) => {}
1171        Err(
1172            error @ (TimerError::NotInitialized
1173            | TimerError::RuntimeBusy
1174            | TimerError::Register(_)
1175            | TimerError::Schedule(_)
1176            | TimerError::RegistrationExpired
1177            | TimerError::OwnershipInvariant
1178            | TimerError::ReconciliationConflict),
1179        ) => {
1180            if token.role() == CallbackRole::WatchdogWork {
1181                trap_callback_failure("watchdog provider-handle completion", &error);
1182            }
1183            fail_provider_binding(token).unwrap_or_else(|binding_error| {
1184                trap_callback_failure("provider-binding failure cleanup", &binding_error)
1185            });
1186        }
1187    }
1188}
1189
1190fn fail_provider_binding(token: &CallbackToken) -> Result<(), TimerError> {
1191    let claim = RegistrationClaim::from_callback(token);
1192    fail_claim_provider_binding(&claim)
1193}
1194
1195fn fail_claim_provider_binding(claim: &RegistrationClaim) -> Result<(), TimerError> {
1196    let failed = with_registry_mut(|registry| {
1197        registry
1198            .fail_registration(claim, TimerControlFailure::ProviderBindingFailed)
1199            .map_err(TimerError::from)
1200    });
1201    clear_provider_handles(failed?);
1202    Ok(())
1203}
1204
1205#[derive(Clone, Copy)]
1206struct CallbackMeasurementStart {
1207    instructions_before: u64,
1208    memory_start: platform::MemoryPages,
1209}
1210
1211impl CallbackMeasurementStart {
1212    fn capture() -> Self {
1213        // Keep page observation outside the established instruction interval.
1214        let memory_start = platform::memory_pages();
1215        let instructions_before = platform::instruction_counter();
1216        Self {
1217            instructions_before,
1218            memory_start,
1219        }
1220    }
1221
1222    fn finish(self) -> CallbackMeasurement {
1223        // Close the instruction interval before taking its paired end extent.
1224        let instructions = platform::instruction_counter().saturating_sub(self.instructions_before);
1225        let memory_end = platform::memory_pages();
1226        CallbackMeasurement {
1227            instructions,
1228            memory_start: self.memory_start,
1229            memory_end,
1230        }
1231    }
1232}
1233
1234#[derive(Clone, Copy)]
1235struct CallbackMeasurement {
1236    instructions: u64,
1237    memory_start: platform::MemoryPages,
1238    memory_end: platform::MemoryPages,
1239}
1240
1241fn record_callback_measurements(token: &CallbackToken, measurement: CallbackMeasurement) {
1242    with_registry_mut(|registry| {
1243        registry
1244            .record_callback_measurements(
1245                token,
1246                measurement.instructions,
1247                measurement.memory_start,
1248                measurement.memory_end,
1249            )
1250            .map_err(TimerError::from)
1251    })
1252    .unwrap_or_else(|error| trap_callback_failure("callback measurement accounting", &error));
1253}
1254
1255fn trap_callback_failure(context: &str, error: &TimerError) -> ! {
1256    platform::trap(&format!("ic-timers {context} failed: {error}"))
1257}
1258
1259fn with_registry<T>(
1260    operation: impl FnOnce(&TimerRegistry) -> Result<T, TimerError>,
1261) -> Result<T, TimerError> {
1262    RUNTIME.with(|runtime| {
1263        let runtime = runtime.try_borrow().map_err(|_| TimerError::RuntimeBusy)?;
1264        let registry = runtime.as_ref().ok_or(TimerError::NotInitialized)?;
1265        operation(registry)
1266    })
1267}
1268
1269fn with_registry_mut<T>(
1270    operation: impl FnOnce(&mut TimerRegistry) -> Result<T, TimerError>,
1271) -> Result<T, TimerError> {
1272    RUNTIME.with(|runtime| {
1273        let mut runtime = runtime
1274            .try_borrow_mut()
1275            .map_err(|_| TimerError::RuntimeBusy)?;
1276        let registry = runtime.as_mut().ok_or(TimerError::NotInitialized)?;
1277        operation(registry)
1278    })
1279}
1280
1281#[cfg(test)]
1282fn reset_for_test(now_ns: u64, canister_version: u64) {
1283    platform::reset(now_ns, canister_version);
1284    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(false));
1285    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(None));
1286    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.set(false));
1287    RUNTIME.with(|runtime| {
1288        *runtime.borrow_mut() = None;
1289    });
1290}
1291
1292#[cfg(test)]
1293thread_local! {
1294    static WATCHDOG_COMPLETION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1295    static PROVIDER_INSTALL_FAULT_AFTER: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
1296    static PROVIDER_CONFIRMATION_FAULT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1297}
1298
1299#[cfg(test)]
1300fn inject_watchdog_completion_fault() {
1301    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.set(true));
1302}
1303
1304#[cfg(test)]
1305fn take_watchdog_completion_fault() -> bool {
1306    WATCHDOG_COMPLETION_FAULT.with(|fault| fault.replace(false))
1307}
1308
1309#[cfg(test)]
1310fn inject_provider_install_fault() {
1311    inject_provider_install_fault_after(0);
1312}
1313
1314#[cfg(test)]
1315fn inject_provider_install_fault_after(successful_installs: u64) {
1316    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| fault.set(Some(successful_installs)));
1317}
1318
1319#[cfg(test)]
1320fn take_provider_install_fault() -> bool {
1321    PROVIDER_INSTALL_FAULT_AFTER.with(|fault| match fault.get() {
1322        Some(0) => {
1323            fault.set(None);
1324            true
1325        }
1326        Some(remaining) => {
1327            fault.set(Some(remaining - 1));
1328            false
1329        }
1330        None => false,
1331    })
1332}
1333
1334#[cfg(test)]
1335fn inject_provider_confirmation_fault() {
1336    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.set(true));
1337}
1338
1339#[cfg(test)]
1340fn take_provider_confirmation_fault() -> bool {
1341    PROVIDER_CONFIRMATION_FAULT.with(|fault| fault.replace(false))
1342}
1343
1344#[cfg(test)]
1345mod tests;