The API surface should remain as simple as possible so I think the proposed API is really good.
To cancel an interval timer after a certain time you can simply set another timer which does the cancellation. There is no need to add complexity to the API.
That’s because of the WebAssembly type system: “Integers are not inherently signed or unsigned, their interpretation is determined by individual operations.”
Even though the System API uses i64 it will be interpreted as an unsigned number similar to how the existing ic0.time : () -> (timestamp : i64); does it.
It is also worth noting that the ic0.set_global_timer() will not be directly exposed by Rust CDK. Instead we will have a higher level library operates on proper time types.
hoi guys,
A small update. The timers has been implemented on the IC side. The final piece was merged yesterday, and the functionality will be rolled out ~next week. We’re focusing now on implementing Rust and Motoko CDK libraries.
If you’re a CDK developer and plan to implement the timers for your CDK, please read the details below. Otherwise, we’ll keep you posted!
CDK Implementation Details
A new System API call was just added: ic0.global_timer_set(timestamp: i64). It adds support for a single global timer, and the support for multiple and interval timers will be implemented as CDK libraries for Rust/Motoko.
Here is an example implementation of the set_timer() in rusty pseudocode:
struct Task {
id: u32,
time_to_schedule: u64,
fun: Fn,
}
struct TimersLib {
tasks: MinBinaryHeap<Task>;
}
static GLOBAL_TIMERS: TimersLib = TimersLib{};
static GLOBAL_TIMER_ID: u32 = 0;
impl TimersLib {
/// One-off execution of the fun after a minimum `delay` in seconds.
pub fn set_timer(delay: u64, fun: Fn) -> u32 {
GLOBAL_TIMER_ID += 1;
let id = GLOBAL_TIMER_ID;
let now = ic0.time();
let time_to_schedule = now + delay;
GLOBAL_TIMERS.tasks.push(Task{id, time_to_schedule, fun});
let next_task = GLOBAL_TIMERS.tasks.peek();
// Schedule the timer for the next task
ic0.global_timer_set(next_task.time_to_schedule);
return id;
}
}
#[global_timer]
fn global_timer() {
let now = ic0.time();
for task in GLOBAL_TIMERS.tasks {
if task.time_to_schedule <= now {
// Execute the task
let fun = GLOBAL_TIMERS.tasks.pop().fun;
// IMPORTANT: call ourself (see the details below)
ic0.call(SELF, “global_task_executor”, fun);
} else {
// Schedule the timer for a next task
ic0.global_timer_set(task.time_to_schedule);
break;
}
}
}
#[update]
async fn global_task_executor(fun: Fn) {
// IMPORTANT: check the caller (see the details below)
if ic0.caller_id() == SELF {
// It’s ok for the user function to trap here
fun.call();
}
}
There are a few important details:
If the global_timer() traps, all the changes are reverted. So if we set the next timer and then execute some code which traps, the timer will be lost.
Instead, the timer library in global_timer() should just ic0.call() itself, and then do the ic0.global_timer_set(). Now when the execution in ic0.call() traps, the timer is still properly set.
The global_task_executor() is a normal canister update function, so it might be called by other canisters or a user. This is potentially insecure.
The timer library in global_task_executor() should either check again that the timer is expired, or (as in the example above) should check that the caller is this canister.
Hope that helps, and if you have questions, we’re here to help!
Hi @ulan . I have not participated in this thread so I apologize if this has already been addressed; but, I would really appreciate it if the current system heartbeat function was left in place. All of my designs use multiple canisters that all share a common heartbeat source and I prefer to keep it this way for simplicity and reliability sake.
I hope this is not too controversial of an ask. I just prefer to have all my canisters run on the same frequency and not worry about how the timer is implemented on each one. I think what y’all have come up with here is going to be really useful for a lot of applications. I also think it is different enough in how it works to not be redundant with the system heartbeat.