Skip to content

django-ox 1.8.0

Choose a tag to compare

@PhiLily PhiLily released this 04 Oct 17:33
· 14 commits to main since this release
75f7b0f

django-ox 1.8.0

No database migration is required. Upgrade workers so invalid stored schedules cannot stop other schedules or queued tasks. This release fixes schedule failures affecting 1.2.0 through 1.7.0. See GHSA-gq8r-4ccg-vxcr.

What changed

  • Fixed worker termination from an uncaught interval tick overflow (CWE-248). An interval above 62,135,596,800 seconds, combined with certain phases, ended every worker reading it on its first dispatch pass and after each restart. Queued tasks did not run, and other schedules did not fire. Stored rows can trigger this on SQLite through creation or update APIs, admin add or change access, or direct table writes. PostgreSQL, MySQL and MariaDB columns reject intervals this large. A person who can edit SCHEDULES can trigger the settings path on any backend, before database access. The overflow was reproduced on SQLite. The other database conclusions for this interval value come from code inspection.
  • Fixed worker startup and dispatch failures from stored schedule bounds outside the supported date range. On PostgreSQL, an out-of-range bound could prevent every worker from starting and stop every dispatch pass of a running worker. Other schedules were left unprocessed, but tasks already queued still ran. This was reproduced on every release from 1.2.0 through 1.7.0 with Django 6.0.8 and Python 3.12, not each release's own support matrix. The runs used an end time late on 9999-12-31 in a zone west of UTC, stored in year 10000 UTC.
  • Writers now reject schedule bounds outside years 1 to 9999 in the database's time zone. They reject times with a time zone when USE_TZ is off. Every write path limits every_seconds to 62,135,596,800. Oversized SCHEDULES intervals are refused by manage.py check and at worker start with django_ox.E002.
  • Workers skip and log unreadable or invalid stored rows instead of letting one row stop the worker. This includes impossible dates, out-of-range bounds, PostgreSQL infinity or BC dates, MySQL zero dates and invalid stored intervals. Other schedules and queued tasks continue to run. Tick calculation and bound comparison failures are reported per schedule as schedule_dispatch_error for any schedule source. Database outages still stop the pass.
  • Added the public, stable django_ox.stored.create_schedules(rows, *, user=None) API. Pass a list of mappings accepted by create_schedule. Creation is all-or-nothing and returns a list of OxSchedule instances, or [] for an empty batch. Validation errors identify each row, name, field and message. Permission checks follow validation. A concurrent name conflict raises IntegrityError and rolls back the batch. Database errors are annotated with the row concerned.
  • Corrected the beat importer to use Celery's crontab grammar, interval duration calculation and argument decoding. It compares timezone data rather than two sample offsets. It lists rows that cannot be translated instead of printing calls for them. Diagnostics name rows that can stop beat's whole-table scheduling pass and bound the size of quoted values.
  • The importer now prints one all-or-nothing created = create_schedules([...]) call. Its SCHEDULABLE_TASKS fragment includes only entries used by printed rows. The new --beat-timezone ZONE option supplies beat's timezone when the crontab table has no timezone column or DJANGO_CELERY_BEAT_TZ_AWARE=False.
  • Fixed MySQL lease renewal when callers share a Worker and one calls run_once() inside its own atomic block on the worker's database. In 1.7.0, renewal could wait on that call's uncommitted row and block renewal for every other task on the Worker. Other tasks could lose their leases, be requeued and run twice. Renewal now excludes the uncommitted claim, which the caller's transaction protects until commit publishes its outcome.
  • With psycopg_pool 3.3.0 or later, PostgreSQL pool recovery no longer runs statements on idle connections. In django-ox 1.5.0 through 1.7.0, the recovery sweep could wait indefinitely for an idle connection that stayed open without answering. Recovery now drains the pool only when the driver reports a connection as lost. Idle connections are closed without testing them. Checked-out connections are closed and replaced when returned. A failed attempt with no reported connection loss leaves the pool alone.

Compatibility

Existing schedules

On SQLite, look for interval schedules with every_seconds above 62,135,596,800. Check stored start and end times against years 1 to 9999 in the database's time zone. Check SCHEDULES entries too.

Correct readable invalid rows in the admin change form or with update_schedule, pause them, or delete them. update_schedule(row, enabled=False) with no other changes and the admin Disable action now work for readable rows that fail validation. Pausing does not repair a row. Enabling it again is refused until it is corrected.

The unreadable PostgreSQL year-10000 row is different. The admin changelist returns HTTP 500 and lists no rows, including healthy ones. The change form and Disable action also return HTTP 500. update_schedule(row, enabled=False) raises DataError. Delete this row with delete_schedule(OxSchedule(pk=schedule_pk)) or a plain SQL DELETE.

On 1.7.0 with SQLite, disabling the triggering interval row through the admin Disable action or update_schedule(row, enabled=False) is a working workaround. A worker never builds a disabled row. This workaround does not apply to the unreadable PostgreSQL row.

The interval ceiling also rejects previously harmless schedules, including oversized intervals with phase 0. Phase and starting deadline values are limited to 86,399,999,999,999 seconds. A SCHEDULES every or phase outside Python's timedelta range now produces django_ox.E002 instead of OverflowError. "Run once now" reports an invalid stored row as not runnable instead of enqueueing it.

Integer validation now uses the range of the database that stores schedules. Several invalid numeric, empty and mixed-timezone inputs now raise ValidationError instead of TypeError. Callers handling those errors should update their exception handling. See the CHANGELOG for the affected inputs and unchanged early validation in update_schedule.

Beat imports

If you applied importer output from 1.2.0 through 1.7.0, check schedules restricting both day fields and crontabs from a zone other than TIME_ZONE. Those versions passed crontab fields through as stored and judged zones by two sample offsets. Also check beat rows with microsecond intervals, which were listed as below one second. Regenerate output with 1.8.0 to see these rows by name.

Check every named omission and every entry under "Translated, with a difference from beat" before applying output. Celery requires both restricted day fields to match. Stored schedules run when either matches. Rows that still restrict both fields are listed rather than translated.

Every interval is listed as a behavior difference. Stored schedules count from a fixed instant; Celery counts from the last run. With USE_TZ on, stored schedules run matching repeated clock times on both passes, while beat runs them on the first only. Clock-change notices use the actual tick pattern and scan ten years from import.

Past start times are dropped. Rows with future starts are listed instead of translated. Beat runs once when the start arrives; a stored schedule waits for its next tick. Arrange those starts separately before switching schedulers.

Run the command with beat's Python environment, timezone data and Django settings. The command cannot verify that they match. Resolve omitted rows before switching. Apply the batch only after checking the output. Keep SCHEDULE_SOURCE in the same OPTIONS; without it, no stored schedule ever runs. Applying output remains subject to validation, permissions, concurrent changes and database errors.

MySQL renewal

Affected shared-Worker deployments should upgrade. PostgreSQL, pooled or unpooled, two plain run_once() calls outside atomic blocks, and two separate Workers were unaffected by this defect.

SQLite's single write-lock limit is unchanged. On MySQL, callers using autocommit off without an atomic block must still use an unshared Worker. Tasks inside a caller's atomic block must not commit through raw transaction-control SQL or implicitly committing DDL. Those operations expose a claim whose lease is not renewed.

PostgreSQL pools

The drain-based stop-hang fix requires psycopg_pool 3.3.0 or later. Version 3.2.x retains the legacy sweep and is explicitly excluded from that fix. It still tests idle connections and can wait indefinitely for a silent connection. When draining is unavailable, connection_pool_cannot_drain warns that restart recovery is preserved but the drain-based fix is not in effect.

This change adds no shutdown deadline. Stopping can still wait for the pool's checkout timeout and the stop-time recovery look. Lowering the pool's timeout shortens that checkout wait. Statements in the poll loop, renewal, outcome writes and task-thread connection probes can still wait without limit.

In repeated tests of the real manage.py ox_worker, every connection was made silent and SIGTERM was sent one second later. With psycopg_pool 3.3.3, the fixed worker exited 0 after 34.4 seconds with the default 30-second pool timeout, and after 6.4 seconds with a 2-second timeout. django-ox 1.7.0 and the fixed worker on psycopg_pool 3.2.8 were still running after 40 seconds. Both needed a second SIGTERM and exited 130. These are observations, not shutdown bounds. Tests ran on one shared machine through a local relay that acknowledged TCP traffic, not a network blackhole.

Draining replaces healthy idle connections too. It can open more connections and take longer to resume work than the old sweep. In the pool-of-10 session-termination test with four tasks in flight, 1.8.0 opened 31 connections over the test, compared with 9 on 1.7.0. All four outcomes were written once on both versions. The additional opens do not increase the configured pool size or change the worker's connection budget.

psycopg_pool 3.3.3 or newer is recommended. Separately from draining, versions before 3.3.1 can lose pooled connections when task timeouts interrupt checkout. That issue also affects django-ox 1.7.0. Version 3.3.3 also fixes pool maintenance threads terminating after 24 hours without work.

The supported release line is now 1.8.x. Version 1.7.x is no longer supported.