Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.9.1] - 2026-10-17

**Upgrading:** No database migration is required.

### Changed

- `worker_connection_hint` is now a WARNING on PostgreSQL. A worker with
`CONN_MAX_AGE = 0` and no `OPTIONS["pool"]` opens a new connection for every
task, about 5 ms each on a local server, and logs one line at startup saying
so. To remove it, set `CONN_MAX_AGE = 600` or configure Django's pool and
leave `CONN_MAX_AGE` at 0. MySQL keeps the INFO line, and SQLite logs
nothing. "Retaining worker connections" in `docs/production.md`
lists both settings and the pool size to use.
- The benchmarks page now reports django-ox 1.9.0 against django-tasks-db 0.13.0
at the same connection settings.

### Fixed

- A worker whose database refuses the connection for an outcome write, as a
host out of ephemeral ports does under `CONN_MAX_AGE=0`, now tries the write
again after waits of 50 ms doubling to 1.6 s, 3.15 s in all, and the waits
total at most a tenth of `LOCK_TIMEOUT`. A worker told to stop ends a wait at
once.

## [1.9.0] - 2026-10-12

**Upgrading:** No database migration is required.
Expand Down Expand Up @@ -2418,6 +2442,7 @@ Initial release.
the public API surface, the pre-1.0 SemVer rule, the deprecation
window, and the supported Python and Django matrix.

[1.9.1]: https://github.com/oxpull/django-ox/compare/v1.9.0...v1.9.1
[1.9.0]: https://github.com/oxpull/django-ox/compare/v1.8.0...v1.9.0
[1.7.0]: https://github.com/oxpull/django-ox/compare/v1.6.0...v1.7.0
[1.8.0]: https://github.com/oxpull/django-ox/compare/v1.7.0...v1.8.0
Expand Down
45 changes: 24 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Django tasks in your database

Stop running Redis to send an email. django-ox runs Django's Tasks framework in your existing database, where a dead worker doesn't mean lost tasks. Run a worker process, not a separate broker.
Stop running Redis to send an email. django-ox runs Django's Tasks framework in your existing database. When a worker dies, another worker picks up its tasks. You run one process: the worker.

Supports Django 5.2 LTS through the `django-tasks` backport, and Django 6.0 / 6.1 through core `django.tasks`. Requires Python 3.12+. The optional `oxpull` app provides task-coordination features and requires Django 6.0+.

Expand Down Expand Up @@ -56,7 +56,9 @@ Inspect attempts and retry or discard tasks in Django admin. Open **Queue overvi

For fleet alerting, `ox_health` turns queue thresholds into an exit code. For local loop-liveness probes, pair `ox_worker --heartbeat-file PATH` with `ox_health --heartbeat-file PATH`, using the same process count. Both health modes support `--format json`. Monitor through `django_ox.stats` or `/ox/metrics`, and clear finished rows with `ox_prune`. Use `--database` on `ox_worker`, `ox_prune` and the database mode of `ox_health` to select the database alias.

In the [published benchmarks](https://oxpull.com/django-ox/benchmarks/), 2,000 of 2,000 tasks finished after 20 worker kills per trial, queue drain was up to 20% faster than django-tasks-db, and enqueueing 10,000 tasks took 0.71 s.
With pooled or persistent connections, django-ox delivered 1.4 to 1.7× django-tasks-db's throughput at matched Django connection settings. See the [published benchmarks](https://oxpull.com/django-ox/benchmarks/).

<sub>PostgreSQL 16; no-op tasks; django-ox 1.9.0 vs django-tasks-db 0.13.0; medians of five runs.</sub>

CI covers Python 3.12 to 3.14, Django 5.2 to 6.1, PostgreSQL, MySQL and SQLite. Open source under the BSD 3-Clause licence.

Expand All @@ -70,7 +72,7 @@ Documentation: <https://oxpull.com/django-ox/>

Add `"oxpull"` to `INSTALLED_APPS`, run migrations, and use `"oxpull.backend.OxpullBackend"` for the task aliases that need its features. No separate package installation is needed.

Unique tasks deduplicate pending tasks at enqueue time. Batches also need a reconciler schedule. Rate limits need `OPTIONS["RATE_LIMITS"]` and `"oxpull.worker.OxpullWorker"` as the alias's `OPTIONS["WORKER_CLASS"]`. Workflows need `OPTIONS["WORKFLOWS"] = True`, `OxpullWorker` and a reconciler schedule. Workflows are created at runtime, not declared as workflow definitions in settings.
Unique tasks deduplicate pending tasks at enqueue time. Batches also need a reconciler schedule. Rate limits need `OPTIONS["RATE_LIMITS"]` and `"oxpull.worker.OxpullWorker"` as the alias's `OPTIONS["WORKER_CLASS"]`. Workflows need `OPTIONS["WORKFLOWS"] = True`, `OxpullWorker` and a reconciler schedule. Workflows are created at runtime.

See the [feature setup](https://oxpull.com/django-ox/features/) and, for an existing installation, [Earlier `oxpull` installs](https://oxpull.com/django-ox/migrating/#earlier-oxpull-installs).

Expand Down Expand Up @@ -105,7 +107,7 @@ limit, keeping the full traceback of every attempt.
[The benchmarks page](https://oxpull.com/django-ox/benchmarks/) compares
django-ox 1.3.0 with `django-tasks-db` 0.13.0, another database backend
for the Tasks framework, in runs dated 2026-09-19. Both use Django core
`django.tasks`, PostgreSQL 16 in Docker on an Apple M1 Max, no-op task
`django.tasks`, PostgreSQL 16 in Docker, no-op task
bodies and package defaults except `LOCK_TIMEOUT` of 15 seconds for the
kill test. It covers backlog drain at two queue depths with one and four
workers per backend, bulk enqueue of 10,000 rows in one transaction,
Expand Down Expand Up @@ -255,10 +257,10 @@ This mode checks file metadata without querying the database.
| `--processes N` | `1` | Expected worker process count. At 1, check `PATH`; above 1, require `PATH.supervisor` and every slot file from `PATH.0` through `PATH.(N-1)` to pass. Requires `--heartbeat-file`. |

A passing file probe means the expected controlling loops have advanced
recently, not that tasks are progressing. A hung database can make every
file stale and trigger fleet-wide liveness restarts. Use an existing,
writable directory private to the container, allow for startup and slot
replacement, and keep project Django startup database-free. See the
recently. It does not mean that tasks are progressing. A hung database can
make every file stale and trigger fleet-wide liveness restarts. Use an
existing, writable directory private to the container. Allow for startup and
slot replacement, and keep project Django startup database-free. See the
[probe guidance](https://oxpull.com/django-ox/monitoring/#health-checks-ox_health)
before enabling automatic restarts.

Expand All @@ -274,11 +276,12 @@ actions. The same operations are `django_ox.actions.retry(result_id)` and
FAILED or LOST task; a discard closes a READY, WAITING, FAILED or LOST task
without running it. Neither touches a running task.

Open **Queue overview** from the task change list to compare retained status
counts, eligible READY tasks, the oldest eligible task's age, five-minute
throughput and failure rate, and time since the last claim. READY includes
deferred tasks; Eligible ready excludes them. Status totals are retained
rows, not lifetime counts.
Open **Queue overview** from the task change list. It shows retained status
counts, eligible READY tasks and the oldest eligible task's age. It also
shows five-minute throughput and failure rate, and time since the last
claim. READY includes
deferred tasks; Eligible ready excludes them. Status totals count the rows
still in the table.

Each visit scans retained task rows, so cost grows with retention. The page
does not refresh automatically. It uses the database alias selected by
Expand Down Expand Up @@ -349,16 +352,16 @@ failure is logged as `schedule_dispatch_error`. After rollback, the worker
continues to later schedules if the same database connection remains usable.

Missed ticks: if every worker was down when a tick passed, the latest
missed tick fires once on recovery and older ones are skipped, so a
nightly job still runs after an unlucky deploy window but a backlog never
stampedes. A newly deployed schedule waits for its next tick rather than
firing for a time before it existed.
missed tick fires once on recovery. Older missed ticks are skipped. A
nightly job still runs after an unlucky deploy window, and a backlog never
stampedes. A newly deployed schedule waits for its next tick. It does not
fire for a time before it existed.

Schedules can also live in the database and be edited in the Django admin
without a deploy, for the cases where whoever needs to pause a job cannot
ship one. A row names a task the code has exposed rather than an import
path, so admin access does not become permission to run anything. See
[Schedules in the database](https://oxpull.com/django-ox/stored-schedules/).
without a deploy. That is for the cases where whoever needs to pause a job
cannot ship a deploy. A row names a task the code has exposed. Import paths
are not accepted, so admin access does not become permission to run anything.
See [Schedules in the database](https://oxpull.com/django-ox/stored-schedules/).

## Behavior details

Expand Down
12 changes: 8 additions & 4 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ Recorded automatically into the raw results JSON at run time
(`collect_environment()` in `bench.py`). The machine used for the published
results:

- Apple M1 Max, 10 logical cores, 64 GiB RAM
- macOS, Python, Django, psycopg and PostgreSQL versions: the `environment`
block of the published raw file, recorded at run time
- PostgreSQL 16 (official `postgres:16` image) in Docker Desktop,
Expand Down Expand Up @@ -288,7 +287,7 @@ all four ran interleaved, run by run, in one window.

Read this before quoting any number.

- **Single machine, single run day.** One Mac, one OS state, no controlled
- **Single machine, single run day.** One machine, one OS state, no controlled
thermal or background-load environment. Numbers describe relative
behaviour on this hardware; absolute performance elsewhere will differ.
- **Localhost database.** PostgreSQL runs in Docker on the same machine
Expand Down Expand Up @@ -446,9 +445,14 @@ Disclosure, all of it:
after the clock stopped; 0 is a clean stop on SIGTERM) and
`stderr_tail`. Files from before 2026-09-19 carry `concurrency` in
place of `processes`, and their concurrency-4 cell is the legacy cell.
- `results-raw-2026-10-14-matched.json`: the matched-connection run
(django-ox 1.9.0 against django-tasks-db 0.13.0): one summary row per
non-warmup run, with the failed and invalid rows and their flags. Not
produced by `bench.py`; it was condensed from the per-run files of a
separate harness.
- `render_results.py`: recomputes the results page's figures from
`results-raw-<date>.json` and `results-kill-<date>.json`; `--check PAGE`
verifies every number on the page against them.
`results-raw-<date>.json`, `results-kill-<date>.json` and the matched file;
`--check PAGE` verifies every number on the page against them.
- `docs/benchmarks.md` (in the repository root's docs): the results page.
- `soak.py` / `soaksite/`: soak and chaos harness (see above).
- `SOAK-<date>.md` / `soak-results-raw-<date>.json`: its results.
Expand Down
Loading
Loading