Periodic tasks and timers
View this sample's code on GitHub
Overview
Unlike other blockchains, the BigFile can automatically execute canister smart contracts after a specified delay or periodically.
There are two ways to schedule an automatic canister execution on the BIG:
- Timers: one-shot or periodic canister calls with specified minimum timeout or interval.
- Heartbeats: legacy periodic canister invocations with intervals close to the blockchain finalization rate (1s). Heartbeats are supported by the BIG for backward compatibility and some very special use cases. Newly developed canisters should prefer using timers over the heartbeats.
This example demonstrates different ways of scheduling periodic tasks on the BigFile: timers and heartbeats. The example shows the difference between the two and helps to decide which method suits you the best.
The example consists of two canisters named heartbeat
and timer
, both implementing the same functionality: schedule a periodic task to increase a counter.
Prerequisites
This example requires an installation of:
- Install the BIG SDK.
- Clone the example dapp project:
git clone https://github.com/dfinity/examples
Example 1:
Step 1: Begin by opening a terminal window and navigating into the project's directory.
cd examples/rust/periodic_tasks
Step 2: Start a clean local BigFile replica and a web server:
dfx stop
dfx start --clean
This terminal will stay blocked, printing log messages, until the Ctrl+C
is pressed or dfx stop
command is run.
Example output:
% dfx stop && dfx start --clean
[...]
Dashboard: http://localhost:63387/_/dashboard
Step 3: Open another terminal window in the same directory:
cd examples/rust/periodic_tasks
Step 4: Compile and deploy
heartbeat
andtimer
canisters, setting the interval for periodic tasks to 10s:
dfx deploy heartbeat --argument 10
dfx deploy timer --argument 10
The counter will start increasing every ~10s in both canisters.
Example output:
% dfx deploy heartbeat --argument 10
[...]
Deployed canisters.
URLs:
Backend canister via Candid interface:
heartbeat: http://127.0.0.1/...
timer: http://127.0.0.1/...
% dfx deploy timer --argument 10
[...]
Deployed canisters.
URLs:
Backend canister via Candid interface:
heartbeat: http://127.0.0.1/...
timer: http://127.0.0.1/...
Step 5: After 10s, observe similar non-zero counters in both canisters:
dfx canister call heartbeat counter
dfx canister call timer counter
Note, as the canisters deployed one by one, there might be a minor discrepancy in the counters.
Example output:
% dfx canister call heartbeat counter
(8 : nat32)
% dfx canister call timer counter
(7 : nat32)
Step 6: Compare the amount of cycles used to schedule the periodic task with 10s interval:
dfx canister call heartbeat cycles_used
dfx canister call timer cycles_used
Example output:
% dfx canister call heartbeat cycles_used
(10_183_957 : nat64)
% dfx canister call timer cycles_used
(2_112_067 : nat64)
For periodic tasks with 10 sec intervals, the heartbeat
canister uses more cycles than the timer
canister.
Not only do timers use fewer cycles, but they are also more composable. As there is no global state or methods to export, different libraries with timers could be easily used in the same project.
Also, timers provide isolation between the scheduling logic and the periodic task. If the periodic task fails, all the changes made by this task will be reverted, but the timers library state will be updated, i.e. the failed task will be removed from the list of timers to execute.
For such isolation of execution and scheduling contexts, the internal timers library uses self-canister calls:
# This is a pseudo-code of a self call:
ic_cdk::call(ic_cdk::id(), "periodic_task", ());
Despite the costs associated with such self-canister calls, the timers library still uses fewer cycles than the heartbeats.
Example 2: Cycles usage for tasks with 1s interval
Step 1: Open a new terminal window in the example root directory:
cd examples/rust/periodic_tasks
Step 2: Start a clean local BigFile replica and a web server:
dfx stop
dfx start --clean
This terminal will stay blocked, printing log messages, until the Ctrl+C
is pressed or dfx stop
command is run.
Step 3: Open another terminal window in the same directory:
cd examples/rust/periodic_tasks
Example output:
% dfx stop && dfx start --clean
[...]
Dashboard: http://localhost:63387/_/dashboard
Step 4:. Compile and deploy
heartbeat
andtimer
canisters, setting the interval for periodic tasks to 1s:
dfx deploy --argument 1 heartbeat
dfx deploy --argument 1 timer
The counter will start increasing every second in both canisters.
Example output:
% dfx deploy --argument 1 heartbeat
[...]
Deployed canisters.
URLs:
Backend canister via Candid interface:
heartbeat: http://127.0.0.1/...
timer: http://127.0.0.1/...
% dfx deploy --argument 1 timer
[...]
Deployed canisters.
URLs:
Backend canister via Candid interface:
heartbeat: http://127.0.0.1/...
timer: http://127.0.0.1/...
Step 5: After a few seconds, observe similar non-zero counters in both canisters:
dfx canister call heartbeat counter
dfx canister call timer counter
Note, as the canisters deployed one by one, there might be a minor discrepancy in the counters.
Example output:
% dfx canister call heartbeat counter
(8 : nat32)
% dfx canister call timer counter
(9 : nat32)
Step 6: Compare the number of cycles used to schedule the periodic task with 1s interval:
dfx canister call heartbeat cycles_used
dfx canister call timer cycles_used
Example output:
% dfx canister call heartbeat cycles_used
(2_456_567 : nat64)
% dfx canister call timer cycles_used
(4_545_326 : nat64)
For periodic tasks with 1 sec interval, the heartbeat
canister uses less cycles than the timer
canister.
Despite the heartbeat
using fewer cycles in this case, this solution is hard to compose within a big project. If there are two libraries using heartbeats internally, they won't even compile together, as they both would be trying to export the global canister_heartbeat
method required for the heartbeats.
Also, there is no isolation between the scheduling logic and the periodic task. If the periodic task fails, all the changes made by the task and by the canister_heartbeat
method will be reverted. So the failed task will be executed over and over again every heartbeat.
For such isolation of execution and scheduling contexts, the timers library uses internal self-canister calls as described in Demo 1
. Due to the costs associated with such self-canister calls, timer
canister uses more cycles for very frequent periodic tasks.
Further learning
- Have a look at the locally running dashboard. The URL is at the end of the
dfx start
command:Dashboard: http://localhost/...
- Check out
heartbeat
andtimer
canisters Candid user interface. The URLs are at the end of thedfx deploy
command:heartbeat: http://127.0.0.1/...
- Find which interval makes even the costs of running periodic tasks in the
timer
andheartbeat
canisters:dfx deploy heartbeat --argument 5 && dfx deploy timer --argument 5
Cube interface
The heartbeat
and timer
canisters provide the following interface:
counter
— returns the value of theCOUNTER
, i.e. how many times the periodic task was executed (query, both canisters)start_with_interval_secs
— starts a new timer with the specified interval in seconds (timer canister)set_interval_secs
— sets a new interval to call the periodic task in seconds (heartbeat canister)stop
— stops executing periodic task (both canisters)cycles_used
— returns the number of cycles observed in the periodic task (both canisters)
Example usage:
dfx canister call timer start_with_interval_secs 5
Conclusion
For code composability, execution context isolation, and cost efficiency, canister developers should prefer to use timers over heartbeats.
As shown in Example 2
, there might be still very specific use cases for the heartbeats. Those should be considered case by case, with composability and isolation issues in mind.
Security considerations and best practices
If you base your application on this example, we recommend you familiarize yourself with and adhere to the security best practices for developing on the BigFile. This example may not implement all the best practices.