canic_core/ops/ic/
timer.rs

1pub use crate::cdk::timers::TimerId;
2
3use crate::{
4    cdk::timers::{
5        clear_timer as cdk_clear_timer, set_timer as cdk_set_timer,
6        set_timer_interval as cdk_set_timer_interval,
7    },
8    model::metrics::{
9        system::{SystemMetricKind, SystemMetrics},
10        timer::{TimerMetrics, TimerMode},
11    },
12    ops::perf::PerfOps,
13    perf::perf_counter,
14};
15use std::{cell::RefCell, future::Future, rc::Rc, thread::LocalKey, time::Duration};
16
17///
18/// TimerOps
19///
20
21pub struct TimerOps;
22
23impl TimerOps {
24    /// Schedules a one-shot timer.
25    /// The task is a single Future, consumed exactly once.
26    pub fn set(
27        delay: Duration,
28        label: impl Into<String>,
29        task: impl Future<Output = ()> + 'static,
30    ) -> TimerId {
31        let label = label.into();
32
33        SystemMetrics::increment(SystemMetricKind::TimerScheduled);
34        TimerMetrics::ensure(TimerMode::Once, delay, label.as_str());
35
36        cdk_set_timer(delay, async move {
37            TimerMetrics::increment(TimerMode::Once, delay, label.as_str());
38
39            let start = perf_counter();
40            task.await;
41            let end = perf_counter();
42
43            PerfOps::record(label.as_str(), end.saturating_sub(start));
44        })
45    }
46
47    /// Schedules a repeating timer.
48    /// The task is a closure that produces a fresh Future on each tick.
49    pub fn set_interval<F, Fut>(interval: Duration, label: impl Into<String>, task: F) -> TimerId
50    where
51        F: FnMut() -> Fut + 'static,
52        Fut: Future<Output = ()> + 'static,
53    {
54        // Avoid cloning the String every tick.
55        let label = Rc::new(label.into());
56
57        SystemMetrics::increment(SystemMetricKind::TimerScheduled);
58        TimerMetrics::ensure(TimerMode::Interval, interval, label.as_str());
59
60        let task = Rc::new(RefCell::new(task));
61
62        cdk_set_timer_interval(interval, move || {
63            let label = Rc::clone(&label);
64            let task = Rc::clone(&task);
65
66            async move {
67                TimerMetrics::increment(TimerMode::Interval, interval, label.as_str());
68
69                let start = perf_counter();
70                let fut = { (task.borrow_mut())() };
71                fut.await;
72                let end = perf_counter();
73
74                PerfOps::record(label.as_str(), end.saturating_sub(start));
75            }
76        })
77    }
78
79    /// Clears a previously scheduled timer.
80    pub fn clear(id: TimerId) {
81        cdk_clear_timer(id);
82    }
83
84    /// Schedule a one-shot timer only if the slot is empty.
85    /// Returns true when a new timer was scheduled.
86    pub fn set_guarded(
87        slot: &'static LocalKey<RefCell<Option<TimerId>>>,
88        delay: Duration,
89        label: impl Into<String>,
90        task: impl Future<Output = ()> + 'static,
91    ) -> bool {
92        slot.with_borrow_mut(|entry| {
93            if entry.is_some() {
94                return false;
95            }
96
97            let id = Self::set(delay, label, task);
98            *entry = Some(id);
99            true
100        })
101    }
102
103    /// Schedule a guarded init timer that installs a repeating interval timer.
104    /// Returns true when a new timer was scheduled.
105    /// The interval is only installed if the slot still holds the init timer.
106    pub fn set_guarded_interval<FInit, InitFut, FTick, TickFut>(
107        slot: &'static LocalKey<RefCell<Option<TimerId>>>,
108        init_delay: Duration,
109        init_label: impl Into<String>,
110        init_task: FInit,
111        interval: Duration,
112        interval_label: impl Into<String>,
113        tick_task: FTick,
114    ) -> bool
115    where
116        FInit: FnOnce() -> InitFut + 'static,
117        InitFut: Future<Output = ()> + 'static,
118        FTick: FnMut() -> TickFut + 'static,
119        TickFut: Future<Output = ()> + 'static,
120    {
121        let init_label = init_label.into();
122        let interval_label = interval_label.into();
123
124        slot.with_borrow_mut(|entry| {
125            if entry.is_some() {
126                return false;
127            }
128
129            let init_id_cell = Rc::new(RefCell::new(None));
130            let init_id_cell_task = Rc::clone(&init_id_cell);
131
132            let init_id = Self::set(init_delay, init_label, async move {
133                init_task().await;
134
135                let init_id = init_id_cell_task.borrow();
136                let Some(init_id) = init_id.as_ref() else {
137                    return;
138                };
139
140                let still_armed = slot.with_borrow(|slot_val| slot_val.as_ref() == Some(init_id));
141                if !still_armed {
142                    return;
143                }
144
145                let interval_id = Self::set_interval(interval, interval_label, tick_task);
146
147                // Atomically replace the slot value and clear the previous timer id.
148                // This prevents orphaned timers if callers clear around the handover.
149                slot.with_borrow_mut(|slot_val| {
150                    let old = slot_val.replace(interval_id);
151                    if let Some(old_id) = old
152                        && old_id != interval_id
153                    {
154                        Self::clear(old_id);
155                    }
156                });
157            });
158
159            *init_id_cell.borrow_mut() = Some(init_id);
160            *entry = Some(init_id);
161            true
162        })
163    }
164
165    /// Clear a guarded timer slot if present.
166    /// Returns true when a timer was cleared.
167    #[must_use]
168    pub fn clear_guarded(slot: &'static LocalKey<RefCell<Option<TimerId>>>) -> bool {
169        slot.with_borrow_mut(|entry| {
170            entry.take().is_some_and(|id| {
171                Self::clear(id);
172                true
173            })
174        })
175    }
176}