1#![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
34thread_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 pub struct TimerId;
61}
62
63struct Timer {
64 task: TimerId,
65 time: u64,
66}
67
68impl 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#[export_name = "canister_global_timer"]
92extern "C" fn global_timer() {
93 ic_cdk::futures::in_executor_context(|| {
94 ic_cdk::futures::spawn(async {
95 let mut call_futures = FuturesUnordered::new();
99 let now = ic_cdk::api::time();
100 TIMERS.with(|timers| {
101 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 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 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 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 Task::Once(_) => {
157 tasks.remove(task_id);
158 }
159 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
184pub 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
207pub 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
235pub fn clear_timer(id: TimerId) {
237 TASKS.with(|tasks| tasks.borrow_mut().remove(id));
238}
239
240fn 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 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 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}