use std::{cell::RefCell, cmp::Ordering, collections::BinaryHeap, mem, time::Duration};
use futures::{stream::FuturesUnordered, StreamExt};
use slotmap::{new_key_type, KeyData, SlotMap};
thread_local! {
static TASKS: RefCell<SlotMap<TimerId, Task>> = RefCell::default();
static TIMERS: RefCell<BinaryHeap<Timer>> = RefCell::default();
}
enum Task {
Repeated {
func: Box<dyn FnMut()>,
interval: Duration,
},
Once(Box<dyn FnOnce()>),
}
impl Default for Task {
fn default() -> Self {
Self::Once(Box::new(|| ()))
}
}
new_key_type! {
pub struct TimerId;
}
struct Timer {
task: TimerId,
time: u64,
}
impl Ord for Timer {
fn cmp(&self, other: &Self) -> Ordering {
self.time.cmp(&other.time).reverse()
}
}
impl PartialOrd for Timer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Timer {
fn eq(&self, other: &Self) -> bool {
self.time == other.time
}
}
impl Eq for Timer {}
#[export_name = "canister_global_timer"]
extern "C" fn global_timer() {
crate::setup();
crate::spawn(async {
let mut call_futures = FuturesUnordered::new();
let now = crate::api::time();
TIMERS.with(|timers| {
loop {
let mut timers = timers.borrow_mut();
if let Some(timer) = timers.peek() {
if timer.time <= now {
let timer = timers.pop().unwrap();
if TASKS.with(|tasks| tasks.borrow().contains_key(timer.task)) {
call_futures.push(async move {
(
timer.task,
crate::call(
crate::api::id(),
"<ic-cdk internal> timer_executor",
(timer.task.0.as_ffi(),),
)
.await,
)
});
}
continue;
}
}
break;
}
});
while let Some((task_id, res)) = call_futures.next().await {
match res {
Ok(()) => {}
Err((code, msg)) => {
crate::println!("in canister_global_timer: {code:?}: {msg}");
}
}
TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
if let Some(task) = tasks.get(task_id) {
match task {
Task::Once(_) => {
tasks.remove(task_id);
}
Task::Repeated { interval, .. } => {
match now.checked_add(interval.as_nanos() as u64) {
Some(time) => TIMERS.with(|timers| {
timers.borrow_mut().push(Timer {
task: task_id,
time,
})
}),
None => crate::println!(
"Failed to reschedule task (needed {interval}, currently {now}, and this would exceed u64::MAX)",
interval = interval.as_nanos(),
),
}
}
}
}
});
}
update_ic0_timer();
});
}
pub fn set_timer(delay: Duration, func: impl FnOnce() + 'static) -> TimerId {
let delay_ns = u64::try_from(delay.as_nanos()).expect(
"delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
);
let scheduled_time = crate::api::time().checked_add(delay_ns).expect(
"delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
);
let key = TASKS.with(|tasks| tasks.borrow_mut().insert(Task::Once(Box::new(func))));
TIMERS.with(|timers| {
timers.borrow_mut().push(Timer {
task: key,
time: scheduled_time,
});
});
update_ic0_timer();
key
}
pub fn set_timer_interval(interval: Duration, func: impl FnMut() + 'static) -> TimerId {
let interval_ns = u64::try_from(interval.as_nanos()).expect(
"delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
);
let scheduled_time = crate::api::time().checked_add(interval_ns).expect(
"delay out of bounds (must be within `u64::MAX - ic_cdk::api::time()` nanoseconds)",
);
let key = TASKS.with(|tasks| {
tasks.borrow_mut().insert(Task::Repeated {
func: Box::new(func),
interval,
})
});
TIMERS.with(|timers| {
timers.borrow_mut().push(Timer {
task: key,
time: scheduled_time,
})
});
update_ic0_timer();
key
}
pub fn clear_timer(id: TimerId) {
TASKS.with(|tasks| tasks.borrow_mut().remove(id));
}
fn update_ic0_timer() {
TIMERS.with(|timers| {
let timers = timers.borrow();
let soonest_timer = timers.peek().map_or(0, |timer| timer.time);
unsafe { ic0::global_timer_set(soonest_timer as i64) };
});
}
#[export_name = "canister_update <ic-cdk internal> timer_executor"]
extern "C" fn timer_executor() {
if crate::api::caller() != crate::api::id() {
crate::trap("This function is internal to ic-cdk and should not be called externally.");
}
let (task_id,) = crate::api::call::arg_data();
let task_id = TimerId(KeyData::from_ffi(task_id));
let task = TASKS.with(|tasks| {
let mut tasks = tasks.borrow_mut();
tasks.get_mut(task_id).map(mem::take)
});
if let Some(mut task) = task {
match task {
Task::Once(func) => {
func();
TASKS.with(|tasks| tasks.borrow_mut().remove(task_id));
}
Task::Repeated { ref mut func, .. } => {
func();
TASKS.with(|tasks| tasks.borrow_mut().get_mut(task_id).map(|slot| *slot = task));
}
}
}
crate::api::call::reply(());
}