Question 1: This may be a dumb question, but I can set many timers correct?
let timer1 = setTimer( 365 * 24 * 60* 60 *1000000000, sayHappyAnniversary);
let timer2 = setTimer( 365 * 24 * 60* 60 *1000000000 * 25 , sayHappySilverAnniversary);
Question 2: Do the timers persist across upgrades? If yes, what happens if you take the function out of your class?
Question 3: Would there be any benefit to having an async* pathway here?
Iceypee
December 29, 2022, 5:59pm
104
It was mentioned in the description here
dfinity:master
← dfinity:gabor/timers
opened 12:20PM - 02 Nov 22 UTC
This PR adds the necessary primitives for the base library to rely on.
``` Moto… ko
setTimer : (delayNanos : Nat64, recurring : Bool, job : () -> async ()) -> (id : Nat)
cancelTimer : (id : Nat) -> ()
```
It also adds a mechanism `system func timer`, so that the user can choose to provide a custom implementation.
A new compiler flag `-no-timer` is also provided to disable all timer-related functionality (e.g. for critical applications).
-----------
# How it works
By default the timer mechanism is enabled, and the `setTimer`/`cancelTimer` utilities can be used. If the `system func timer` is present, the user is responsible for the timer implementation, i.e. she can supply a very simple or very elaborate mechanism. When `moc` is invoked with `-no-timer` then the whole mechanism is disabled. This is useful for minimising the code footprint.
## Canister-global timer discipline
The IC's underlying timer mechanism is one timer per canister. The canister infrastructure is responsible for providing the abstraction of several (and recurring) timers. Timer expiration is best-effort, i.e. the timer's action will never be executed before the desired expiration time, but there is no guarantee of execution _at_ expiration time. It will usually be later.
## The default mechanism
When the user doesn't choose to implement `system func timer` (and doesn't suppress it by `-no-timer`), a default mechanism gets generated. The default mechanism is basically a call to the builtin `@timer_helper`, which will be provided by the compiler.
In the default mechanism the user calls the `setTimer` call from (Motoko-base). This will register an expiration relative to the current time in form of a timer node that `@timer_helper` will visit. Correspondingly a `cancelTimer` call will expunge a timer node. Canister-global timer expirations will also do this, as well as pruning the node tree (a priority-queue).
### The `Node` structure
`Node`s will track the expiration time, the timer Id, and optionally a duration for recurring timers. For administrative reasons they also keep pointers to the earlier and later trees. Each timer node further has a thunk (a function of type `() -> async ()`) which (when called) returns a _future_ (concurrent action).
The most important invariant for nodes is that the expiration time divides the earlier and later trees temporally:
- every expiration on a `Node` in the earlier-tree is `<=` the expiration of the parent node, and analogously
- every expiration on a `Node` in the later-tree is `>=` the expiration of the parent node.
There is a twist to the expiration of the `Nodes` though, as they are mutable, and are set to 0 when the timer expires or gets cancelled. So the above invariant is strictly only valid for nodes (with their child trees) where no node therein is expunged.
This allows for some neat optimisations. E.g. expiration is caused by time marching from early to later, so if a node's expiration is 0 then we can assume that the earlier-tree is completely expired. _Pruning_ (the cleaning of a timer tree from expired nodes) becomes a later-tree-only operation in this case.
The node expired _implies_ earlier-tree expired is a _corollary of the fundamental invariant_ with expungable nodes.
### Insertion
Timer Ids are given out as `1..` and never reused (but begins after upgrade from 1 again).
The _initial_ insertion of a timer (`setTimer`) respects the invariant that a higher Ids will execute after lower Ids when they have the same expiration time. For _recurrent_ expirations this is also the case if the delays are identical.
### Cancellation
Each timer node carries the Id, and this allows expunging the node when `cancelTimer` is called. To maintain the above corollary, each `cancelTimer` must be followed by a compacting operation (basically pruning, but considering the earlier-trees too). Alternatively a grafting operation can be used, appending the later-tree onto the latest slot of the earlier-tree.
It is important to note that while code is running that expunges nodes as a result of canister global timer expiration, there must not be calls to `cancelTimer` (or similar) since that would likely interfere with the corollary. This is only potentially a problem for self-implemented `system func timer`, as the default mechanism will use `@timer_helper`, which is not doing such things.
### Recurrence
When a timer expires that was set up being recurrent, the timer node needs to be re-added to the timer tree but with an expiration shifted by the timer's delay. This will happen in the loop that expunges timers that are already expired. Thus it may happen that the loop will detect the same timer and expunge it again. For a delay of 0 this might cause an infinite loop, so recurrent timers with 0 delay will be treated as non-recurring. 5b9b62faa implements this by avoiding re-adding.
### Tree walking and reconstruction
Several operations walk (traverse) the timer tree. `setTimer` in particular prunes the expired nodes while adding the new node and similarly the reinsertion of recurrent timers.
Since expiration times are kept in mutable ~~fields~~ array elements, node reinsertion due to recurrence will not cause duplicated expirations when tree nodes get copied. This is due to mutable ~~cell~~ array aliasing in the tree walk code. ~~If we decide to get rid of this feature we have to come up with a different scheme to suppress duplicated firing of timers.~~
### Setting the canister-global timer
There are three triggers to call the system API `global_timer_set`:
- connected to `setTimer` (with the expiration time of the earliest active timer)
- when the timer expiration callback is being invoked
- and there is another active timer
- no active timer (`global_timer_set(0)`)
- `cancelTimer` is called, and there is no active timer remaining.
### Trap avoidance
A timed action trapping must not impede the execution of other actions, and thus we use the type `() -> async ()` for registering such actions with `setTimer`. This ensures that
- each action has its own future (even for each recurrence)
- each future gets run in its own `async` context and as such traps and `throw`s are isolated.
### Reclaiming the continuation slots
_Note_: originally the futures were `awaited`, but this seems redundant and has been removed in 204d87a39. Also, an upgrade test is now run to check that executed timer actions don't cause stuck canisters.
~~Upon timer expiration the callback is responsible for installing the workers into the continuation table (this is the `async` part), but it is also important to `await` those futures, so they don't fill up the continuation table. I expect that this can be done by a trailing `await` of an `async forall <futures> try await <it> catch {}`, since the `canister_global_timer` endpoint is called in an `async` context.~~
### The `@timer_helper` builtin
When no user-written `system func timer` is present, `moc` will insert a call to an internal function `@timer_helper`. This
works by creating a worklist of expired timers relative to the current time, then expunging the corresponding nodes and re-inserting recurring ones with respective delays added.
The worklist is then transformed into an array of futures, each in its trap-proof compartment. Finally all the pending futures are `await`ed.
### The upgrade story
<a name="upgrades"></a>
Easy. The global timer gets jettisoned on upgrade, and the timers need to be set up in the post-upgrade hook. Stable variables can be used to remember the timers (not the Ids, but the setup parameters) if they don't have a rigid structure.
_Note_: The timers themselves cannot be stored in stable variables as the job's type involves `async` and also we have no mechanism for locating the same function after an upgrade automatically.
## Opting out
To opt-out of the default mechanism the user just declares `system func timer`. By using the passed in function argument to set the global timer, full control is given to either implement a very basic wakeup scheme or an even more elaborate mechanism than the one presented above.
### The initial expiration
After the canister starts running, `system func timer` gets invoked automatically. This also happens after an upgrade.
### An example of a periodic wakeup
The simplest way to achieve a periodic wakeup of a canister with fixed intervals can be coded as
``` Motoko
import Time "mo:base/Time";
import { fromIntWrap } = "mo:base/Nat64";
actor {
system func timer(set : Nat64 -> ()) : async () {
set(fromIntWrap(Time.now()) + 60_000_000_000); // 60 seconds from now
doSomething();
};
func doSomething() {
// whatever
}
}
```
### An example of a exponential approximation
Imagine you are at a hackathon, and there is a deadline to submit the hash of your solution. In the last hour you want to be reminded 60, 30, 15, 7.5, etc. minutes before the deadline. Following implementation could serve the purpose:
``` Motoko
import { now } = "mo:base/Time";
import { fromIntWrap } = "mo:base/Nat64";
import { print } = "mo:base/Debug";
actor {
let hour = 60 * 60_000_000_000;
let deadline = now() + hour; // nanos
system func timer(set : Nat64 -> ()) : async () {
let time = now();
let toGo = deadline - time;
if (toGo < 0) { set 0; return };
debug { print("Still to go: " # debug_show toGo) };
set(fromIntWrap(time + toGo / 2));
}
}
```
# Open question(s)
- ~~Should the recurring timer be re-inserted with expiration relative to old expiration time or current time (of callback)?~~ — to avoid time skew, using the planned expiration time as the base
- Can we use `async*` to avoid the context switch when calling into `@timer_helper`? — probably yes, but in another PR
- Stopped canisters won't receive global timer expirations, but the global timer remains in the same state. So upon `canister_start` all expired timers will fire. Recurring timers may have expired several times in the canister's stopped state so will begin to catch up. This may lead to unnecessary repeated work. See #3871.
------------
TODO:
- [x] add `-no-timer` option to `test/perf` sources — dbfaf545b
- [x] release notes
- [x] check that `setTimer` documented as taking nanoseconds
- [ ] debug deactivated tests (compacting GC in `ic-ref` are a possible indication of GC bug! Or maybe starvation.)
- [x] stabilisation (needed? — CWV: no)
- [ ] tests
- [ ] after upgrade, still on? ("the timers get canceled on upgrade")
- [x] when defining `system func timer`, after upgrade it also gets called — 8df4d6f79d67877b43e386b52cdc9f28b9ecad82
- [x] can upgrade after a timer has run
- [x] test recurrent delay 0 — See 5b9b62faa and c440b3e8d.
- [ ] does `actor class` behaves correctly?
- [x] docs: `doc/md/heartbeats.md`
- [ ] what more is needed?
- [x] run with `ic-ref` too (currently no support, but see https://github.com/dfinity/interface-spec/pull/111)
- [x] `var` aliasing is being used to good effect (can we remove it?) — replaced in #3617
- [ ] check the invariant: no more expiration <=> global timer is 0
- [x] add `test/fail` for `-no-timer`
- [x] cancelling of a timer must be followed by compaction (to not break the corollary)
- [x] duration 0 with recurring timers should be excluded
- [x] traps in jobs, `catch`
- [x] add the low-level API `setGlobalTimer` (but see also: #3614)
- [x] try it on the Playground — _works!_
ggreif
December 29, 2022, 11:47pm
106
Q1: yes
Q2: no persistence, you have to set up your timers in post_upgrade
(this is explained in PR 3542)
Q3: yes, there is an issue for that
1 Like
ggreif
December 29, 2022, 11:53pm
107
This only means that the heartbeat system function is invoked via await
, which adds some lag. But there is no blockage. @claudio how to explain this better?
mparikh
December 30, 2022, 3:38am
108
What is also interesting is that, apparently, you cannot clear_timer out of the target function.
So in my use case of providing streaming local backups of memory managed stable stores, i have to launch another timer to cancel the timer (timer interval) of the main function that preps the backup.
You mean the function call has no effect, or it’s about passing timer_id
around?
mparikh
December 31, 2022, 8:03am
110
In general, a function that knows when to end it’s own timer_interval (and has access to the timer_id that does this time_interval triggering) but needs to execute for more than the cycle execution limit, it should be able to end it’s own time interval (at least logically).
Specifically , assuming thread_local’d BCK_COUNTER and BCK_TIMER
async fn start_bck_up() {
let bck_counter = BCK_COUNTER.with(|refcell| *&*refcell.borrow());
if bck_counter == 2 {
BCK_TIMER.with( |refcell| {
let timer_id = &mut *refcell.borrow_mut();
ic_cdk::timer::clear_timer(*timer_id);
});
}
BCK_COUNTER.with(move |refcell| {
refcell.replace(bck_counter + 1);
});
ic_cdk::println!("Called start_bck ONCE !!");
}
should ideally work. However this, while it compiles, produces a run-time panic.
[Canister rrkah-fqaaa-aaaaa-aaaaq-cai] Panicked at 'called `Option::unwrap()` on a `None` value', /home/user1/.cargo/registry/src/github.com-1ecc6299db9ec823/ic-cdk-0.6.8/src/timer.rs:230:73
[Canister rrkah-fqaaa-aaaaa-aaaaq-cai] in canister_global_timer: CanisterError: IC0503: Canister rrkah-fqaaa-aaaaa-aaaaq-cai trapped explicitly: Panicked at 'called `Option::unwrap()` on a `None` value', /home/user1/.cargo/registry/src/github.com-1ecc6299db9ec823/ic-cdk-0.6.8/src/timer.rs:230:73
My workaround is to have another timer chase this timer_interval (because this function executes in finite and roughly constant clock time) and the clear it from that timer function.
async fn stop_bck_up() {
let bck_counter = BCK_COUNTER.with(|refcell| *&*refcell.borrow());
if bck_counter > 2 {
ic_cdk::println!("Now stopping counter at ... {}", bck_counter);
BCK_TIMER.with( |refcell| {
let timer_id = &mut *refcell.borrow_mut();
ic_cdk::timer::clear_timer(*timer_id);
});
}
}
In this line we unconditionally set the swapped out task back, while it might have been removed by the func()
:
Task::Repeated { ref mut func, .. } => {
func();
TASKS.with(|tasks| *tasks.borrow_mut().get_mut(task_id).unwrap() = task);
}
Seems, we should check if the task_id
is still present in the tasks. We will fix it, mparikh, thanks for reporting.
1 Like
It’s great to enter 2023 with working timers both in Rust and Motoko. Very good job @AdamS , @lwshang , @ggreif , @claudio , @mraszyk and everyone involved!
We’ve captured already some feedback, we’re on it. If you have a suggestion, it’s never too late to share it in this thread.
Happy new year everyone! See you all in 2023!
4 Likes
Regarding recurring timers. What happens if a job takes more time than a delay? Will the 2nd job start executing before the 1st one finishes? Or… Will the 2nd job start executing right after the 1st one finishes? Or… Will the delay still be awaited after the 1st job finishes?
In short, “normal” async rules apply. Other executions might start only in the await
points of the 1st job.
If the 1st job is a long execution (i.e. continuous execution of WASM instructions longer than one round, no await
s), no 2nd job (or any other execution) might start until the long execution is finished.
If the 1st job await
s for a call to complete, any other execution might start at this point, including timers, inter-canister calls or ingress messages.
2 Likes
Hoi everyone,
It seems we have quite some differences across Motoko and Rust CDKs.
Motoko CDK provides timers feature opt-out flag -no-timer
and the following API:
setTimer : (Duration, () -> async ()) -> TimerId
recurringTimer : (Duration, () -> async ()) -> TimerId
cancelTimer : TimerId -> ()
In Rust CDK the timers
feature should be explicitly enabled and the following API is implemented:
set_timer(delay: Duration, func: FnOnce()) -> TimerId
set_timer_interval(interval: Duration, func: FnMut()) -> TimerId
clear_timer(id: TimerId)
What do you think guys of unifying somehow the flags and the API @ggreif @claudio @AdamS @lwshang
I am getting this same error. Please, can anyone help?
ggreif
January 12, 2023, 8:38am
117
First thing to make sure is that you have moc
0.7.5 installed, and dfx
does access it.
I used prefix DFX_MOC_PATH="$(vessel bin)/moc"
before command like:
DFX_MOC_PATH="$(vessel bin)/moc" dfx build <canister_name>
2 Likes
Iceypee
January 16, 2023, 11:54pm
122
is there some tutorial or reading on moc? I kinda found out where it is and how to use it but not really. And how to upgrade it is as well?
I think MOC used in dfx gets upgraded with dfx upgrade automatically, and dfx 0.12.1 do not use moc 0.7.5. So to use specific version, you can specify compiler version in vessel bin (vessel.dhall), and then use it for dfx build, using below command :
DFX_MOC_PATH="$(vessel bin)/moc" dfx build <canister_name>
Hopefully, the new dfx
release will be out soon, so we won’t need to hassle with the vessel
…
1 Like