Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cron schedules

Cron expressions say when something runs. zapline uses them in two places:

RunsUse it for
App::schedule(Schedule::cron(expr), job) (tasks)in every instance of the app, in memoryrefreshing a local cache, per-instance housekeeping
.jobs(|q| q.cron(expr, job)) (durable jobs)once per tick for the whole cluster, stored and retriedreports, billing runs, emails, anything that must happen exactly once per tick
#![allow(unused)]
fn main() {
use zapline::db::Database;
use zapline::jobs::Job;
use zapline::prelude::*;
use zapline::tasks::Schedule;

#[model]
struct InvoiceRun {}

impl Job for InvoiceRun {
    const NAME: &'static str = "invoice_run";
    async fn run(self, _res: Resources) -> Result<()> { Ok(()) }
}

fn app() -> App {
    App::new()
        .database(Database::from_env())
        // Each instance: every 5 minutes during office hours, UTC.
        .schedule(Schedule::cron("*/5 8-18 * * MON-FRI").name("warm-cache"), |_res: Resources| async move {
            // ...
        })
        // The cluster: 02:00 Paris time on the 1st, enqueued once.
        .jobs(|q| q.cron("CRON_TZ=Europe/Paris 0 2 1 * *", InvoiceRun {}))
}
}

zapline::cron is always compiled in. Only IANA time zone names (Europe/Paris) need the tz feature.

Syntax

Five fields, or six with seconds first:

┌───────────── second (0-59)          only in the 6-field form
│ ┌─────────── minute (0-59)
│ │ ┌───────── hour (0-23)
│ │ │ ┌─────── day of month (1-31)
│ │ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌─── day of week (0-7 or SUN-SAT; 0 and 7 are Sunday)
│ │ │ │ │ │
0 30 9 * * MON-FRI      09:30:00 on weekdays
  30 9 * * MON-FRI      the same, 5 fields

Each field is a comma-separated list of:

ItemMeaningExample
*every value* * * * * every minute
none value0 12 * * * at noon
a-ba range, which may wrap around0 22-2 * * * 22:00 to 02:00; FRI-MON
*/nevery n-th value*/15 * * * * every 15 minutes
a-b/nevery n-th value in a range0 9-17/2 * * * 09:00, 11:00, ... 17:00
a/nevery n-th value from a5/20 * * * * minutes 5, 25, 45
namesmonths and weekdays, any case0 0 1 JAN,jul *

Extensions in the day fields (from Quartz):

ItemFieldMeaning
?either day fieldsame as *
Lday of monththe last day of the month (28th to 31st)
L-nday of monthn days before the last day: L-2
nWday of monththe weekday nearest the n-th, inside the month: 15W
LWday of monththe last weekday of the month
d#nday of weekthe n-th such weekday of the month: FRI#2, 1#1
dLday of weekthe last such weekday of the month: 5L, SUNL

When both day fields are restricted (neither starts with * or ?), a day matches if either matches, as in classic cron: 0 0 1 * MON runs on the 1st and every Monday. If one of them starts with * or ?, both must match: 0 0 */2 * MON runs on Mondays that fall on an odd date.

Macros:

MacroSame as
@yearly, @annually0 0 1 1 *
@monthly0 0 1 * *
@weekly0 0 * * 0
@daily, @midnight0 0 * * *
@hourly0 * * * *

Expressions are checked when the app is built: a typo panics with the reason (invalid cron expression `0 25 * * *`: hour `25` is out of range 0-23), and so does an expression that can never match, such as 0 0 30 2 *. To handle errors yourself, for example for a schedule read from configuration, parse it first:

#![allow(unused)]
fn main() {
use zapline::cron::Cron;
use zapline::tasks::Schedule;

fn schedule_from_config(expr: &str) -> Result<Schedule, zapline::cron::CronError> {
    let cron: Cron = expr.parse()?;
    Ok(Schedule::cron(cron))
}
}

Time zones

An expression runs in UTC unless it starts with CRON_TZ=<zone> (or TZ=<zone>):

ZoneExampleNeeds
UTCCRON_TZ=UTC, Z, GMTnothing
A fixed offsetCRON_TZ=+05:30, -0800, UTC+2nothing
An IANA zoneCRON_TZ=America/New_Yorkthe tz feature
zapline = { version = "0.1", features = ["tz"] }

With tz, zone rules come from the system's time zone database (/usr/share/zoneinfo on Linux and macOS; bundled on Windows), through the jiff crate. Without it, an IANA name is an error that says to enable the feature. You can also set the zone in code with Cron::with_timezone(Tz::parse("Europe/Paris")?).

Fields are matched against the local time: CRON_TZ=+05:30 0 1 * * MON runs at 01:00 on Mondays in that zone, which is Sunday 19:30 UTC.

Daylight-saving time

Wall clocks skip an hour in spring and repeat one in autumn. zapline handles both without surprises:

  • Clocks go forward (02:00 jumps to 03:00): a time that doesn't exist that day, such as 30 2 * * *, runs once, at the jump (03:00 local). A schedule with several times in the missing hour also runs just once there.
  • Clocks go back (02:00 returns to 01:00): a time that happens twice, such as 30 1 * * *, runs once, the first time. A schedule whose hour field covers every hour, such as */15 * * * *, keeps its rhythm through both passes, so it never pauses for an hour.

UTC and fixed offsets have no daylight-saving changes, which is why they are the default.

Inspecting a schedule

Cron computes fire times without running anything, which is handy in tests and admin pages:

#![allow(unused)]
fn main() {
use std::time::{Duration, UNIX_EPOCH};
use zapline::cron::Cron;

let cron = Cron::parse("0 0 L * *")?;                // midnight on the last day of each month
let jan_1_2024 = UNIX_EPOCH + Duration::from_secs(1_704_067_200);
let next: Vec<_> = cron.upcoming(jan_1_2024).take(2).collect();
assert_eq!(next[0], UNIX_EPOCH + Duration::from_secs(1_706_659_200)); // 2024-01-31
assert_eq!(next[1], UNIX_EPOCH + Duration::from_secs(1_709_164_800)); // 2024-02-29
Ok::<(), zapline::cron::CronError>(())
}
MethodReturns
Cron::parse(expr) / expr.parse::<Cron>()the parsed expression or a CronError
cron.next_after(t)the first fire time strictly after t (whole seconds)
cron.upcoming(t)every fire time after t, as an iterator
cron.with_timezone(tz) / cron.timezone()change or read the zone
cron.to_string()the expression, with CRON_TZ= when the zone isn't UTC
Schedule::next_after(t)the same for a task schedule

In debug builds the dev console lists every scheduled task with its next run, and every cron job with its next enqueue.