-
Notifications
You must be signed in to change notification settings - Fork 0
Failure Detection and Retries
This page documents how yacron2 decides whether a job run failed (failsWhen), the exact precedence order of failure reasons, the retry mechanism with exponential backoff (onFailure.retry), and when each of the three report hooks (onFailure, onPermanentFailure, onSuccess) fires.
After a job process exits, yacron2 computes a single failure reason from the run's exit code and captured output. If the reason is non-empty the run is failed; otherwise it succeeded. Failure triggers onFailure reporting and, if a retry is configured and not yet exhausted, schedules another run after a backoff delay. When retries are exhausted (or none was configured) onPermanentFailure reporting fires. Success cancels any pending retry and fires onSuccess reporting.
failsWhen is a per-job (or per-defaults) block of four booleans. It is evaluated by RunningJob.fail_reason (yacron2/job.py) after the process exits and its streams have been read.
| Option | Type | Default | Description |
|---|---|---|---|
producesStdout |
Bool | false |
If true, any captured standard output marks the run as failed. |
producesStderr |
Bool | true |
If true, any captured standard error marks the run as failed. |
nonzeroReturn |
Bool | true |
If true, an exit code other than 0 marks the run as failed. |
always |
Bool | false |
If true, the run is always considered failed regardless of exit code or output. |
In the strictyaml schema (yacron2/config.py), only producesStdout is required within a failsWhen map; producesStderr, nonzeroReturn, and always are Opt(...). Defaults come from DEFAULT_CONFIG["failsWhen"] and are merged in before a failsWhen block is applied, so a partial failsWhen block inherits the defaults for the keys it omits.
Output detection considers both retained and discarded lines. A stream is treated as non-empty if it has saved content or if any lines were discarded (saveLimit exhausted, or saveLimit: 0). See Output Capturing for how captureStdout/captureStderr and saveLimit govern what is captured. If a stream is not captured, it cannot produce a failure reason: producesStderr only fires when captureStderr is enabled, and producesStdout only fires when captureStdout is enabled.
fail_reason returns the first matching condition, in this fixed order, and None if none match:
-
alwaysis true ->"failsWhen=always". -
nonzeroReturnis true andretcode != 0->"failsWhen=nonzeroReturn and retcode={retcode}". -
producesStdoutis true and stdout is non-empty or any stdout lines were discarded ->"failsWhen=producesStdout and stdout is not empty". -
producesStderris true and stderr is non-empty or any stderr lines were discarded ->"failsWhen=producesStderr and stderr is not empty".
The first match wins; later conditions are not evaluated. The resulting string is exposed to report templates as the fail_reason variable and to the shell reporter as YACRON2_FAIL_REASON. The boolean failed is simply fail_reason is not None.
Two synthetic exit codes are set by the runtime rather than the child process:
-
127: the subprocess could not be launched at all (e.g. the command does not exist, or the argv could not be encoded).RunningJobsetsstart_failedand, inwait(), assignsretcode = 127so the run is treated as an ordinary failure rather than raising an internal error. With the defaultnonzeroReturn: true, this is a failure. -
-100: the run exceeded itsexecutionTimeoutand was cancelled.wait()setsretcode = -100before terminating the process. With the defaultnonzeroReturn: true, this is a failure. See Concurrency and Timeouts.
A run cancelled to make way for a newer instance (concurrencyPolicy: Replace) is marked replaced and is not evaluated for failure, reported, or retried.
jobs:
- name: strict-job
command: ./run.sh
schedule: "*/5 * * * *"
captureStdout: true
captureStderr: true
failsWhen:
producesStdout: false
producesStderr: true
nonzeroReturn: true
always: falseRetries are configured under onFailure.retry. Retry orchestration lives in yacron2/cron.py (launch_scheduled_job, handle_job_failure, schedule_retry_job, cancel_job_retries); per-job backoff state is JobRetryState in yacron2/job.py.
| Option | Type | Default | Description |
|---|---|---|---|
maximumRetries |
Int | 0 |
Number of retries after the initial failed run. 0 disables retrying. -1 retries forever. |
initialDelay |
Float | 1 |
Delay in seconds before the first retry. |
maximumDelay |
Float | 300 |
Upper bound in seconds on the backoff delay. |
backoffMultiplier |
Float | 2 |
Factor the delay is multiplied by after each retry. |
In the schema, all four keys are required within a retry map (no Opt(...)), but the entire retry map is optional and the DEFAULT_CONFIG values above are merged in, so a job that omits retry entirely gets these defaults. If a retry map is given, strictyaml requires all four keys to be present (a partial retry block is a validation error). Numeric ranges are validated in JobConfig._validate_numeric_ranges and raise ConfigError on violation:
maximumRetries >= -1initialDelay >= 0maximumDelay > 0backoffMultiplier > 0
JobRetryState.next_delay() returns the current delay, then advances it for the next retry:
delay = current delay (returned, used to sleep)
next delay = min(current delay * backoffMultiplier, maximumDelay)
The first retry waits initialDelay; each subsequent retry waits the previous delay times backoffMultiplier, capped at maximumDelay. With initialDelay: 1, backoffMultiplier: 2, maximumDelay: 30, the delay sequence is 1, 2, 4, 8, 16, 30, 30, ... seconds. The retry counter (count) increments on each next_delay() call.
- A retry state is created only when
maximumRetriesis truthy (non-zero). WithmaximumRetries: 0no state is created and a failed run goes straight to permanent failure. -
launch_scheduled_jobcallscancel_job_retries(name)before starting a scheduled run, then creates a freshJobRetryState. A scheduled run therefore resets any in-progress retry sequence for that job. A manually triggered run (POST /jobs/{name}/start, see HTTP Control API) goes throughmaybe_launch_jobdirectly and does not reset or create retry state; it reuses whatever retry state currently exists. - On each failed run,
handle_job_failurefiresonFailurereporting, then: if no retry state exists or it was cancelled, firesonPermanentFailureand stops; otherwise, ifcount >= maximumRetriesandmaximumRetries != -1, cancels the retry state and firesonPermanentFailure; otherwise schedules the next retry afternext_delay()seconds. - A success (
handle_job_success) callscancel_job_retriesand firesonSuccess, ending the sequence. - If a job is removed from the configuration while a retry is pending,
schedule_retry_joblogs a warning, discards the stale retry state, and skips the run cleanly (no exception). - When leader election is enabled (
cluster.electLeader),schedule_retry_jobre-checks the cluster gate before relaunching. A transient fail-closed condition (lost quorum, a detected conflict, a rebuilt gossip manager's still-converging view, a backend read error) does not end the sequence: the retry state is kept and the gate is re-checked after another delay of the same length (floored at one second; the first deferral of a wait is logged at INFO, repeats at DEBUG), so a keep-alive job survives the blip. Only when another node is positively identified as the job's owner is the pending retry abandoned: the retry state is cancelled and discarded, a WARNING is logged, and the abandonment is recorded in the run history ascancelled. An abandoned sequence ends without firingonPermanentFailure, and the failed attempt is not re-run elsewhere: the new owner only picks up the job's future scheduled firings, which an@rebootone-shot does not have (its boot run is already recorded, so an abandoned@rebootkeep-alive ends cluster-wide). See Clustering and Leader Election. - On shutdown, all pending retries are cancelled before yacron2 exits.
jobs:
- name: flaky-job
command: ./flaky.sh
schedule: "*/10 * * * *"
captureStderr: true
onFailure:
report:
mail:
from: cron@example.com
to: ops@example.com
smtpHost: 127.0.0.1
retry:
maximumRetries: 10
initialDelay: 1
maximumDelay: 30
backoffMultiplier: 2A schedule of @reboot runs the job once at yacron2 startup. Combined with maximumRetries: -1, this re-launches the process whenever it exits with a failure, indefinitely: a way to keep a long-running process alive under yacron2.
jobs:
- name: keep-alive
command: ./long-running-server
schedule: "@reboot"
onFailure:
retry:
maximumRetries: -1
initialDelay: 1
maximumDelay: 30
backoffMultiplier: 2See Schedules and Timezones for @reboot semantics.
Each hook has its own independent report block (Sentry, mail, shell, webhook), defaulted from _REPORT_DEFAULTS (deep-copied per hook so they do not alias). All four reporters in a block run for the relevant outcome; reporting errors are logged and do not abort the others. See Reporting (Mail, Sentry, Shell, Webhook) for the report block options.
| Hook | Fires when | Frequency |
|---|---|---|
onFailure.report |
Every failed run. | Once per failed attempt (including each retry that fails). |
onPermanentFailure.report |
Retries are exhausted, or no retry was configured, or the retry state was cancelled. | Once, at the end of a failing sequence. |
onSuccess.report |
The run succeeded (fail_reason is None). |
Once per successful run. |
With no retry configured, a single failed run fires both onFailure.report (always) and then onPermanentFailure.report (because there is no retry state). To report only after all retries are exhausted, leave onFailure.report empty and configure onPermanentFailure.report instead, as in the example below.
jobs:
- name: eventually-consistent
command: ./run.sh
schedule: "*/10 * * * *"
captureStderr: true
onFailure:
retry:
maximumRetries: 10
initialDelay: 1
maximumDelay: 30
backoffMultiplier: 2
onPermanentFailure:
report:
mail:
from: cron@example.com
to: ops@example.com
smtpHost: 127.0.0.1A note on mail reporting: an onSuccess mail whose rendered body is empty (after strip()) is suppressed (no email is sent). This applies only to success reports.
-
nonzeroReturnchecksretcode != 0, so both the synthetic127(launch failure) and-100(timeout) codes count as non-zero failures under the default. - The
failsWhenevaluation runs once per completed run, including each retried run, so a retry that still produces stderr (withproducesStderr: true) fails again and continues the backoff sequence. - Output-based failure (
producesStdout/producesStderr) depends on stream capturing; withoutcaptureStdout/captureStderrthe corresponding condition can never trigger because nothing is captured. - During shutdown,
handle_job_failurereturns early if the stop event is set: a job that finishes failing while yacron2 is shutting down is not reported (onFailure/onPermanentFailuredo not fire) and is not retried. A job that finishes successfully during shutdown still cancels its retries and firesonSuccess.
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