As you probably know, timers in Motoko give only a few guarantees (same applies to global replica timers, so we inherit this):
they expire at (or after) the specified delay
recurrent timers will stick to a regular expiration “grid”
I’d like to describe and discuss an issue with recurring timers in the default mechanism that might be slightly surprising to you.
When a recurring timer is added, the expiration grid is being defined as the current (round) time and the multiples of the specified delay (until upgrade). This is done to eliminate “wander”, and takes care that expected expiration times are equidistant. There will still be perceived “jitter” (variation after the expected expiration time) due to the underlying replica’s coarse temporal resolution.
One consequence of this is that if you set up a recurring timer with zero delay, all expirations coincide, so we treat these as one-off timers.
The other consequence is that you should not specify recurring timers with a small delay (less or comparable to the round time of the subnet) as the timer may lag the “grid” infinitely and never catch up.
Then there is the question about stopped canisters with recurring timers. Stopping also causes the suspension of global timer callbacks to the canister, and thus introducing bad jitter. When the canister is started, the default mechanism will try to process the jobs of all missed expirations according to the grid and thus exhibit a heartbeat-like pattern until the timer has caught up.
If this sounds like a behaviour negating the purpose of timers, you are probably right. The quick sequence of expirations causes unnecessary cycle burn and can probably not be amortised with useful work.
So what I am proposing is to skip missed grid points and catch up instantly by always setting the next scheduled expiration into the future. This would also fix the “too short” delay problem, but potentially break “counting” jobs that rely on every grid point spawning a fresh job.
How about pre-upgrades. If I wanted to wait x seconds/blocks to allow all my functions await calls to settle, can I throw a ten second timer in the preupgrade that executes all the upgrades after 10 seconds?
When control returns from the pre_upgrade hook, the IC will assume that all relevant data is written to stable memory and will thus activate the new binary. All information in the regular heap (where your new timers live) will be erased. So I doubt your idea will work, moreover you’ll expose yourself to data loss.
Please check with the canister lifecycle state machine that is compiled into the Motoko canisters, as it contains the necessary logic to only stop the canister when all outstanding messaging has come to an end.