James Thomas

Notes on JavaScript

Advanced Openwhisk Alarm Schedules

Apache OpenWhisk supports a cron-based alarm package for invoking serverless functions on a fixed schedule, e.g. every 5 minutes, every day at 5PM, once a week.

Scheduled events allow functions to be invoked for background processes or batch operations, like processing logs generated in the past 24 hours.

Using a cron-based schedule pattern, running functions once a minute, every two hours or 5pm on Mondays is simple, but what about more complex schedule patterns? πŸ€”

What if we need to…

  • ⏰ Fire a single one-off event at a specific time in the future?
  • ⏰ Fire events a fixed period of time from an action finishing?
  • ⏰ Fire events on an irregular schedule?

It is possible to implement all these examples with a few tricks… πŸ€Ήβ€β™‚οΈπŸ€Ήβ€β™‚οΈπŸ€Ήβ€β™‚οΈ.

Before we dive into the details, let’s review how the alarm feed provider works…

Alarm Trigger Feeds

OpenWhisk triggers are connected to external event sources using feed providers.

Feed providers listen to event sources, like message queues, firing triggers with event parameters as external events occur.

There are a number of pre-installed feed providers in the whisk.system namespace. This includes the alarms package which includes a feed provider (/whisk.system/alarms/alarm).

1
2
3
4
5
$ wsk package get /whisk.system/alarms --summary
package /whisk.system/alarms: Alarms and periodic utility
   (parameters: *apihost, *cron, *trigger_payload)
 feed   /whisk.system/alarms/alarm: Fire trigger when alarm occurs
   (parameters: none defined)

feed parameters

The following parameters are used to configure the feed provider.

  • cron - crontab syntax used to configure timer schedule.
  • trigger_payload - event parameters to fire trigger with.
  • maxTriggers - maximum number of triggers to fire (-1 for no limit).

cron is the parameter which controls when triggers will be fired. It uses the cron syntax to specify the schedule expression.

cron schedule format

Cron schedule values are a string containing sections for the following time fields. Field values can be integers or patterns including wild cards.

1
2
3
4
5
6
7
8
9
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ second (0 - 59, optional & defaults to 0)
# β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute (0 - 59)
# β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour (0 - 23)
# β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of month (1 - 31)
# β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ month (0 - 11)
# β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of week (0 - 6) (Sunday to Saturday)
# β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
# β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
# * * * * * *  

NOTE: Month field starts from 0 not 1, 0 is January with December being 11. Day of week also starts from 0. Sunday is first day of the week.

The second field is a non-standard cron field and does not need to be used. The Node.js module used to parse the cron schedules supports a value with five or six fields.

crontab examples

Here are some example patterns…

  • */10 * * * * * - run every 10 seconds
  • * * * * * - run every minute
  • 0 * * * * - run every hour
  • 0 */2 * * * - run every two hours
  • 30 11 * * 1-5 - run Monday to Friday at 11:30AM
  • 0 0 1 * * - run at midnight the first day of the month

https://crontab.guru/ is an online editor for generating cron schedule expressions.

Creating Alarm Triggers

Using the wsk cli triggers can be created using the alarm feed. Schedule and event parameters are passed in using command-line arguments (-p name value).

1
2
3
4
$ wsk trigger create periodic --feed /whisk.system/alarms/alarm -p cron '* * * * * *' -p trigger_payload '{"hello":"world"}'
ok: invoked /whisk.system/alarms/alarm with id 42ca80fbe7cf47318a80fbe7cff73177
...
ok: created trigger feed periodic

Trigger invocations are recorded in the activation records.

1
2
3
4
5
6
7
8
9
10
11
$ wsk activation list periodic
activations
d55d15297781474b9d15297781974b92 periodic
...
$ wsk activation get d55d15297781474b9d15297781974b92
ok: got activation d55d15297781474b9d15297781974b92
{
    "namespace": "user@host.com_dev",
    "name": "periodic",
    ...
}

Deleting the trigger will automatically remove the trigger from the alarm scheduler.

1
2
3
4
$ wsk delete periodic
ok: invoked /whisk.system/alarms/alarm with id 44e8fc5e76c64175a8fc5e76c6c175dd
...
ok: deleted trigger periodic

Programmatic Creation

The OpenWhisk JavaScript library can also register and remove triggers with feed providers.

1
2
3
4
5
6
7
8
const params = {cron: '* * * * * *', trigger_payload: {"hello":"world"}}
const name = '/whisk.system/alarms/alarm'
const trigger = 'periodic'
ow.feeds.create({name, trigger, params}).then(package => {
  console.log('alarm trigger feed created', package)
}).catch(err => {
  console.error('failed to create alarm trigger', err)
})

Triggers must already exist before registering with the feed provider using the client library.

Using the client library provides a mechanism for actions to dynamically set up scheduled events.

Advanced Examples

Having reviewed how the alarm feed works, let’s look at some more advanced use-cases for the scheduler…

Schedule one-off event at a specific time in the future

Creating one-off events, that fire at a specific date and time, is possible using the cron and maxTriggers parameters together.

Using the minute, hour, day of the month and month fields in the cron parameter, the schedule can be configured to run once a year. The day of the week field will use the wildcard value.

Setting the maxTriggers parameter to 1, the trigger is removed from the scheduler after firing.

happy new year example

What if we want to fire an event when the New Year starts?

Here’s the cron schedule for 00:00 on January 1st.

1
2
3
4
5
6
7
8
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ minute (0 - 59)
# β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ hour (0 - 23)
# β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of month (1 - 31)
# β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ month (0 - 11)
# β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ day of week (0 - 6) (Sunday to Saturday)
# β”‚ β”‚ β”‚ β”‚ β”‚
# β”‚ β”‚ β”‚ β”‚ β”‚
# 0 0 1 0 *

Here are the cli commands to set up a trigger to run at 01/01/2018 @ 00:00 to celebrate the new year.

1
2
3
4
$ wsk trigger create new_year --feed /whisk.system/alarms/alarm -p cron '0 0 1 0 *' -p maxTriggers 1 -p trigger_payload '{"message":"Happy New Year!"}'
ok: invoked /whisk.system/alarms/alarm with id 754bec0a58b944a68bec0a58b9f4a6c1
...
ok: created trigger new_year

Firing events a fixed period of time from an action finishing

Imagine you want to run an action on a loop, with a 60 second delay between invocations. Start times for future invocations are dependent on the finishing time of previous invocations. This means we can’t use the alarm feed with a fixed schedule like ’* * * * *’.

Instead we’ll schedule the first invocation as a one-off event and then have the action re-schedule itself using the JavaScript client library!

action code

Here’s the sample JavaScript code for an action which does that….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const openwhisk = require('openwhisk');

function calculateSchedule() {
  const now = new Date()
  const seconds = now.getSeconds()
  const nextMinute = (now.getMinutes() + 1) % 60
  return `${seconds} ${nextMinute} * * * *`
}

function main(params) {
  const ow = openwhisk();
  const params = {cron: calculateSchedule(), maxTriggers: 1}
  console.log(params)
  return ow.feeds.delete({name: '/whisk.system/alarms/alarm', trigger: 'delay'}).then(() => {
    console.log('delay trigger feed deleted.')
    return ow.feeds.create({name: '/whisk.system/alarms/alarm', trigger: 'delay', params: params})
    }).then(result => {
     console.log('delay trigger feed created.')
    })
    .catch(err => {
      console.error('failed to create/delete delay trigger', err)
      console.log("ERROR", err.error.response.result)
    })
}

setting up

  • Create an action called reschedule with code from above.
1
2
$ wsk action create reschedule reschedule.js
ok: created action reschedule
  • Create a trigger (delay) using the alarm feed, set to run in the next 60 seconds.
1
2
3
4
$ wsk trigger create delay --feed /whisk.system/alarms/alarm -p cron '* * * * * *'
ok: invoked /whisk.system/alarms/alarm with id b3da4de5726b41679a4de5726b0167c8
...
ok: created trigger delay
  • Connect the action (reschedule) to the trigger (delay) with a rule (reschedule_delay).
1
2
$ wsk rule create reschedule_delay delay reschedule
ok: created rule reschedule_delay

This action will continue to re-schedule itself indefinitely.

Stop this infinite loop by disabling or removing the rule connecting the action to the trigger.

1
2
$ wsk rule disable reschedule_delay
ok: disabled rule reschedule_delay

Firing events on an irregular schedule

How can you schedule events to occur from a predictable but irregular pattern, e.g. sending a daily message to users at sunrise?

Sunrise happens at a different time each morning. This schedule cannot be defined using a static cron-based pattern.

Using the same approach as above, where actions re-schedule triggers at runtime, events can created to follow an irregular schedule.

sunrise times

This external API provides the sunrise times for a location. Retrieving the sunrise times for tomorrow during execution will provide the date time used to re-schedule the action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "results": {
        "astronomical_twilight_begin": "5:13:40 AM",
        "astronomical_twilight_end": "6:48:52 PM",
        "civil_twilight_begin": "6:14:23 AM",
        "civil_twilight_end": "5:48:09 PM",
        "day_length": "10:40:26",
        "nautical_twilight_begin": "5:43:50 AM",
        "nautical_twilight_end": "6:18:42 PM",
        "solar_noon": "12:01:16 PM",
        "sunrise": "6:41:03 AM",
        "sunset": "5:21:29 PM"
    },
    "status": "OK"
}

action code

Here’s the sample JavaScript action that will re-schedule itself at sunrise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const openwhisk = require('openwhisk');
const request = require('request-promise');

function getNextSunrise(lat, lng, when) {
  const options = {
    uri: 'https://api.sunrise-sunset.org/json',
    qs: { lat: lat, lng: lng, when: when },
    json: true
  }

  return request(options)
    .then(result => result.results.sunrise)
}

function calculateSchedule(sunrise) {
  console.log('Next sunrise:', sunrise)
  const sections = sunrise.split(':')
  const hour = sections[0], minute = sections[1]
  return `${minute} ${hour} * * *`
}

function scheduleSunriseEvent (sunrise) {
  const ow = openwhisk();
  const params = {cron: sunrise, maxTriggers: 1}
  return ow.feeds.delete({name: '/whisk.system/alarms/alarm', trigger: 'sunrise'}).then(() => {
    console.log('trigger feed deleted.')
    return ow.feeds.create({name: '/whisk.system/alarms/alarm', trigger: 'sunrise', params: params})
  }).then(result => {
    console.log('trigger feed created.')
  })
  .catch(err => {
    console.error('failed to create/delete trigger', err)
    console.log("ERROR", err.error.response.result)
  })
}

function main(params) {
  console.log('GOOD MORNING!')

  return getNextSunrise(params.lat, params.lng, 'tomorrow')
    .then(calculateSchedule)
    .then(scheduleSunriseEvent)
}

setting up

  • Create an action called wake_up with code from above. lat and lng parameters define location for sunrise.
1
2
$ wsk action create wake_up wake_up.js -p lat 51.50 -p lng -0.076
ok: created action wake_up
  • Create a trigger (sunrise) with the alarm feed, scheduled for the next sunrise.
1
2
3
4
$ wsk trigger create sunrise --feed /whisk.system/alarms/alarm -p cron '03 41 06 * * *'
ok: invoked /whisk.system/alarms/alarm with id 606dafe276f24400adafe276f2240082
...
ok: created trigger sunrise
  • Connect the action (wake_up) to the trigger (sunrise) with a rule (wake_up_at_sunrise).
1
2
$ wsk rule create wake_up_at_sunrise sunrise wake_up
ok: created rule wake_up_at_sunrise

Checking the activation logs the following morning will show the trigger being fired, which invokes the action, which re-schedules the one-off event! πŸŒ…πŸŒ…πŸŒ…

Caveats

Here’s a few issues you might encounter using the alarm feed that I ran into….

  • Month field in cron schedule starts from zero not one. January is 0, December is 11.
  • Day of the week field starts from zero. First day of the week is Sunday, not Monday.
  • Feeds cannot be updated with a new schedule once created. Feeds must be deleted before being re-created to use a different schedule.

Future Plans

Extending the alarm feed to support even more features and improve the developer experience is in-progress. There are a number of Github issues in the official OpenWhisk repository around this work.

If you have feature requests, discover bugs with the feed or have other suggestions, please comment on the existing issues or open new ones.

Conclusion

Scheduled events are a necessary feature of serverless cloud platforms. Due to the ephemeral nature of runtime environments, scheduling background tasks must be managed by the platform.

In Apache OpenWhisk, the alarm feed allows static events to be generated on a customisable schedule. Using a cron-based schedule pattern, running functions once a minute, every two hours or 5pm on Mondays, is simple but what about more complex schedule patterns?

Using the cron and maxTriggers parameters with the OpenWhisk client library, much more advanced event schedules can be utilised within the platform. In the examples above, we looked at how to schedule one-off events, events using a predictable but irregular schedule and how actions can re-schedule events at runtime. πŸ’―πŸ’―πŸ’―

Comments