Task scheduler rust library for the Internet Computer
The IC provides built-in "heartbeat" functionality which is basically a special function that gets executed each time consensus ticks. But this is not enough for a comprehensive task scheduling - you still have to implement scheduling logic by yourself. This rust library does exactly that - provides you with simple APIs for complex background scheduling scenarios to execute your code at any specific time, as many times as you want.
Make sure you're using dfx 0.8.4
or higher.
# Cargo.toml
[dependencies]
ic-cron = "0.7"
// somewhere in your canister's code
ic_cron::implement_cron!();
#[derive(CandidType, Deserialize)]
enum TaskKind {
SendGoodMorning(String),
DoSomethingElse,
}
// enqueue a task
#[ic_cdk_macros::update]
pub fn enqueue_task_1() {
cron_enqueue(
// set a task payload - any CandidType is supported
TaskKind::SendGoodMorning(String::from("sweetie")),
// set a scheduling interval (how often and how many times to execute)
ic_cron::types::SchedulingOptions {
1_000_000_000 * 60 * 5, // after waiting for 5 minutes delay once
1_000_000_000 * 10, // each 10 seconds
iterations: Iterations::Exact(20), // until executed 20 times
},
);
}
// enqueue another task
#[ic_cdk_macros::update]
pub fn enqueue_task_2() {
cron_enqueue(
TaskKind::DoSomethingElse,
ic_cron::types::SchedulingOptions {
0, // start immediately
1_000_000_000 * 60 * 5, // each 5 minutes
iterations: Iterations::Infinite, // repeat infinitely
},
);
}
// in a canister heartbeat function get all tasks ready for execution at this exact moment and use it
#[ic_cdk_macros::heartbeat]
fn heartbeat() {
// cron_ready_tasks will only return tasks which should be executed right now
for task in cron_ready_tasks() {
let kind = task.get_payload::<TaskKind>().expect("Serialization error");
match kind {
TaskKind::SendGoodMorning(name) => {
// will print "Good morning, sweetie!"
println!("Good morning, {}!", name);
},
TaskKind::DoSomethingElse => {
...
},
};
}
}
Since this library is just a fancy task queue, there is no significant overhead in terms of cycles.
This library uses built-in canister heartbeat functionality. Each time you enqueue a task it gets added to the task
queue. Tasks could be scheduled in different ways - they can be executed some exact number of times or infinitely. It is
very similar to how you use setTimeout()
and setInterval()
in javascript, but more flexible. Each
time canister_heartbeat
function is called, you have to call cron_ready_tasks()
function which efficiently iterates
over the task queue and pops tasks which scheduled execution timestamp is <= current timestamp. Rescheduled tasks get
their next execution timestamp relative to their previous planned execution timestamp - this way the scheduler
compensates an error caused by unstable consensus intervals.
Since ic-cron
can't pulse faster than the consensus ticks, it has an error of ~2s.
- Introduction To ic-cron Library
- Extending Sonic With Limit Orders Using ic-cron Library
- How to Execute Background Tasks on Particular Weekdays with IC-Cron and Chrono
- How To Build A Token With Recurrent Payments On The Internet Computer Using ic-cron Library
See the example project for better understanding.
This macro will implement all the functions you will use: get_cron_state()
, cron_enqueue()
, cron_dequeue()
and cron_ready_tasks()
.
Basically, this macro implements an inheritance pattern. Just like in a regular object-oriented programming language. Check the source code for further info.
Schedules a new task. Returns task id, which then can be used in cron_dequeue()
to de-schedule the task.
Params:
payload: CandidType
- the data you want to provide with the taskscheduling_interval: SchedulingInterval
- how often your task should be executed and how many times it should be rescheduled
Returns:
ic_cdk::export::candid::Result<u64>
-Ok(task id)
if everything is fine, andErr
if there is a serialization issue with yourpayload
Deschedules the task, removing it from the queue.
Params:
task_id: u64
- an id of the task you want to delete from the queue
Returns:
Option<ScheduledTask>
-Some(task)
, if the operation was a success;None
, if there was no such task.
Returns a vec of tasks ready to be executed right now.
Returns:
Vec<ScheduledTask>
- vec of tasks to handle
Returns a static mutable reference to object which can be used to observe scheduler's state and modify it. Mostly
intended for advanced users who want to extend ic-cron
. See the source code for
further info.
Returns (moved) the cron state. Used to upgrade a canister without state cloning. Make sure you're not using get_cron_state()
before _put_cron_state()
after you call this function.
Sets the global state of the task scheduler, so this new state is accessible from get_cron_state()
function.
Params:
Option<TaskScheduler>
- state object you can get fromget_cron_state()
function
These two functions could be used to persist scheduled tasks between canister upgrades:
#[ic_cdk_macros::pre_upgrade]
fn pre_upgrade_hook() {
let cron_state = _take_cron_state();
stable_save((cron_state,)).expect("Unable to save the state to stable memory");
}
#[ic_cdk_macros::post_upgrade]
fn post_upgrade_hook() {
let (cron_state,): (Option<TaskScheduler>,) =
stable_restore().expect("Unable to restore the state from stable memory");
_put_cron_state(cron_state);
}
You don't need to modify your .did
file for this library to work.
You can reach me out here on Github opening an issue, or you could start a thread on Dfinity developer forum.
You're also welcome to suggest new features and open PR's.