Packages

cron

Describe when something should run with five-field cron schedules, work out the next run, and run jobs from a background scheduler.

import "std:cron"

A cron schedule is a short text that says when something should run: every fifteen minutes, at 09:00 on weekdays, on the first of the month. std:cron parses those schedules, tells you when one next fires, and runs functions on them from a scheduler in the background.

Reading a schedule#

A schedule has five fields separated by spaces: minute, hour, day of month, month and day of week (0 or 7 is Sunday, 1 is Monday). Each field is one of:

  • *, every value.
  • A number, such as 9.
  • A range, such as 9-17.
  • A step, such as */15 (every 15th) or 10-50/10.
  • A comma-separated list of those, such as 0,30.

When both day fields are restricted, a day matches if either one does. Times are local time.

cron.isValid(schedule) says whether a schedule parses and fires at least once:

main.osl
import "std:cron"

string[] schedules = [
  "* * * * *", "*/15 * * * *", "0 9-17 * * 1-5", "0 0 1 1 *", "60 * * * *", "* * * *", "0 0 31 2 *"
]
for schedule in schedules (
  log schedule cron.isValid(schedule)
)
Click the code to edit it

60 is not a minute, the sixth schedule has four fields, and February 31st never comes, so a schedule that can never fire is invalid too. Names such as @daily aren't supported: write 0 0 * * *.

When does it next run?#

cron.next(schedule, options) returns when the schedule next fires after now, in unix milliseconds. The from option finds the next time after a moment you choose, which also makes a calculation repeatable:

main.osl
import "std:cron"

number start = 1700000000000

number first = cron.next("*/15 * * * *", {from: start})
number second = cron.next("*/15 * * * *", {from: first})

log first > start
log first - start <= 900000
log second - first
log first % 60000
Click the code to edit it

A quarter-hour schedule fires at most 15 minutes after any moment, and exactly 15 minutes after the previous fire. A fire time is always on a whole minute. next throws a ValueError for an invalid schedule, so check with isValid when the text comes from a user.

A scheduler with jobs#

cron.new() makes a scheduler with no jobs. add(name, schedule, fn) registers a job. Each name is unique. jobs() describes every job, sorted by name:

main.osl
import "std:cron"

auto scheduler = cron.new()
scheduler.add("backup", "0 3 * * *", def() -> (
  log "backing up"
))
scheduler.add("report", "0 9 * * 1-5", def() -> (
  log "sending report"
))

for job in scheduler.jobs() (
  log job.name job.schedule job.enabled job.runCount
)
Click the code to edit it

Adding a name that exists, an invalid schedule, or something that isn't a function throws, so mistakes show up when the program starts rather than at three in the morning:

main.osl
import "std:cron"

auto scheduler = cron.new()
scheduler.add("tidy", "0 * * * *", def() -> (
  log "tidying"
))

auto duplicate = try(scheduler.add("tidy", "30 * * * *", def() -> (
  log "again"
)))
log duplicate.unwrapErr().kind

auto bad = try(scheduler.add("other", "sometimes", def() -> (
  log "never"
)))
log bad.unwrapErr().kind
Click the code to edit it

Running a job now#

run(name) runs one job straight away, on the calling thread, and returns when it finishes. It works for disabled jobs too, and an error the job throws propagates to the caller. runAll() runs every enabled job one after another in name order. Both make the job's record update, so a scheduled job can be tested without waiting for its time:

main.osl
import "std:cron"

auto scheduler = cron.new()
scheduler.add("b-second", "0 * * * *", def() -> (
  log "second job"
))
scheduler.add("a-first", "*/5 * * * *", def() -> (
  log "first job"
))

scheduler.runAll()
scheduler.run("a-first")

auto info = scheduler.job("a-first")
log info.runCount
log info.lastRun != null
log scheduler.job("b-second").runCount
Click the code to edit it

job(name) returns an object with name, schedule, enabled, running (a run is in progress), nextRun and lastRun (unix milliseconds, null when not applicable) and runCount. It returns null when there is no such job. run on an unknown job throws a NotFoundError.

Enable, disable and remove#

A disabled job doesn't run on its schedule and has no next run, but run still works. remove deletes a job, and removing one that doesn't exist does nothing:

main.osl
import "std:cron"

auto scheduler = cron.new()
scheduler.add("sync", "*/10 * * * *", def() -> (
  log "syncing"
))

log scheduler.isEnabled("sync")
log scheduler.job("sync").nextRun != null

scheduler.disable("sync")
log scheduler.isEnabled("sync")
log scheduler.job("sync").nextRun

scheduler.enable("sync")
log scheduler.isEnabled("sync")

scheduler.remove("sync")
scheduler.remove("sync")
log scheduler.has("sync")
log scheduler.jobs().len
Click the code to edit it

clear() removes every job.

Starting the scheduler#

start() runs enabled jobs in the background whenever their schedule matches. It checks every second, runs each due job on its own thread, and keeps going until stop(). isRunning() says whether it is started. Starting a running scheduler, or stopping a stopped one, does nothing:

main.osl
import "std:cron"

auto scheduler = cron.new()
scheduler.add("tick", "* * * * *", def() -> (
  log "tick"
))

log scheduler.isRunning()
scheduler.start()
log scheduler.isRunning()
scheduler.stop()
log scheduler.isRunning()
Click the code to edit it

A job that is still running when it is due again is skipped, so a slow job never overlaps itself. A job that throws has its error printed and the scheduler keeps going. Runs already in progress finish after stop.

The program still ends when its main code ends, so a program that exists to run jobs must be kept alive. This one never returns, so it only compiles here:

main.osl
import "std:cron"
import "std:thread"

auto scheduler = cron.new()
scheduler.add("cleanup", "0 3 * * *", def() -> (
  log "cleaning up old files"
))
scheduler.add("heartbeat", "* * * * *", def() -> (
  log "still alive"
))

scheduler.start()
log "scheduler started, next cleanup at " ++ scheduler.job("cleanup").nextRun

while true (
  thread.sleep(1000)
)
Click the code to edit it

Keeping a job short#

Each run goes on a thread of its own. A job that can fail should handle its own failures, so nothing is lost in the scheduler's error printing:

main.osl
import "std:cron"
import "std:retry"

string[] done = []

auto scheduler = cron.new()
scheduler.add("download", "0 * * * *", def() -> (
  auto outcome = retry.call(def(int attempt) string (
    if attempt < 2 (
      throw error("NetError", "busy")
    )
    return "downloaded"
  ), {delay: 1})
  done.append(outcome.unwrapOr("gave up"))
))

scheduler.run("download")
log done
Click the code to edit it

See std:retry for the retry options, and std:thread for running work on threads.

Quick check

When does a job with the schedule 0 9 * * 1-5 run?

Quick check

What does cron.isValid("0 0 31 2 *") return?

Your turn

Run a job on demand

Add a job named "hello" that runs every five minutes and logs hi. Run it twice with run, then log its runCount.

main.osl
Ctrl Enter runs
Expected output
hi
hi
2

API reference#

54 members, generated from the std:cron source of OSL 9.3.0.

Functions

#cron.addJob(any name, any schedule, any callback) boolean

Deprecated. Deprecated: use cron.new() and scheduler.add(name, schedule, fn) instead.

addJob adds a job to the shared scheduler and reports false for an invalid schedule or a duplicate name.

#cron.clear()

Deprecated. Deprecated: use cron.new() and scheduler.clear() instead.

clear removes every job of the shared scheduler.

#cron.create() *cron.Cron

Deprecated. Deprecated: use cron.new() instead.

create returns a new scheduler.

#cron.disableJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.disable(name) instead.

disableJob disables a job of the shared scheduler.

#cron.enableJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.enable(name) instead.

enableJob enables a job of the shared scheduler.

#cron.getJob(any name) object

Deprecated. Deprecated: use cron.new() and scheduler.job(name) instead.

getJob describes a job of the shared scheduler.

#cron.getJobCount() int

Deprecated. Deprecated: use cron.new() and scheduler.jobs().len instead.

getJobCount counts the shared scheduler's jobs.

#cron.getJobs() array

Deprecated. Deprecated: use cron.new() and scheduler.jobs() instead.

getJobs describes the shared scheduler's jobs.

#cron.getLastRun(any name) int64

Deprecated. Deprecated: use cron.new() and scheduler.job(name).lastRun instead.

getLastRun returns a shared job's last run in unix seconds, or 0.

#cron.getNextRun(any name) int64

Deprecated. Deprecated: use cron.new() and scheduler.job(name).nextRun instead.

getNextRun returns a shared job's next run in unix seconds, or 0.

#cron.getRunCount(any name) int

Deprecated. Deprecated: use cron.new() and scheduler.job(name).runCount instead.

getRunCount returns how often a shared job has run, or 0.

#cron.isEnabled(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.isEnabled(name) instead.

isEnabled reports whether a job of the shared scheduler is enabled.

#cron.isValid(string schedule) boolean

isValid reports whether schedule is a valid five-field schedule that fires at least once.

#cron.new() *cron.Scheduler

new returns a Scheduler with no jobs. Add jobs with add, then call start to run them in the background.

#cron.next(string schedule, object? options) number

next returns when schedule next fires after now, in unix milliseconds. Options: from (unix ms) finds the next time after that instead. Throws ValueError when the schedule is invalid or never fires.

#cron.removeJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.remove(name) instead.

removeJob removes a job from the shared scheduler.

#cron.runAll() object

Deprecated. Deprecated: use cron.new() and scheduler.runAll() instead.

runAll runs every job of the shared scheduler.

#cron.runJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.run(name) instead.

runJob runs a job of the shared scheduler and reports whether it succeeded.

#cron.runOnce() object

Deprecated. Deprecated: use cron.new() and scheduler.runAll() instead.

runOnce runs every job of the shared scheduler.

#cron.start() *cron.Ticker

Deprecated. Deprecated: use cron.new() and scheduler.start() instead.

start runs the shared scheduler in the background.

#cron.stop(*cron.Ticker done)

Deprecated. Deprecated: use cron.new() and scheduler.stop() instead.

stop stops a background run started with start.

#cron.validateSchedule(any schedule) boolean

Deprecated. Deprecated: use cron.isValid(schedule) instead.

validateSchedule reports whether schedule parses and can fire.

*cron.Scheduler

#scheduler.add(string name, string schedule, any fn)

add registers fn to run on schedule under name, enabled. Throws ValueError when the schedule is invalid or never fires or a job already has that name, and TypeError when fn isn't a function.

#scheduler.clear()

clear removes every job.

#scheduler.disable(string name)

disable stops the job called name from running on its schedule; run can still run it. Throws NotFoundError when there is no such job.

#scheduler.enable(string name)

enable lets the job called name run on its schedule again, from its next matching time. Throws NotFoundError when there is no such job.

#scheduler.has(string name) boolean

has reports whether a job called name exists.

#scheduler.isEnabled(string name) boolean

isEnabled reports whether a job called name exists and is enabled.

#scheduler.isRunning() boolean

isRunning reports whether start has been called without a later stop.

#scheduler.job(string name) {enabled: boolean, lastRun: int?, name: string, nextRun: int?, runCount: int, running: boolean, schedule: string}?

job describes the job called name, or returns null when there is none. Keys: name, schedule, enabled, running (a run is in progress), nextRun (unix ms, null while disabled), lastRun (unix ms, null before the first run) and runCount.

#scheduler.jobs() {enabled: boolean, lastRun: int?, name: string, nextRun: int?, runCount: int, running: boolean, schedule: string}[]

jobs describes every job, sorted by name, with the keys job returns.

#scheduler.remove(string name)

remove deletes the job called name; removing a job that doesn't exist does nothing. A run already in progress finishes.

#scheduler.run(string name)

run runs the job called name now, on the calling thread, even when it is disabled, and returns when it finishes. An error the job throws propagates. Throws NotFoundError when there is no such job.

#scheduler.runAll()

runAll runs every enabled job now, one after another in name order, on the calling thread. Every job runs even if an earlier one throws; then the first error is rethrown.

#scheduler.start()

start runs enabled jobs in the background whenever their schedule matches, checking every second, until stop is called. Starting a running scheduler does nothing. A job that is still running when it is due again is skipped, and a job that throws has its error printed while the scheduler keeps running. The program still exits when its main code ends, so keep it alive (for example with a thread that waits) while jobs should run.

#scheduler.stop()

stop stops running jobs on their schedules; runs in progress finish. Stopping a scheduler that isn't running does nothing.

*cron.Cron

#cron.addJob(any name, any schedule, any callback) boolean

Deprecated. Deprecated: use cron.new() and scheduler.add(name, schedule, fn) instead.

addJob adds a job and reports false for an invalid schedule or a duplicate name.

#cron.clear()

Deprecated. Deprecated: use cron.new() and scheduler.clear() instead.

clear removes every job.

#cron.disableJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.disable(name) instead.

disableJob disables a job and reports whether it existed.

#cron.enableJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.enable(name) instead.

enableJob enables a job and reports whether it existed.

#cron.getJob(any name) object

Deprecated. Deprecated: use cron.new() and scheduler.job(name) instead.

getJob describes one job, or returns null.

#cron.getJobCount() int

Deprecated. Deprecated: use cron.new() and scheduler.jobs().len instead.

getJobCount counts the jobs.

#cron.getJobs() array

Deprecated. Deprecated: use cron.new() and scheduler.jobs() instead.

getJobs describes every job as {name, schedule, enabled, nextRun, lastRun, runCount} objects with times in unix seconds.

#cron.getLastRun(any name) int64

Deprecated. Deprecated: use cron.new() and scheduler.job(name).lastRun instead.

getLastRun returns a job's last run in unix seconds, or 0.

#cron.getNextRun(any name) int64

Deprecated. Deprecated: use cron.new() and scheduler.job(name).nextRun instead.

getNextRun returns a job's next run in unix seconds, or 0.

#cron.getRunCount(any name) int

Deprecated. Deprecated: use cron.new() and scheduler.job(name).runCount instead.

getRunCount returns how often a job has run, or 0.

#cron.isEnabled(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.isEnabled(name) instead.

isEnabled reports whether a job exists and is enabled.

#cron.removeJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.remove(name) instead.

removeJob removes a job and reports whether it existed.

#cron.runAll() object

Deprecated. Deprecated: use cron.new() and scheduler.runAll() instead.

runAll runs every job and returns an object of {success, duration, count} keyed by job name.

#cron.runJob(any name) boolean

Deprecated. Deprecated: use cron.new() and scheduler.run(name) instead.

runJob runs an enabled job and reports whether it ran without throwing.

#cron.runOnce() object

Deprecated. Deprecated: use cron.new() and scheduler.runAll() instead.

runOnce runs every job.

#cron.start() *cron.Ticker

Deprecated. Deprecated: use cron.new() and scheduler.start() instead.

start checks for due jobs every second in the background and returns a token for stop.

#cron.stop(*cron.Ticker done)

Deprecated. Deprecated: use cron.new() and scheduler.stop() instead.

stop stops the background checks start began; stopping twice does nothing.

#cron.validateSchedule(any schedule) boolean

Deprecated. Deprecated: use cron.isValid(schedule) instead.

validateSchedule reports whether schedule parses and can fire.

*cron.Ticker

A handle with no methods of its own: pass it to the package's functions.