-
Notifications
You must be signed in to change notification settings - Fork 0
Schedules and Timezones
Every job's schedule determines when it runs; utc and timezone determine in which clock the schedule is evaluated. This page documents the three accepted schedule forms, how the daemon wakes and tests them, and how the effective timezone is resolved.
schedule is required on every job. The strictyaml schema accepts two YAML types for it (yacron2/config.py):
"schedule": Str()
| Map({
Opt("minute"): Str(),
Opt("hour"): Str(),
Opt("dayOfMonth"): Str(),
Opt("month"): Str(),
Opt("year"): Str(),
Opt("dayOfWeek"): Str(),
})
so schedule is either a string or an object. JobConfig._parse_schedule turns that raw value into one of:
- a
crontab.CronTabinstance (a parsed crontab expression), or - the literal string
"@reboot".
Any other value raises ConfigError("invalid schedule: ...").
The crontab dialect is parse-crontab (josiahcarlson/parse-crontab), pinned as crontab>=1,<2. Field syntax (ranges 1-5, steps */5, lists 1,15,30, names like mon/jan) follows that library, not the system cron(5) man page.
A standard five-field crontab expression: minute hour day-of-month month day-of-week.
jobs:
- name: every-five-minutes
command: echo hello
schedule: "*/5 * * * *"The string is passed verbatim to CronTab(...); yacron2 does not pre-validate or rewrite it, so a malformed expression surfaces as whatever parse-crontab raises at config-load time. Quote the value in YAML — a bare */5 * * * * is not valid YAML scalar syntax in all positions.
The exact string "@reboot" is stored as-is (not parsed into a CronTab). A @reboot job runs only once, at daemon startup, and never on a recurring schedule.
jobs:
- name: warm-cache
command: /usr/local/bin/warm-cache
schedule: "@reboot"Behavior comes from Cron.job_should_run (yacron2/cron.py): on the first scheduler pass the startup flag is True and a @reboot job returns True; on every subsequent pass startup is False, so @reboot jobs return False. Conversely, CronTab-scheduled jobs return False during the startup pass and are only evaluated on later passes. There is no recurring "@reboot". To keep a long-running process alive, README.md recommends a @reboot schedule combined with onFailure.retry.maximumRetries: -1 (retry forever), so yacron2 relaunches the process whenever it exits/fails.
"@reboot" is the only @-keyword recognized by yacron2 itself. Other shorthands (@daily, @hourly, etc.) are not intercepted by _parse_schedule; whether they work depends entirely on whether parse-crontab accepts them.
An object lets you name fields individually. Each omitted key defaults to "*".
jobs:
- name: noon-on-weekdays
command: echo hello
schedule:
minute: "0"
hour: "12"
dayOfWeek: "mon-fri"_parse_schedule builds a five-field crontab string from exactly these keys and feeds it to CronTab:
| Object key | Crontab field | Default if omitted |
|---|---|---|
minute |
field 1 | * |
hour |
field 2 | * |
dayOfMonth |
field 3 | * |
month |
field 4 | * |
dayOfWeek |
field 5 | * |
The assembled string is f"{minute} {hour} {day} {month} {dow}". All values are typed Str() in the schema, so write minute: "0", not minute: 0 — although strictyaml will coerce an unquoted scalar to a string here, quoting is the documented convention and avoids surprises with values like "7".
The schema declares Opt("year"): Str(), and README.md shows a schedule object that includes year: 2017. _parse_schedule never reads year. It assembles only a five-field string from minute/hour/dayOfMonth/month/dayOfWeek; schedule_unparsed.get("year", ...) is never called and year is not appended to the crontab string.
Consequences:
- A
yearkey is accepted by the schema (no validation error) but has no effect on scheduling. - The README example claiming a job "only on the specific date 2017-07-19" does not restrict by year — it runs every July 19th, every year, that matches the other fields.
This is a discrepancy between the schema/README and the implementation. Do not rely on year. If you need a one-shot run, use @reboot plus an external guard, or remove the job after it fires.
The clock used to evaluate a schedule is resolved by JobConfig._resolve_timezone, driven by two job options:
| Option | Type | Default | Description |
|---|---|---|---|
utc |
Bool | true |
When no timezone is set: true evaluates the schedule in UTC; false uses the host's naive local time. |
timezone |
Str | (unset; None) |
IANA timezone name (e.g. America/Los_Angeles). When set, it overrides utc. New in yacron2 0.11. |
Resolution order (timezone wins):
- If
timezoneis set, the job usesZoneInfo(timezone). An unknown name raisesConfigError("unknown timezone: ..."). Theutcvalue is ignored in this case. - Else if
utcistrue(the default), the job usesdatetime.timezone.utc. - Else (
utc: false, notimezone) the resolved tzinfo isNone, i.e. naive host local time.
The resolved value is a datetime.tzinfo (or None) stored on the job and passed to get_now(job.timezone) when the schedule is tested. Because utc is true by default, schedules are interpreted in UTC unless you opt out.
Timezone names are resolved via the standard-library zoneinfo, with the tzdata package providing the database. yacron2 depends on tzdata>=2024.1 so resolution works on minimal/distroless images that lack a system zoneinfo database. (Prior to the migration documented in HISTORY.md, yacron2 used pytz; invalid timezones now raise ConfigError rather than being silently accepted.)
Local time:
jobs:
- name: nightly-local
command: /usr/local/bin/nightly
schedule: "27 19 * * *" # 19:27 host local time
utc: falseExplicit timezone:
jobs:
- name: nightly-la
command: /usr/local/bin/nightly
schedule: "27 19 * * *" # 19:27 America/Los_Angeles
timezone: America/Los_Angelesutc and timezone are ordinary job options and can be set per job or in a defaults block. See the Configuration Reference for where they sit among all options.
The scheduler does not run a per-job timer. It wakes on a fixed cadence and tests every job (yacron2/cron.py):
-
WAKEUP_INTERVAL = datetime.timedelta(minutes=1). -
next_sleep_interval()computes the time until the next minute boundary using UTC:now.replace(second=0) + WAKEUP_INTERVAL. The daemon therefore wakes aligned to the top of each wall-clock minute (in UTC), not at a fixed N-second period from startup. - On each wake,
spawn_jobsiterates all jobs and callsjob_should_run. For aCronTabjob it evaluatescrontab.test(get_now(job.timezone).replace(second=0))— the current time truncated to the minute in the job's resolved timezone.
Implications:
-
Sub-minute schedules are meaningless. The seconds component is zeroed before testing, and the daemon only wakes once per minute, so the finest effective resolution is one minute. A crontab expression cannot schedule anything more frequent than every minute via yacron2, regardless of what
parse-crontabitself supports. - A job whose schedule matches a given minute fires at most once for that minute (one test per wake). If multiple instances would overlap,
concurrencyPolicygoverns the outcome. - The reload/sleep loop catches up to the boundary even if a wake is slightly late; alignment is recomputed each iteration from the current UTC time.
- A job that is disabled (
enabled: false) returnsFalsefromjob_should_runregardless of schedule and never fires, including at@reboot.
The HTTP control API /status endpoint reports, per job, either running, disabled, or scheduled with a scheduled_in value. For CronTab jobs that value is crontab.next(now=now, default_utc=job.utc) evaluated in the job's timezone; for @reboot jobs it is the literal string @reboot. This is the recommended way to verify that a schedule resolves to the instant you expect.
See also: Commands and Environment, Concurrency and Timeouts, Troubleshooting and FAQ.
This wiki documents yacron2. See the README and the changelog.
yacron2 is a fork of gjcarneiro/yacron.
- Getting Started
- Configuration
- Job Behavior
- Integrations
- Reference and Development