-
Notifications
You must be signed in to change notification settings - Fork 0
How to use the scheduler in your zome
Sometimes a hApp will want to do something in the background without user interaction. You can always do this behind the scenes in your UI’s code, but what if the UI is closed and this background task is important to how the hApp works?
The conductor exposes a host function called schedule
to your zome code. You can use it to schedule other functions inside your zome code. These scheduled functions shoud just be normal hdk_extern
s, like zome functions or special callbacks, with two differences:
- They must receive an
Option<
Schedule
>
as their input, which tells them what schedule they're currently running on. They must also return anOption<Schedule>
as their output, which tells the conductor what schedule they should be run on in the future. - They must be marked up with
#[hdk_extern(infallible)]
, which means that they must not return an error. Instead, they should handle errors by making decisions about whether to continue scheduling themselves.
When a zome call first schedules a function, it’ll be run shortly after the zome call completes. (The scheduler clock ticks every ten seconds.) On this first run, it doesn’t have a schedule defined, so it receives None
as its input. Now it has a decision to make:
- Run only this once, returning
None
once it’s done. - Run itself again after a period of time, returning
Some<Schedule::Ephemeral>
containing theDuration
to wait. - Run itself on a recurring schedule, returning
Some<Schedule::Persisted>
containing a crontab format string.
Ephemeral scheduling is useful when you want to schedule something to happen once in the future, similar to setTimeout
in browser JavaScript. It isn’t guaranteed to happen immediately at the exact duration specified, and it isn’t expected to persist if the conductor is restarted. And if an ephemeral scheduled function fails unexpectedly (say, because of a full disk), it won’t be retried.
Some use cases:
- Retry a task that can 'soft fail' (e.g., network timeout when trying to contact a peer) with exponential backoff.
- Loop 'best effort' tasks with a sleep between each attempt.
- In combination with a persisted scheduler on long time intervals, quickly work through a queue of tasks one by one until completed.
Persisted scheduling works like Linux cronjobs or the Windows task scheduler. It runs the scheduled function on a recurring schedule, making a best effort to get as close to the specified time as possible. As the name suggests, the schedule is persisted across conductor restarts. If execution fails unexpectedly at one time interval, it will still retry on the next intervals.
As we've seen, a scheduled function’s first run happens shortly after the completion of the zome call that scheduled it, and at this point it doesn’t know how it’s meant to be scheduled in the future. For persisted functions, this means that you’ll probably want to check for a None
schedule input and return early with the actual crontab string, skipping its actual work. On subsequent runs, you’ll want it to do its work and return the same schedule it received — unless it has a reason to change or cancel the schedule, of course.
Some use cases:
- Attempt to complete pre-authorised countersigning sessions.
- Generate periodic reports, notifications, or invoices.
- Clear out stale data and other hApp-specific housekeeping tasks.
Anything you can normally do in a zome function — almost. The only things your scheduled functions can’t do is receive input parameters or throw an error — there’s no direct interaction between the scheduled function and the hApp’s client.
In JavaScript, you can specify input parameters for functions scheduled with setTimeout
or setInterval
by wrapping the function in a closure. Unfortunately you can’t do closures in WebAssembly yet, and our WASM environment is stateless by design. The best place to store calling context for your scheduled functions is as a private entry on the agent’s source chain. (When you use that data to make decisions about rescheduling, make sure the data has been sanitised — see the notes below about not being able to specify an initial schedule.)
What happens when a function is scheduled twice? If you try to reschedule a function that’s already scheduled to run in the future, it won’t be double-scheduled. (That is, scheduling is an idempotent action.)
This reduces the potential for attacks involving remote calls — if a malicious agent could schedule a compute-expensive task with a tight schedule on another agent’s node, they could cause serious problems. Instead, the scheduled function's logic makes its own decisions about when to reschedule, protecting the agent from injection of malicious parameters.
It depends on what the scheduled function is doing. If you’re designing an ephemeral function that tries to contact a peer with exponential backoff, a network timeout error means it should retry later. But if that function discovers data that prevents it from doing its work, it should definitely not try again. Likewise, a persisted function might fail at one interval but succeed the next interval; in both cases, it should probably still return the same persisted schedule it was called with. Unexpected system errors such as disk errors, on the other hand, are handled by the conductor and will cancel ephemeral functions.