Skip to content

Releases: ptweezy/yacron2

1.2.7

Choose a tag to compare

@github-actions github-actions released this 07 Jul 04:03
Immutable release. Only release title and notes can be modified.

Full Changelog: 1.2.6...1.2.7

1.2.6

Choose a tag to compare

@github-actions github-actions released this 03 Jul 21:18
Immutable release. Only release title and notes can be modified.

A toolchain and packaging release. There are no behavior, API, or
configuration changes
and no change to the core dependency footprint: the
published wheel/sdist and the release binaries are built from the same sources
as before and install exactly the same runtime dependencies. The work adopts
uv across the paths where it pays off -- CI, the
build/release pipeline, and the local dev loop -- refreshes the container base
images, and pins the CI action versions.

  • uv on the runner-native CI, build, and dev paths. The dist build
    (uv build), the runner-native PyInstaller binaries (macOS, Windows, and the
    all-wheels Linux arches, each installed into a throwaway uv venv and frozen
    with uv run), the version probe (uv run --no-project --with setuptools-scm), and twine check (uvx twine) all run through uv now:
    parallel downloads and a shared global wheel cache make them markedly faster,
    and the results are behavior-identical. UV_PYTHON_DOWNLOADS=never keeps uv on
    the exact interpreter setup-python pinned rather than fetching a managed one,
    and UV_HTTP_TIMEOUT carries the same transient-network hardening the pip
    paths had.

  • The emulated foreign-arch binary legs stay on pip, on purpose. The
    musl and glibc-extra binary jobs (armv7/armv6, ppc64le, s390x, riscv64, i686)
    build inside docker run containers under QEMU, where uv is not a fit: its
    official image is amd64/arm64 only and it has no musl ppc64le/s390x wheels, so
    pip remains the arch-portable choice there and keeps PIP_RETRIES/PIP_TIMEOUT
    hardening. The uvloop bundling and per-arch --version smoke test are
    unchanged on every leg.

  • uv in the local dev loop. tox.ini now declares requires = tox-uv, so a
    plain tox auto-provisions its environments and installs dependencies with uv
    (much faster, behavior-identical); tox-uv is added to the dev extra and
    requirements_dev.txt. CONTRIBUTING.md documents the uv quickstart
    (uv venv, uv pip install -e ".[dev]") alongside the unchanged stock
    venv+pip path, and notes the tox --runner virtualenv escape hatch for
    anyone who wants the legacy runner.

  • Refreshed container base images. The Docker variant matrix moves to
    current bases: ubuntu 24.04 -> 26.04 (Python 3.12 -> 3.14), rhel UBI9 ->
    UBI10, fedora 41 -> 44 (3.13 -> 3.14), opensuse Leap 15.6 -> 16.0 (3.11 ->
    3.13), and distroless to Python 3.13. The Debian/Alpine images and every
    image tag stay as they were.

  • Internal: every CI-consumed action is pinned to an exact version
    (checkout@v7.0.0, setup-python@v6.3.0, setup-uv@v8.2.0, the docker/*
    actions, etc.), and a dependabot.yml is added to keep those pins and the
    Python dev dependencies current.

Full Changelog: 1.2.5...1.2.6

1.2.5

Choose a tag to compare

@github-actions github-actions released this 03 Jul 19:19
Immutable release. Only release title and notes can be modified.

A performance and footprint release. There are no behavior or configuration
changes
: every schedule fires exactly as before, the metrics endpoint renders
byte-for-byte identically, and the core install stays zero-new-dependency. The
work trims CPU on the daemon's hottest repeating paths -- the once-a-minute
config reload, every Prometheus scrape, and each cluster poll / gossip round /
lease renew -- lowers steady-state memory, and adds an optional faster event
loop.

  • Optional uvloop event loop (speedups extra). pip install yacron2[speedups] swaps asyncio's selector loop for uvloop's faster
    libuv-based one, speeding every I/O path yacron2 drives: cluster gossip and
    lease HTTP, the web dashboard, and the Prometheus scrape. It is entirely
    opt-in and best-effort -- __main__ selects uvloop lazily on POSIX and falls
    back to stock asyncio, behavior unchanged, whenever it is absent or
    unimportable -- so it stays off the core install to keep the baseline
    architecture-portable. Windows always uses its Proactor loop (there is no
    uvloop build there, and the Proactor loop is required for subprocess support
    anyway). The prebuilt POSIX binaries now bundle uvloop wherever it builds: a
    wheel where one exists, an otherwise verified source build, with a start-up
    self-test (verify_uvloop.py) that uninstalls a miscompiled build (a real
    risk under QEMU emulation) before freezing so the binary cleanly runs on
    asyncio instead. An arch where uvloop cannot build ships the asyncio binary
    exactly as before.

  • The once-a-minute reload no longer reparses an unchanged config. The
    scheduler rereads and reparses the config every minute so an on-disk edit is
    picked up promptly, but strictyaml is a slow pure-Python parser and reparsing
    an unchanged file was pure wasted work (in a worker thread, but still real CPU
    plus thread-pool churn). reload_config now compares a cheap os.stat
    fingerprint -- (path, mtime_ns, size) per file, plus the config directory's
    own mtime -- of exactly the files the last parse read (the top-level config,
    every transitively included file, and each job's env_file) and skips the
    reparse entirely when nothing has changed, returning the already-loaded
    config. A genuine edit, a vanished file, or a new entry dropped into a config
    directory still reparses on the next pass.

  • Cheaper Prometheus scrapes. The job-set fingerprint (job_set_id, queried
    on every scrape and every cluster poll / gossip round / lease renew) is a pure
    function of the loaded jobs, so it is now computed once per reload and
    memoized rather than re-deriving its per-job deepcopy / JSON / SHA-256 each
    time. The job_next_run_timestamp gauge reads the scheduler's authoritative
    next-fire index instead of re-walking every crontab and building two aware
    datetimes per job per scrape (falling back to a direct computation only in the
    brief start-up window before the index is seeded). The histogram le label
    strings are precomputed once from the bucket bounds rather than re-rendered for
    every bucket of every job on every scrape.

  • Lower steady-state memory and faster attribute access. JobConfig -- one
    instance per configured job for the life of the process -- now declares
    __slots__, trimming its per-instance __dict__ and speeding the attribute
    reads on the scheduling hot path. Fingerprint redaction is now copy-on-write
    instead of deepcopy, so the long immutable report templates (the sentry body,
    the webhook body) are shared by reference rather than duplicated on each
    fingerprint. The PyInstaller binaries are now built with optimize=2, which
    strips docstrings and (side-effect-free, internal-invariant) asserts from the
    frozen bytecode -- yacron2's modules are deliberately docstring-dense, so this
    shrinks the binary and lowers resident memory for the life of the daemon.

  • The idle scheduler no longer polls once a second. The job reaper waited on
    its "any job running?" event with a one-second timeout, waking every second
    even when nothing was running. It now blocks on the event outright -- the wait
    condition can only change when a job launches or shutdown is signalled, both of
    which set the event (shutdown now does so explicitly, so the reaper exits
    promptly) -- so a fully idle daemon does no per-second work. A related fix reads
    the running-jobs map with .get() so a concurrency check can no longer leave a
    phantom empty entry that would spin the reaper hot at shutdown.

  • Internal: the reload skip cache (change detection, the failed-parse and
    worker-thread paths), the memoized fingerprint, and the scrape reading the
    seeded next-fire index are covered by new tests; the uvloop bundling is gated
    behind a build-time verification step and the per-arch --version smoke test.

Full Changelog: 1.2.4...1.2.5

1.2.4

Choose a tag to compare

@github-actions github-actions released this 03 Jul 04:29
Immutable release. Only release title and notes can be modified.

This release re-implements the scheduler core added in 1.2.3 without changing
what it does: every schedule fires exactly when it did before, and there are no
configuration changes. The daemon no longer wakes on a fixed cadence and tests
every job against the clock; it keeps each job's next fire time in an index and
sleeps until the soonest one is due, servicing only the jobs whose moment
has arrived.

  • Per-wake cost scales with jobs due, not jobs configured. The previous
    loop matched every enabled job against the clock on every tick -- and with a
    second-level job present that tick was once a second, so the whole job set was
    scanned every second. The scheduler now maintains a next-fire index (each
    job's next-fire instant, mirrored in a min-heap), sleeps until the earliest
    entry, and touches only the jobs actually due. An idle wake over a large fleet
    is an O(1) heap peek that runs zero cron matching; a wake with a cohort due
    matches only that cohort. A deployment running thousands of sparsely-scheduled
    jobs pays dramatically less per wake, and adding a second-level job no longer
    imposes a per-second scan of everything else.

  • Robust across wall-clock and NTP steps. The sleep is realized against the
    event loop's monotonic clock, and firing compares the wall clock against
    fixed, forward-only next-fire instants. A clock step backward (an NTP
    correction or a manual set) now defers the pending fire instead of re-running
    a slot that already fired; a large step forward, a resume from suspend, or
    an RTC-less boot corrected far ahead resumes at the current slot in O(1)
    instead of enumerating and replaying every occurrence in the skipped span. The
    bounded catch-up for a genuinely overrun pass -- a slow config reload, a burst
    of simultaneous launches -- is retained, still capped at the ten-second
    CATCHUP_LIMIT, and now covers minute- and second-level jobs by the same
    path.

  • De-duplication is now structural. A fired slot cannot fire twice because
    advancing the index moves a job's next fire strictly past the slot it just
    fired; the old per-slot "did this already run?" gate is gone (_last_run_slot
    is kept only for status/introspection). All the surrounding guarantees are
    preserved: a job fires exactly once per matching slot, a mid-period restart
    skips the period already under way (the index is seeded strictly-future at
    start-up), @reboot jobs run once at boot, a config reload landing on a job's
    own boundary minute does not skip that fire, and housekeeping (config reload,
    cluster and web upkeep, logging) still runs at most once a wall-clock minute.

  • Internal: the next-fire index, monotonic sleep, clock-step handling, reload
    reconciliation, and a fleet-scale performance demonstration are covered by a
    new batch of scheduler tests; the wiki's "How the scheduler ticks" section is
    rewritten to describe the index.

Full Changelog: 1.2.3...1.2.4

1.2.3

Choose a tag to compare

@github-actions github-actions released this 03 Jul 02:54
Immutable release. Only release title and notes can be modified.

This release brings second-level (sub-minute) scheduling: a job can now
fire at second granularity, either through a new second field on the schedule
object or a full seven-field crontab string. The scheduler keeps its historical
once-a-minute cadence -- and its zero overhead -- until some enabled job
actually asks for seconds, at which point it ticks once a second, firing
second-level jobs on time while every minute-level job still fires exactly once
in its minute. The release also starts honoring the schedule object's year
key (previously accepted but silently dropped, a behavior change for the few
configs that set it), surfaces a malformed schedule as a named ConfigError at
reload instead of an anonymous traceback, teaches the web dashboard to parse,
describe, and preview five-, six-, and seven-field expressions, and ships two
runnable examples (pulse-monitor and its clustered sibling pulse-cluster)
built around second-level probing. Sub-minute scheduling is entirely opt-in;
see the upgrade notes below for the one behavior change that can affect an
existing deployment.

Second-level (sub-minute) scheduling

  • New second field and seven-field crontab strings. parse-crontab reads
    extra columns from the ends of a crontab line, so the field count selects
    the dialect: a five-field line has an implicit second of 0 and any year, a
    six-field line adds a trailing year column, and a seven-field line adds a
    leading second column too (second minute hour dayOfMonth month dayOfWeek year). So the object second: "*/15" and the seven-field string
    "*/15 * * * * * *" both fire every 15 seconds, while a six-field string pins
    a year and stays minute-granular. The second field takes the same syntax as
    any other (*, */5, 0,30, 10-20); second: "*" fires every second.
    Second-level scheduling is a YAML feature: classic crontab files stay
    five-field and minute-granular.

  • Adaptive cadence, zero cost when unused. The scheduler ticks once a second
    only while some enabled job pins a second (Cron._needs_subminute());
    otherwise it keeps the historical once-a-minute cadence, aligned to the top of
    each UTC minute, byte-for-byte as before. A disabled second-level job never
    forces the per-second cadence. The cadence is re-evaluated every tick, so a
    reload that adds or removes a second-level job switches modes on that same
    tick.

  • Exactly once per slot; mixed cadences. Each pass reads the clock once and
    tests every job against a single scheduling "slot" truncated to that job's own
    resolution -- the whole second for a second-level job, the top of the minute
    otherwise. Launches are de-duplicated per slot (_last_run_slot), so a
    minute-level job now tested up to 60 times in its due minute still fires
    exactly once, and a second-level job fires once per matching second even if two
    ticks land in the same second. A leader-gated job is evaluated once per slot
    regardless of which node runs it. Sub-minute and per-minute jobs mix freely in
    one config; concurrencyPolicy still governs overlap as before.

  • Catch-up for overrun seconds, bounded. In sub-minute mode, if a pass runs
    long -- many simultaneous launches, or the once-a-minute config reload -- and
    the clock crosses one or more whole seconds before the next pass, the skipped
    seconds are serviced after the fact, so a second-level job due in the gap still
    fires (once) rather than being silently dropped. The replay is bounded by a
    ten-second CATCHUP_LIMIT: a larger gap is treated as a stall, suspend, or
    clock jump and resumed past with a warning, never replayed as a burst of
    backdated launches (matching cron's no-catch-up-after-an-outage behavior).
    Minute-level jobs need no catch-up: their minute-truncated slot already
    absorbs any sub-minute overrun.

  • No spurious run at a mid-period restart. On startup the de-dup map is
    seeded with the in-progress slot for every scheduled job, so a job whose
    minute (or second) is already under way does not fire immediately on the first
    tick; it first fires at the next matching boundary, exactly as in minute-only
    mode. Without this, merely having any second-level job present would have made
    every minute-level job fire about a second after a mid-minute restart.
    @reboot jobs are unaffected and still fire once at startup.

  • Concurrent launches within a slot. When several jobs are due in the same
    slot, spawn_jobs now launches them concurrently instead of one at a time.
    With N jobs sharing a slot the old serial form cost N times a subprocess spawn
    -- the dominant source of same-second overrun -- which now collapses to about
    a single spawn. The single-job case (the norm) still takes a direct await and
    is byte-identical to before, and the de-dup and cluster-gate decisions are
    still made sequentially, so only the per-job "Starting"/"spawned" log lines may
    now interleave.

  • Config reload moved off the event loop. The once-a-minute reload now runs
    its disk read and full reparse in a worker thread (reload_config), so a slow
    parse no longer freezes the event loop -- web API, cluster gossip, job-output
    pumping -- for its whole duration. The parsed job set is still applied on the
    loop thread and before jobs are serviced, so the cluster leader-gate is
    always current for the tick. Housekeeping (config reload, cluster and web
    (re)start, logging config) is gated to run at most once per wall-clock minute
    even while the loop ticks per second; in pure minute-tick mode it runs every
    iteration, exactly as before.

The year schedule key is now honored

  • year restricts the schedule to specific years. Earlier releases accepted
    a year key on the schedule object but built only a five-field crontab string
    from it, silently dropping year so it had no effect -- a job with an
    object-form year ran every year. It is now emitted as parse-crontab's
    trailing year column and honored, so year: "2017" really does pin the
    schedule to 2017. (String schedules were always passed to parse-crontab
    verbatim, so a six-field string already honored its year; only the object form
    changes.) This is a behavior change -- see the upgrade notes below.

  • Honoring year changes that job's job-set fingerprint, so during a rolling
    upgrade of a cluster the old and new binaries compute different job_set_ids
    for the identical config and will not treat each other as agreed peers until
    every node is upgraded -- the same transient, self-healing drift as any config
    rollout, and leader election stays at-most-once throughout. Jobs that do not
    use object-form year are unaffected: their fingerprint is byte-for-byte
    identical to before.

Schedule parsing, errors, and fingerprints

  • A malformed schedule now fails the reload with a named error.
    parse-crontab's ValueError on a bad field (an out-of-range value, the wrong
    field count) is caught and re-raised as ConfigError("invalid schedule '...': ..."), naming the offending expression, so a bad schedule fails config
    load or reload cleanly with a message the reload loop can log, rather than
    surfacing as an anonymous traceback.

  • One object-to-crontab builder, shared everywhere. A single
    schedule_object_to_crontab helper now renders the object form to a crontab
    line -- five fields normally, six or seven when year/second are used -- and
    is shared by parsing, the fingerprint, and the dashboard's schedule label, so
    those three can never disagree on the mapping. The object form still collapses
    to the exact five-field line as before when neither second nor year is set,
    so its fingerprint is unchanged. Whether a schedule counts as second-level is
    derived from the actual rendered field count (seven), not mere key presence,
    so a blank second: value that renders an empty column does not force the
    whole scheduler onto the per-second cadence.

Web dashboard

  • Cron parsing, description, and preview understand five-, six-, and
    seven-field expressions.
    The client-side cron engine normalizes any of the
    three widths (implicit second 0 and any year for five fields, a trailing year
    for six, a leading second for seven), computes next-fire times at second
    resolution with year restriction (and parse-crontab's 2099 year ceiling), and
    renders wall-clock times with a seconds component where the schedule has one.
    Plain-English descriptions gain "Every second", "Every N seconds", "At
    second(s) ...", and an "in {year}" clause -- and deliberately do not lead
    with a per-second cadence phrase when a coarser field is restricted, so a
    schedule like * 30 * * * * * is not described as firing every second.

  • Cron sandbox covers the new widths. The palette's schedule sandbox
    validates 5-, 6-, and 7-field expressions (its error copy and field-breakout
    labels updated to match, labelling the leading second and trailing year
    columns correctly), and its next-fire preview shows seconds.

  • Clicking the wordmark spins the logo. The "yacron2" wordmark now triggers
    the same mark-spin animation as clicking the mark glyph.

Examples and documentation

  • example/pulse-monitor -- a small, runnable real-time uptime / SLA monitor
    built entirely on second-level scheduling: it probes a latency-critical service
    every few seconds, heartbeats every ten, and rolls up a summary once a minute
    (which still fires exactly once per minute alongside the per-second probes). It
    watches yacron2's own /status endpoint, so docker compose -f docker-compose-pulse.yml up needs nothing else running.

  • example/pulse-cluster -- the clustered sibling: a three-node,
    mutual-TLS, leader-electing cluster that splits ...

Read more

1.2.2

Choose a tag to compare

@github-actions github-actions released this 02 Jul 22:47
Immutable release. Only release title and notes can be modified.
  • New webhook reporter: native Slack/Discord/Teams/ntfy notifications. A
    fourth reporter joins sentry/mail/shell in every report block: webhook
    sends an HTTP request (POST by default) to a configured URL with a
    jinja2-templated body. The default body is a {"text": ...} JSON payload
    carrying the same subject-plus-body text as the default mail/sentry
    templates, JSON-encoded with jinja2's tojson filter so quotes, newlines,
    and non-ASCII job output always produce valid JSON -- point url at a
    Slack, Mattermost, or Teams incoming webhook and it works with no further
    configuration. method, contentType, headers, body, and timeout
    cover everything else (Discord's {"content": ...} shape, ntfy's
    plain-text body and header-driven priority, or your own endpoint). The URL
    resolves like the sentry DSN (value / fromFile / fromEnvVar) and is
    treated as a secret throughout: it is never logged, and the job-set
    fingerprint redacts the inline URL value and all header values (which
    commonly carry Authorization tokens). No new dependency -- outbound
    delivery rides the core aiohttp. Note: because every job's effective
    config gains the new default block, job-set ids change on upgrade;
    replicas must be on the same version to compare ids, as before.

  • Unchanged peers now answer gossip polls with a bodyless 304. Every
    /peer response carries a strong ETag (a content hash of the payload),
    and each polling node echoes the tag of the last full body a peer served
    it back as If-None-Match, so a peer whose state has not changed since
    then skips re-sending the full O(members + jobs) JSON: a converged, idle
    cluster's steady-state round costs headers rather than bodies. This is a
    transport optimization, not a protocol change. A 304 is still a fresh,
    mutually-authenticated round trip, and because the tag is content-derived,
    a match proves the peer's payload is exactly the one the poller already
    holds, so the poller replays its cached observation and every gate (mutual
    agreement, conflict detection, the cluster.driftAfter debounce) advances
    exactly as if the identical body had been re-sent. The one live field, a
    job's seconds-to-next-fire countdown, is hashed as the absolute next-fire
    time instead, so the tag stays stable between fires and rolls exactly when
    a schedule fires. Mixed fleets degrade safely during a rolling upgrade: an
    older peer ignores If-None-Match and keeps serving full bodies, a
    tagless response stops the poller from sending the header at all, an
    unsolicited 304 is recorded as a failed poll, and an over-long or
    non-printable tag is never stored or echoed.

  • Fleet-view countdowns are aged, not frozen. With 304 rounds
    refreshing a peer's liveness without re-shipping its job summaries, a
    stored snapshot can now legitimately outlive many polling rounds, so
    GET /fleet re-derives each peer job's advertised scheduled_in
    countdown from the snapshot's age (an elapsed duration measured on the
    local clock alone, so peer clock offsets never leak in) instead of serving
    the value the snapshot arrived with, clamping at zero. The fire itself
    rolls the peer's ETag, so the next poll ships a full body carrying the
    real successor value.

  • /peer bodies that do go out are gzip-compressed once they reach
    1 KiB (below that, the per-request CPU spend outweighs the few bytes
    saved). The polling side already advertised gzip support, and the existing
    response-size cap applies to the decompressed payload, so compression
    does not weaken it.

  • Dashboard: the version and job-set id header chips copy their value on
    click.
    Header text is chrome and is no longer text-selectable; the two
    values worth grabbing hand themselves out instead. Clicking the version
    chip copies the version, and clicking the job-set chip copies the full
    job-set id even though the header shows only a short prefix; both
    tooltips say so. The command palette carries the same two copies, so both
    values stay reachable from the keyboard.

  • Dashboard: the quick "power-on sweep" flash between boots is gone. The
    full POST boot screen still replays once its cooldown elapses, but the
    visits in between now start the app directly instead of playing a
    full-screen power-on animation first.

  • Internal: the conditional exchange ships with a matching batch of cluster
    tests (tag stability across countdown ticks and rollover on a fire, the
    304 replay path, unsolicited-304 and unusable-tag rejection, countdown
    aging, and an end-to-end mutual-TLS 304-plus-gzip round), and the wiki's
    Architecture and Internals page documents the exchange.

Full Changelog: 1.2.1...1.2.2

1.2.1

Choose a tag to compare

@github-actions github-actions released this 02 Jul 17:58
Immutable release. Only release title and notes can be modified.

This is the largest yacron2 release since the fork. Its headline feature is
clustering: several replicas can now verify they hold the same job set, elect
a leader so each scheduled job runs on one node instead of every node, spread
jobs across the fleet, and fail over when a node dies, coordinating either
peer-to-peer over mutual TLS or through a Kubernetes or etcd lease. The
release also adds native Prometheus metrics, accepts classic crontab files as
configuration, and grows the web dashboard into a small operations console.
Clustering is entirely opt-in; see the upgrade notes below for the few
behavior changes that apply to existing deployments.

Clustering and leader election

  • New optional top-level cluster: section. Give every replica the same
    static peer list and a dedicated mutual-TLS listener (cluster.listen,
    cluster.tls.{ca,cert,key}, cluster.peers), and each node polls every
    peer once per round (cluster.interval, default 30 seconds), comparing
    job-set ids (see 1.1.8) to attest that all replicas hold an identical job
    set. Peers are reported as agreed, syncing, drifted (a mismatch that
    persists for cluster.driftAfter consecutive rounds, default 3),
    unreachable, untrusted (TLS verification failed), or conflict. On its
    own this is observe-only (every node still runs every job), which makes it
    a safe first rollout step.
  • Leader election (cluster.electLeader: true). The leader is the lowest
    cluster.nodeName (default: the hostname) among the agreeing nodes this
    node can see, and only when they form a strict majority of the declared
    cluster size; below quorum a node stands down and runs nothing. Two
    disjoint majorities cannot exist, so a clean partition produces at most one
    leader within about one polling interval. Conflicts fail closed: a
    duplicate nodeName, a cluster-size disagreement (say, a rolling resize
    from 3 to 5 nodes), or a coordination-policy divergence parks Leader jobs
    until the fleet reconverges, and each is logged loudly and shown on the
    dashboard. A freshly started or reconfigured node holds its jobs until it
    has polled every configured peer at least once, so a blank-view node cannot
    elect itself. Two-node election is refused at config load (it is strictly
    worse than a single replica); even cluster sizes log a warning.
  • A per-job clusterPolicy (also settable under defaults:) decides
    what election means for each job: Leader (the default: only the elected
    leader runs it, and it is skipped when in doubt), PreferLeader (never
    skip: the lowest reachable agreeing node runs it even without quorum,
    accepting a rare double-run, so reserve it for idempotent jobs), and
    EveryNode (all nodes run it, for node-local housekeeping). Manual runs
    via POST /jobs/{name}/start are never gated. Automatic retries re-check
    the gate before every relaunch and abandon a pending retry when ownership
    has demonstrably moved to another node; @reboot jobs under election are
    deferred until the cluster converges and then run once, on the owner.
  • Load-balanced jobs: cluster.distribution: spread. Instead of one
    leader running every Leader/PreferLeader job, each such job is
    assigned an owning node by rendezvous (highest-random-weight) hashing over
    the quorate agreeing members, spreading work across the fleet. A
    membership change moves only the departing or joining node's share of
    jobs. The same quorum gating applies, and GET /jobs reports each job's
    current clusterOwner.
  • Mutual TLS is the entire trust boundary. A peer must present a
    certificate chaining to the configured CA and matching the host it was
    reached at, so the CA should be dedicated to yacron2 nodes and nothing
    else. Certificates rotated in place are detected and reloaded without a
    restart. The peer endpoint is served only on the cluster listener, never
    on the web API listener.
  • The failure semantics are documented, not hand-waved. The built-in
    backend is best-effort by design: there are narrow, self-healing windows
    where a Leader job can be skipped or a PreferLeader job can
    double-run, and the new wiki page enumerates them. When you need fenced
    exactly-once execution, use one of the lease backends below.
  • yacron2 --validate-config validates the entire cluster section (peer
    list, TLS material, lease timing invariants) without starting anything.

Kubernetes and etcd lease backends

  • cluster.backend: gossip | kubernetes | etcd picks the coordination
    mechanism (default gossip, the peer-to-peer mode above). The lease
    backends replace the peer quorum with a fenced, expiring lease in an
    external store: exactly one node holds the lease at a time, so Leader
    jobs are exactly-once while the store is reachable. With a lease backend,
    election is always on, there is no peer list or cluster mTLS to manage,
    and distribution: spread is rejected at config load (a single lease
    cannot express per-job ownership).
  • Kubernetes (cluster.kubernetes.*): replicas campaign for a
    coordination.k8s.io/v1 Lease object using the client-go leader-election
    algorithm (leaseName defaults to yacron2-leader;
    leaseDurationSeconds/renewDeadlineSeconds/retryPeriodSeconds default
    to 15/10/2, with the client-go timing invariants enforced at config load).
    In-cluster ServiceAccount token, CA, namespace, and API server are
    detected automatically; a kubeconfig and an explicit apiServer (https
    only) are supported. The backend talks to the API server over the bundled
    aiohttp by default; installing the new optional extra
    (pip install yacron2[kubernetes]) switches to the official Kubernetes
    client (clientLibrary: auto|http|library). The stored holder identity
    embeds a per-process token, so two pods that accidentally share a name
    cannot both hold the lease. example/kubernetes/ ships a ready-to-apply
    Deployment with the minimal ServiceAccount, Role, and RoleBinding.
  • etcd (cluster.etcd.*): replicas campaign for a lease-bound key
    (electionName defaults to yacron2/leader) through etcd's v3 JSON/HTTP
    gRPC gateway, using a create-if-absent transaction fenced by the lease id
    (ttl defaults to 15 seconds, minimum 3). Multiple endpoints fail over
    in order; optional username/password (literal, fromFile, or
    fromEnvVar) and client TLS are supported, and credentials are refused
    unless every endpoint is https://. example/etcd/ ships a compose demo
    with an etcd and two yacron2 replicas.
  • Failover is fast and clock-safe on both. All fencing runs on the
    monotonic clock with a one-second skew margin (no wall-clock or cross-node
    clock-sync assumptions): a holder whose renewals stall demotes itself
    locally before the store-side lease can expire, without a network call. A
    graceful shutdown releases the lease explicitly (Kubernetes clears
    holderIdentity, etcd revokes its lease) so a survivor takes over
    immediately rather than waiting out the TTL. If the store becomes
    unreachable, Leader jobs fail closed and PreferLeader jobs keep
    running. @reboot bookkeeping is persisted in the store, scoped to the
    job-set id, so a Leader @reboot job runs once per job configuration
    rather than once per fleet restart.
  • No new runtime dependencies. Both lease backends are plain HTTP over
    the existing aiohttp core (no etcd or gRPC client; the Kubernetes client
    is optional), so all packaged architectures keep working.

Prometheus metrics

  • Native Prometheus metrics at GET /metrics, served by the existing
    web listener whenever the web API is enabled; there is no exporter
    sidecar and no new dependency (the exposition is generated in-process).
    Both the classic text format and OpenMetrics 1.0 are served, negotiated
    via the Accept header.
  • Per-job series cover run outcomes (yacron2_job_runs_total labeled by
    job_name and status), retries, permanent failures, failures to start,
    a duration histogram with configurable buckets
    (web.metrics.durationBuckets), last success/failure/run timestamps and
    exit code, and live state (enabled, running, next run). Daemon series
    cover the version, start time, job-set id, job counts, and config-reload
    health. Cluster series cover size, quorum, leadership, per-peer status
    counts, conflicts, and leader/quorum transition counters; the wiki
    suggests alerting on sum(yacron2_cluster_is_leader) > 1 and on losing
    yacron2_cluster_quorate. Metrics are recorded at the same point as the
    run history, so /metrics and /jobs/{name}/runs never disagree.
  • web.authToken, when configured, protects /metrics like every other
    endpoint; web.metrics.public: true exempts just this endpoint for a
    scraper, and web.metrics: false removes it entirely.

Classic crontab files

  • Classic (Vixie) crontabs are now accepted as configuration. A file
    with a .crontab or .cron extension, or named exactly crontab, is
    parsed as a crontab wherever configuration is loaded: passed to -c,
    dropped into a config directory alongside *.yaml files, or pulled in
    via include:; a neutral-named file given to -c or include: is
    content-sniffed. Supported syntax: five-field entries (the same field
    dialect as YAML schedule strings), @keywords (including @reboot and
    @midnight), position-sensitive VAR=value environment lines, comments,
    and the \% escape. SHELL and CRON_TZ assignments are honored as the
    job's shell and timezone.
  • Each entry becomes a normal yacron2 job named <file>:<line>,
    indistinguishable downstream: it shows up on the dashboard and HTTP API,
    participates in the job-set id and clustering, and can be run or
    cancelled on demand. ...
Read more

1.1.11

Choose a tag to compare

@github-actions github-actions released this 29 Jun 18:22
Immutable release. Only release title and notes can be modified.
  • Coverage is now published to Codecov.
    Every CI matrix cell uploads its own coverage.xml under an
    <os>-py<version> flag, and Codecov merges them into one combined number, so
    POSIX-only paths that Windows skips (privilege drop, user/group
    resolution) still count toward the published total instead of dragging it down
    to the lowest single row. tox now also writes the report it consumes
    (pytest --cov-report=xml). The hard pass/fail gate stays with tox's
    --cov-fail-under=82 (see 1.1.10): Codecov's own project and patch status
    checks are configured as informational only, so they annotate pull requests
    without ever blocking them. The upload runs even on failed or cancelled jobs
    and keeps fail_ci_if_error: false, so a Codecov outage never reds the build,
    and flag carryforward keeps the combined number stable when a matrix row is
    skipped on a given run. The README gains a matching coverage badge.

Full Changelog: 1.1.10...1.1.11

1.1.10

Choose a tag to compare

@github-actions github-actions released this 24 Jun 04:57
Immutable release. Only release title and notes can be modified.
  • Numeric user/group is read as a uid/gid, not a login name. In the
    config schema the user/group type was a Str() | Int() union, and
    strictyaml matched the always-accepting Str() first, so user: 1000
    arrived as the string "1000" and was looked up as a login name
    (getpwnam("1000")) rather than used as uid 1000. The union is now
    Int() | Str(), so a bare number is treated as the uid/gid it looks like; a
    non-numeric name (user: www-data) still falls through to Str(). (POSIX
    only; per-job user/group remains rejected with a configuration error on
    Windows.)

  • More resilient container builds. Every image build, across the default
    Debian image and all seven distro variants (-alpine, -ubuntu, -rhel,
    -fedora, -opensuse, -amazonlinux, -distroless), now wraps its
    package-manager and pip network steps in a retry-with-backoff helper,
    alongside each manager's native knobs (apt's Acquire::Retries, dnf's
    --setopt=retries, an explicit zypper refresh retry, and a longer
    pip --timeout), so a transient mirror or package-index hiccup retries
    instead of failing the whole build. The build and test CI workflows get the
    same hardening via PIP_RETRIES/PIP_TIMEOUT, with build.yml forwarding
    them into its emulated cross-architecture binary builds via docker run -e.

  • The -distroless image now builds for amd64/arm64 only. The
    gcr.io/distroless/python3-debian12 base publishes no ppc64le or s390x
    manifest, so requesting those arches aborted the distroless release with
    "no match for platform in manifest". The RPM-based variants (-rhel,
    -fedora, -opensuse) still cover the wider arch set.

  • The README status badges also gain brand new colors
    (and logos on the PyPI/Python badges). yay

  • Internal: branch coverage is now measured and gated in CI (tox runs
    pytest --cov-fail-under=82), backed by substantially expanded unit tests for
    config and user/group validation, config reload and graceful shutdown, the
    job runner, and the job-set-id fingerprint.

Full Changelog: 1.1.9...1.1.10

1.1.9

Choose a tag to compare

@github-actions github-actions released this 24 Jun 02:28
Immutable release. Only release title and notes can be modified.
  • More prebuilt container images. Alongside the default Debian-based image,
    every release now also publishes the same build on seven more bases, each
    tagged with a -<distro> suffix: -alpine, -ubuntu, -rhel (Red Hat
    UBI 9), -fedora, -opensuse (Leap), -amazonlinux (2023) and
    -distroless, plus an explicit -debian alias for the default. Pick the base
    that matches your host userland or image-provenance policy; behaviour is
    identical, since yacron2 is a pure-Python app (Python >= 3.10) and each image
    uses its distro's native interpreter. The Debian image still owns the bare
    latest/<version> tags and the widest architecture coverage. See
    Distro variants.

Full Changelog: 1.1.8...1.1.9