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) or10-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:
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)
)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:
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 % 60000A 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:
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
)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:
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().kindRunning 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:
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").runCountjob(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:
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().lenclear() 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:
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()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:
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)
)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:
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 doneSee std:retry for the retry options, and std:thread for running work on threads.
When does a job with the schedule 0 9 * * 1-5 run?
What does cron.isValid("0 0 31 2 *") return?
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.
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) booleanDeprecated. 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.CronDeprecated. Deprecated: use cron.new() instead.
create returns a new scheduler.
cron.disableJob(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.disable(name) instead.
disableJob disables a job of the shared scheduler.
cron.enableJob(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.enable(name) instead.
enableJob enables a job of the shared scheduler.
cron.getJob(any name) objectDeprecated. Deprecated: use cron.new() and scheduler.job(name) instead.
getJob describes a job of the shared scheduler.
cron.getJobCount() intDeprecated. Deprecated: use cron.new() and scheduler.jobs().len instead.
getJobCount counts the shared scheduler's jobs.
cron.getJobs() arrayDeprecated. Deprecated: use cron.new() and scheduler.jobs() instead.
getJobs describes the shared scheduler's jobs.
cron.getLastRun(any name) int64Deprecated. 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) int64Deprecated. 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) intDeprecated. 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) booleanDeprecated. Deprecated: use cron.new() and scheduler.isEnabled(name) instead.
isEnabled reports whether a job of the shared scheduler is enabled.
cron.isValid(string schedule) booleanisValid reports whether schedule is a valid five-field schedule that fires at least once.
cron.new() *cron.Schedulernew 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) numbernext 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) booleanDeprecated. Deprecated: use cron.new() and scheduler.remove(name) instead.
removeJob removes a job from the shared scheduler.
cron.runAll() objectDeprecated. Deprecated: use cron.new() and scheduler.runAll() instead.
runAll runs every job of the shared scheduler.
cron.runJob(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.run(name) instead.
runJob runs a job of the shared scheduler and reports whether it succeeded.
cron.runOnce() objectDeprecated. Deprecated: use cron.new() and scheduler.runAll() instead.
runOnce runs every job of the shared scheduler.
cron.start() *cron.TickerDeprecated. 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) booleanDeprecated. 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) booleanhas reports whether a job called name exists.
scheduler.isEnabled(string name) booleanisEnabled reports whether a job called name exists and is enabled.
scheduler.isRunning() booleanisRunning 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) booleanDeprecated. 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) booleanDeprecated. Deprecated: use cron.new() and scheduler.disable(name) instead.
disableJob disables a job and reports whether it existed.
cron.enableJob(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.enable(name) instead.
enableJob enables a job and reports whether it existed.
cron.getJob(any name) objectDeprecated. Deprecated: use cron.new() and scheduler.job(name) instead.
getJob describes one job, or returns null.
cron.getJobCount() intDeprecated. Deprecated: use cron.new() and scheduler.jobs().len instead.
getJobCount counts the jobs.
cron.getJobs() arrayDeprecated. 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) int64Deprecated. 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) int64Deprecated. 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) intDeprecated. Deprecated: use cron.new() and scheduler.job(name).runCount instead.
getRunCount returns how often a job has run, or 0.
cron.isEnabled(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.isEnabled(name) instead.
isEnabled reports whether a job exists and is enabled.
cron.removeJob(any name) booleanDeprecated. Deprecated: use cron.new() and scheduler.remove(name) instead.
removeJob removes a job and reports whether it existed.
cron.runAll() objectDeprecated. 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) booleanDeprecated. Deprecated: use cron.new() and scheduler.run(name) instead.
runJob runs an enabled job and reports whether it ran without throwing.
cron.runOnce() objectDeprecated. Deprecated: use cron.new() and scheduler.runAll() instead.
runOnce runs every job.
cron.start() *cron.TickerDeprecated. 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) booleanDeprecated. 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.