Skip to content

How to use the scheduler in your zome

Paul d'Aoust edited this page Sep 30, 2021 · 1 revision

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_externs, 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 an Option<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:

Ephemeral scheduling

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

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.

Questions and answers

What can I do in a scheduled function?

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.

How do I specify input parameters to the scheduled function?

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.)

Why can’t I specify an initial schedule when I call the schedule host function?

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.

If I can’t return errors to the host, how should I handle them?

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.