ic_cdk_timers/
lib.rs

1//! This library implements multiple and periodic timers on the Internet Computer.
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! # use std::time::Duration;
7//! # fn main() {
8//! ic_cdk_timers::set_timer(Duration::from_secs(1), || ic_cdk::println!("Hello from the future!"));
9//! # }
10//! ```
11
12#![warn(
13    elided_lifetimes_in_paths,
14    missing_debug_implementations,
15    missing_docs,
16    unsafe_op_in_unsafe_fn,
17    clippy::undocumented_unsafe_blocks,
18    clippy::missing_safety_doc
19)]
20
21use std::{
22    cell::{Cell, RefCell},
23    cmp::Ordering,
24    collections::BinaryHeap,
25    mem,
26    time::Duration,
27};
28
29use futures::{stream::FuturesUnordered, StreamExt};
30use slotmap::{new_key_type, KeyData, SlotMap};
31
32use ic_cdk::call::{Call, CallFailed, RejectCode};
33
34// To ensure that tasks are removable seamlessly, there are two separate concepts here: tasks, for the actual function being called,
35// and timers, the scheduled execution of tasks. As this is an implementation detail, this does not affect the exported name TimerId,
36// which is more accurately a task ID. (The obvious solution to this, `pub use`, invokes a very silly compiler error.)
37
38thread_local! {
39    static TASKS: RefCell<SlotMap<TimerId, Task>> = RefCell::default();
40    static TIMERS: RefCell<BinaryHeap<Timer>> = RefCell::default();
41    static MOST_RECENT: Cell<Option<u64>> = const { Cell::new(None) };
42}
43
44enum Task {
45    Repeated {
46        func: Box<dyn FnMut()>,
47        interval: Duration,
48    },
49    Once(Box<dyn FnOnce()>),
50}
51
52impl Default for Task {
53    fn default() -> Self {
54        Self::Once(Box::new(|| ()))
55    }
56}
57
58new_key_type! {
59    /// Type returned by the [`set_timer`] and [`set_timer_interval`] functions. Pass to [`clear_timer`] to remove the timer.
60    pub struct TimerId;
61}
62
63struct Timer {
64    task: TimerId,
65    time: u64,
66}
67
68// Timers are sorted such that x > y if x should be executed _before_ y.
69
70impl Ord for Timer {
71    fn cmp(&self, other: &Self) -> Ordering {
72        self.time.cmp(&other.time).reverse()
73    }
74}
75
76impl PartialOrd for Timer {
77    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
78        Some(self.cmp(other))
79    }
80}
81
82impl PartialEq for Timer {
83    fn eq(&self, other: &Self) -> bool {
84        self.time == other.time
85    }
86}
87
88impl Eq for Timer {}
89
90// This function is called by the IC at or after the timestamp provided to `ic0.global_timer_set`.
91#[export_name = "canister_global_timer"]
92extern "C" fn global_timer() {
93    ic_cdk::futures::in_executor_context(|| {
94        ic_cdk::futures::spawn(async {
95            // All the calls are made first, according only to the timestamp we *started* with, and then all the results are awaited.
96            // This allows us to use the minimum number of execution rounds, as well as avoid any race conditions.
97            // The only thing that can happen interleavedly is canceling a task, which is seamless by design.
98            let mut call_futures = FuturesUnordered::new();
99            let now = ic_cdk::api::time();
100            TIMERS.with(|timers| {
101                // pop every timer that should have been completed by `now`, and get ready to run its task if it exists
102                loop {
103                    let mut timers = timers.borrow_mut();
104                    if let Some(timer) = timers.peek() {
105                        if timer.time <= now {
106                            let timer = timers.pop().unwrap();
107                            if TASKS.with(|tasks| tasks.borrow().contains_key(timer.task)) {
108                                // This is the biggest hack in this code. If a callback was called explicitly, and trapped, the rescheduling step wouldn't happen.
109                                // The closest thing to a catch_unwind that's available here is performing an inter-canister call to ourselves;
110                                // traps will be caught at the call boundary. This invokes a meaningful cycles cost, and should an alternative for catching traps
111                                // become available, this code should be rewritten.
112                                let task_id = timer.task;
113                                call_futures.push(async move {
114                                    (
115                                        timer,
116                                        Call::bounded_wait(
117                                            ic_cdk::api::canister_self(),
118                                            "<ic-cdk internal> timer_executor",
119                                        )
120                                        .with_raw_args(task_id.0.as_ffi().to_be_bytes().as_ref())
121                                        .await,
122                                    )
123                                });
124                            }
125                            continue;
126                        }
127                    }
128                    break;
129                }
130            });
131            // run all the collected tasks, and clean up after them if necessary
132            while let Some((timer, res)) = call_futures.next().await {
133                let task_id = timer.task;
134                if let Err(e) = res {
135                    ic_cdk::println!("[ic-cdk-timers] canister_global_timer: {e:?}");
136                    if matches!(
137                        e,
138                        CallFailed::InsufficientLiquidCycleBalance(_)
139                            | CallFailed::CallPerformFailed(_)
140                    ) || matches!(e, CallFailed::CallRejected(e) if e.reject_code() == Ok(RejectCode::SysTransient))
141                    {
142                        // Try to execute the timer again later.
143                        TIMERS.with(|timers| {
144                            timers.borrow_mut().push(timer);
145                        });
146                        continue;
147                    }
148                }
149                TASKS.with(|tasks| {
150                    let mut tasks = tasks.borrow_mut();
151                    if let Some(task) = tasks.get(task_id) {
152                        match task {
153                            // duplicated on purpose - it must be removed in the function call, to access self by value;
154                            // and it must be removed here, because it may have trapped and not actually been removed.
155                            // Luckily slotmap ops are equivalent to simple vector indexing.
156                            Task::Once(_) => {
157                                tasks.remove(task_id);
158                            }
159                            // reschedule any repeating tasks
160                            Task::Repeated { interval, .. } => {
161                                match now.checked_add(interval.as_nanos() as u64) {
162                                    Some(time) => TIMERS.with(|timers| {
163                                        timers.borrow_mut().push(Timer {
164                                            task: task_id,
165                                            time,
166                                        })
167                                    }),
168                                    None => ic_cdk::println!(
169                                        "Failed to reschedule task (needed {interval}, currently {now}, and this would exceed u64::MAX)",
170                                        interval = interval.as_nanos(),
171                                    ),
172                                }
173                            }
174                        }
175                    }
176                });
177            }
178            MOST_RECENT.with(|recent| recent.set(None));
179            update_ic0_timer();
180        });
181    });
182}
183
184/// Sets `func` to be executed later, after `delay`. Panics if `delay` + [`time()`][ic_cdk::api::time] is more than [`u64::MAX`] nanoseconds.
185///
186/// To cancel the timer before it executes, pass the returned `TimerId` to [`clear_timer`].
187///
188/// Note that timers are not persisted across canister upgrades.
189pub fn set_timer(delay: Duration, func: impl FnOnce() + 'static) -> TimerId {
190    let delay_ns = u64::try_from(delay.as_nanos()).expect(
191        "delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
192    );
193    let scheduled_time = ic_cdk::api::time().checked_add(delay_ns).expect(
194        "delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
195    );
196    let key = TASKS.with(|tasks| tasks.borrow_mut().insert(Task::Once(Box::new(func))));
197    TIMERS.with(|timers| {
198        timers.borrow_mut().push(Timer {
199            task: key,
200            time: scheduled_time,
201        });
202    });
203    update_ic0_timer();
204    key
205}
206
207/// Sets `func` to be executed every `interval`. Panics if `interval` + [`time()`][ic_cdk::api::time] is more than [`u64::MAX`] nanoseconds.
208///
209/// To cancel the interval timer, pass the returned `TimerId` to [`clear_timer`].
210///
211/// Note that timers are not persisted across canister upgrades.
212pub fn set_timer_interval(interval: Duration, func: impl FnMut() + 'static) -> TimerId {
213    let interval_ns = u64::try_from(interval.as_nanos()).expect(
214        "delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
215    );
216    let scheduled_time = ic_cdk::api::time().checked_add(interval_ns).expect(
217        "delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
218    );
219    let key = TASKS.with(|tasks| {
220        tasks.borrow_mut().insert(Task::Repeated {
221            func: Box::new(func),
222            interval,
223        })
224    });
225    TIMERS.with(|timers| {
226        timers.borrow_mut().push(Timer {
227            task: key,
228            time: scheduled_time,
229        })
230    });
231    update_ic0_timer();
232    key
233}
234
235/// Cancels an existing timer. Does nothing if the timer has already been canceled.
236pub fn clear_timer(id: TimerId) {
237    TASKS.with(|tasks| tasks.borrow_mut().remove(id));
238}
239
240/// Calls `ic0.global_timer_set` with the soonest timer in [`TIMERS`]. This is needed after inserting a timer, and after executing one.
241fn update_ic0_timer() {
242    TIMERS.with(|timers| {
243        let timers = timers.borrow();
244        let soonest_timer = timers.peek().map(|timer| timer.time);
245        let should_change = match (soonest_timer, MOST_RECENT.with(|recent| recent.get())) {
246            (Some(timer), Some(recent)) => timer < recent,
247            (Some(_), None) => true,
248            _ => false,
249        };
250        if should_change {
251            ic0::global_timer_set(soonest_timer.unwrap());
252            MOST_RECENT.with(|recent| recent.set(soonest_timer));
253        }
254    });
255}
256
257#[cfg_attr(
258    target_family = "wasm",
259    export_name = "canister_update <ic-cdk internal> timer_executor"
260)]
261#[cfg_attr(
262    not(target_family = "wasm"),
263    export_name = "canister_update_ic_cdk_internal.timer_executor"
264)]
265extern "C" fn timer_executor() {
266    if ic_cdk::api::msg_caller() != ic_cdk::api::canister_self() {
267        ic_cdk::trap("This function is internal to ic-cdk and should not be called externally.");
268    }
269    let arg_bytes = ic_cdk::api::msg_arg_data();
270    // timer_executor is only called by the canister itself (from global_timer),
271    // so we can safely assume that the argument is a valid TimerId (u64).
272    // And we don't need decode_one_with_config/DecoderConfig to defense against malicious payload.
273    assert!(arg_bytes.len() == 8);
274    let task_id = u64::from_be_bytes(arg_bytes.try_into().unwrap());
275
276    let task_id = TimerId(KeyData::from_ffi(task_id));
277    // We can't be holding `TASKS` when we call the function, because it may want to schedule more tasks.
278    // Instead, we swap the task out in order to call it, and then either swap it back in, or remove it.
279    let task = TASKS.with(|tasks| {
280        let mut tasks = tasks.borrow_mut();
281        tasks.get_mut(task_id).map(mem::take)
282    });
283    if let Some(mut task) = task {
284        match task {
285            Task::Once(func) => {
286                ic_cdk::futures::in_executor_context(func);
287                TASKS.with(|tasks| tasks.borrow_mut().remove(task_id));
288            }
289            Task::Repeated { ref mut func, .. } => {
290                ic_cdk::futures::in_executor_context(func);
291                TASKS.with(|tasks| tasks.borrow_mut().get_mut(task_id).map(|slot| *slot = task));
292            }
293        }
294    }
295    ic_cdk::api::msg_reply([]);
296}