-
Notifications
You must be signed in to change notification settings - Fork 0
Concurrency and Timeouts
This page documents how yacron2 handles overlapping runs of the same job
(concurrencyPolicy) and how it bounds the duration of a single run
(executionTimeout, killTimeout). These options are per-job (settable in
defaults) and govern only one launch of a job; they have no effect across
different jobs.
A job is identified by its name. yacron2 tracks, per name, a list of
currently-running instances. When a scheduled time arrives (or a manual start
is requested through the HTTP Control API), yacron2 checks whether
any instance of that job is already running and consults concurrencyPolicy
before launching a new one. Independently, each running instance carries a
deadline derived from executionTimeout; on expiry it is cancelled, and
killTimeout controls the SIGTERM-then-SIGKILL escalation used during any
cancellation.
| Option | Type | Default | Description |
|---|---|---|---|
concurrencyPolicy |
enum: Allow, Forbid, Replace
|
Allow |
Behaviour when a launch is requested while another instance of the same job is still running. |
executionTimeout |
float (seconds, > 0 when set) |
none (null) |
Maximum wall-clock duration of a single run. On expiry the run is cancelled and assigned return code -100. |
killTimeout |
float (seconds, >= 0) |
30 |
When a run is cancelled, seconds to wait after SIGTERM before sending SIGKILL. |
Types are from the strictyaml schema (concurrencyPolicy is
Enum(["Allow", "Forbid", "Replace"]); executionTimeout and killTimeout
are Float()). Defaults are from DEFAULT_CONFIG. All three options are
optional (Opt(...) in the schema). Numeric ranges are enforced after parsing:
killTimeout >= 0 and, when set, executionTimeout > 0; a violating value
raises a ConfigError at config load.
See the Configuration Reference for where these
options sit in the document and how defaults apply.
When maybe_launch_job is asked to start a job and one or more instances of
that name are already running, it logs a warning
(Job <name>: still running and concurrencyPolicy is <policy>) and then acts
according to concurrencyPolicy:
The new instance is started immediately alongside the existing one(s). Multiple instances of the same job can run concurrently with no bound on their number. Each instance is tracked and reaped independently.
jobs:
- name: ingest
command: ./ingest.sh
schedule: "* * * * *"
concurrencyPolicy: AllowIf any instance is still running, the new launch is skipped entirely; no new process is started. The already-running instance continues unaffected. This applies equally to scheduled launches and to retry-triggered launches.
jobs:
- name: ingest
command: ./ingest.sh
schedule: "* * * * *"
concurrencyPolicy: ForbidEvery currently-running instance of the job is cancelled, then a new instance
is started. Before cancelling, the scheduler sets replaced = True on each
outgoing instance. This flag changes how the finished run is reaped:
- The replaced run is not treated as a failure:
_handle_finished_jobreturns early whenreplacedis set, loggingJob <name> was replaced by a newer instance. - Because it is not a failure, it is not reported (no Mail/Sentry/Shell
reporters fire for it) and it does not trigger
retries.
cancel()itself does not set a return code, so whatever value the run's ownwait()task happened to record (the signal-derived code, or-100had its ownexecutionTimeoutexpired first) is irrelevant: the reaper short-circuits onreplacedbefore inspecting it.
Cancellation of the outgoing instance uses the same SIGTERM/killTimeout/SIGKILL
escalation described under Cancellation and killTimeout.
maybe_launch_job awaits each cancel() before starting the replacement, so
the new instance is launched only after the old one has terminated.
jobs:
- name: sync
command: ./sync.sh
schedule: "* * * * *"
concurrencyPolicy: Replace
killTimeout: 10executionTimeout bounds the wall-clock duration of a single run. It is unset
by default (null), meaning a run may take arbitrarily long.
(new in version 0.4)
When a run starts, if executionTimeout is set, RunningJob.start records an
absolute deadline using a monotonic clock:
execution_deadline = time.perf_counter() + executionTimeout
time.perf_counter() is used (not wall-clock time), so the deadline is immune
to system clock adjustments while the job runs.
When the run is awaited (RunningJob.wait):
- If no deadline is set, yacron2 waits indefinitely for the process to exit.
- If a deadline is set, the remaining time is computed as
execution_deadline - time.perf_counter(). If that remaining time is> 0, the process exit is awaited underasyncio.wait_for(..., timeout); if it is already<= 0, the timeout path is taken immediately.
On timeout (the remaining time elapses, or was non-positive), yacron2:
- Logs
Job <name> exceeded its executionTimeout of <N> seconds, cancelling it.... - Sets the run's return code to
-100. - Calls
cancel()to terminate the process (see below).
A -100 return code is therefore the marker of a timeout-induced termination.
For a normal (non-replaced) run, retcode = -100 is non-zero, so a job with
the default failsWhen.nonzeroReturn treats the timeout as a failure, which is
then reported and may be retried. See
Failure Detection and Retries for what happens
after a timeout-induced failure. (When the timed-out run was a Replace
victim, the replaced flag suppresses failure handling regardless of the
-100 code.)
jobs:
- name: maybe-hangs
command: |
echo "starting..."
sleep 2
echo "all done."
schedule:
minute: "*"
captureStderr: true
executionTimeout: 1 # seconds; cancel the run if still alive after 1sCancellation (RunningJob.cancel) is invoked both by an executionTimeout
expiry and by concurrencyPolicy: Replace. The sequence is:
- If the process is still running (
returncode is None), send SIGTERM viaproc.terminate(). AProcessLookupError(process already gone) is ignored. - Wait up to
killTimeoutseconds for the process to exit, usingasyncio.wait_for(proc.wait(), killTimeout). - If it has not exited by then, log
Job <name> did not gracefully terminate after <N> seconds, killing it...and send SIGKILL viaproc.kill().
killTimeout defaults to 30 seconds and must be >= 0. A value of 0 is
valid and means SIGKILL is sent almost immediately after SIGTERM (the
asyncio.wait_for with a zero timeout gives the process essentially no grace
period). The SIGTERM/SIGKILL escalation is POSIX-specific.
killTimeout gives a job time to flush buffers and clean up after being asked
to stop; raise it for jobs that need longer to shut down, lower it for jobs
that may ignore SIGTERM and must be force-killed quickly.
jobs:
- name: ignores-sigterm
command: |
trap "echo '(ignoring SIGTERM)'" TERM
echo "starting..."
sleep 10
echo "all done."
schedule:
minute: "*"
captureStderr: true
executionTimeout: 1
killTimeout: 0.5 # SIGKILL 0.5s after the (ignored) SIGTERM-
Per run.
executionTimeoutandkillTimeoutapply to a single instance of a job. The deadline is established at that instance'sstartand is not shared across instances. WithconcurrencyPolicy: Allow, each concurrent instance has its own independent deadline. -
Replace + timeout. A
Replacevictim is cancelled regardless of its ownexecutionTimeout; its termination is governed bykillTimeoutand is not reported as a failure (thereplacedflag). -
Manual starts. Launches via the HTTP Control API
(
POST /jobs/{name}/start) go through the samemaybe_launch_jobpath and thus honourconcurrencyPolicy. -
start-up failures vs. timeouts. A
-100return code specifically denotes a timeout-induced cancellation. A command that could not be launched at all (e.g. not found) is assigned127instead, on the normal failure path; see Commands and Environment and Failure Detection and Retries.
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