dj-queue 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. dj_queue-0.4.0/README.md → dj_queue-0.5.0/PKG-INFO +165 -59
  2. dj_queue-0.4.0/PKG-INFO → dj_queue-0.5.0/README.md +137 -85
  3. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/admin.py +81 -17
  4. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/api.py +55 -8
  5. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/backend.py +4 -4
  6. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/config.py +28 -4
  7. dj_queue-0.5.0/dj_queue/contrib/prometheus.py +129 -0
  8. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/dashboard.py +44 -430
  9. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/management/commands/dj_queue_health.py +8 -1
  10. dj_queue-0.5.0/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +100 -0
  11. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/models/jobs.py +11 -11
  12. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/models/recurring.py +13 -4
  13. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/models/runtime.py +11 -1
  14. dj_queue-0.5.0/dj_queue/observability.py +476 -0
  15. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/operations/cleanup.py +11 -2
  16. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/operations/concurrency.py +33 -11
  17. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/operations/jobs.py +99 -39
  18. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/operations/recurring.py +12 -3
  19. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/base.py +5 -1
  20. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/scheduler.py +3 -1
  21. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/supervisor.py +20 -5
  22. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +1 -0
  23. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/change_form.html +11 -0
  24. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/dashboard.html +4 -48
  25. dj_queue-0.5.0/dj_queue/urls.py +16 -0
  26. dj_queue-0.5.0/dj_queue/views.py +38 -0
  27. {dj_queue-0.4.0 → dj_queue-0.5.0}/pyproject.toml +4 -2
  28. {dj_queue-0.4.0 → dj_queue-0.5.0}/LICENSE +0 -0
  29. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/__init__.py +0 -0
  30. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/apps.py +0 -0
  31. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/contrib/__init__.py +0 -0
  32. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/contrib/asgi.py +0 -0
  33. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/contrib/gunicorn.py +0 -0
  34. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/db.py +0 -0
  35. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/exceptions.py +0 -0
  36. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/hooks.py +0 -0
  37. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/log.py +0 -0
  38. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/management/__init__.py +0 -0
  39. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/management/commands/__init__.py +0 -0
  40. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/management/commands/dj_queue.py +0 -0
  41. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  42. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/migrations/0001_initial.py +0 -0
  43. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  44. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  45. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  46. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/migrations/__init__.py +0 -0
  47. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/models/__init__.py +0 -0
  48. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/operations/__init__.py +0 -0
  49. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/routers.py +0 -0
  50. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/__init__.py +0 -0
  51. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/dispatcher.py +0 -0
  52. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/errors.py +0 -0
  53. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/interruptible.py +0 -0
  54. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/notify.py +0 -0
  55. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/pidfile.py +0 -0
  56. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/pool.py +0 -0
  57. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/procline.py +0 -0
  58. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/runtime/worker.py +0 -0
  59. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  60. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  61. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  62. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  63. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  64. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  65. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  66. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  67. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  68. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templatetags/__init__.py +0 -0
  69. {dj_queue-0.4.0 → dj_queue-0.5.0}/dj_queue/templatetags/dj_queue_admin.py +0 -0
@@ -1,3 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: dj-queue
3
+ Version: 0.5.0
4
+ Summary: Database-backed task queue backend for Django’s Tasks framework.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Framework :: Django
9
+ Classifier: Framework :: Django :: 6.0
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Dist: croniter>=6.2.2
17
+ Requires-Dist: django>=6.0.0
18
+ Requires-Dist: pyyaml>=6.0.3
19
+ Requires-Dist: psycopg>=3.3.3 ; extra == 'postgres'
20
+ Requires-Dist: prometheus-client>=0.4.0 ; extra == 'prometheus'
21
+ Requires-Python: >=3.12
22
+ Project-URL: Homepage, https://github.com/coriocactus/dj_queue
23
+ Project-URL: Repository, https://github.com/coriocactus/dj_queue
24
+ Project-URL: Issues, https://github.com/coriocactus/dj_queue/issues
25
+ Provides-Extra: postgres
26
+ Provides-Extra: prometheus
27
+ Description-Content-Type: text/markdown
28
+
1
29
  # dj_queue
2
30
 
3
31
  [![CI](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml/badge.svg)](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml)
@@ -17,7 +45,7 @@ It keeps the queue, live execution state, runtime metadata, and task results in
17
45
  - immediate, scheduled, recurring, and concurrency-limited work
18
46
 
19
47
  `dj_queue` is inspired by Rails' [Solid Queue](https://github.com/rails/solid_queue),
20
- but shaped to fit Django's [task backend API](https://docs.djangoproject.com/en/6.0/topics/tasks/).
48
+ shaped to fit Django's [task backend API](https://docs.djangoproject.com/en/6.0/topics/tasks/).
21
49
 
22
50
  ## Why dj_queue
23
51
 
@@ -45,20 +73,13 @@ Install the package:
45
73
  pip install dj-queue
46
74
  ```
47
75
 
48
- Backend-specific extras are available when you want `dj_queue` to install a
49
- database adapter for you:
76
+ Optional extras:
50
77
 
51
78
  ```bash
52
- pip install "dj-queue[postgres]"
79
+ pip install "dj-queue[postgres]" # psycopg for PostgreSQL + LISTEN/NOTIFY
80
+ pip install "dj-queue[prometheus]" # prometheus_client for /dj_queue/metrics
53
81
  ```
54
82
 
55
- Notes:
56
-
57
- - `postgres` installs `psycopg`, which Django's PostgreSQL backend and
58
- `dj_queue`'s optional `LISTEN/NOTIFY` wakeups use
59
- - for MySQL or MariaDB, install and configure a Django-compatible driver in
60
- your application following Django's database docs
61
-
62
83
  Add `dj_queue` to `INSTALLED_APPS`, register the router, and point Django's task
63
84
  backend at `DjQueueBackend`:
64
85
 
@@ -138,12 +159,10 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
138
159
  - queue, process, recurring-task, and semaphore overview
139
160
  - backend-aware dashboard and raw changelists
140
161
  - queue controls: pause, resume, clear ready
141
- - job detail action: enqueue a fresh copy of any stored job
142
- - recurring-task actions: unschedule from list and detail views
143
- - pause detail action: resume the paused queue from the raw pause row
144
- - failed-job actions: retry and discard from list and detail views
162
+ - job actions: enqueue a fresh copy of any stored job
163
+ - failed jobs: retry and discard from list and detail views
164
+ - unschedule dynamic recurring tasks
145
165
  - queue drill-down pages for state-specific inspection
146
- - queue drill-down actions: discard ready, scheduled, and blocked jobs; retry or discard failed jobs; enqueue finished jobs again
147
166
 
148
167
  **Dashboard overview**
149
168
 
@@ -302,54 +321,30 @@ Runners heartbeat into the queue database. If claimed work is left behind,
302
321
  Use `python manage.py dj_queue_health` to check whether any fresh runtime
303
322
  process rows exist for a backend.
304
323
 
305
- ## Database Support
306
-
307
- | Backend | Support level | Notes |
308
- |---|---|---|
309
- | PostgreSQL | first-class | polling, `SKIP LOCKED`, and optional `LISTEN/NOTIFY` |
310
- | MySQL 8+ | supported | polling plus `SKIP LOCKED` |
311
- | MariaDB 10.6+ | supported | polling plus `SKIP LOCKED` |
312
- | SQLite | supported with limits | polling only, serialized writes, no `SKIP LOCKED`, no `LISTEN/NOTIFY`; practical for development, CI, and smaller deployments |
313
-
314
- Polling is the portability path everywhere. Backend-specific features improve
315
- latency and throughput but are not correctness requirements.
316
-
317
- ## Data Contract
324
+ ### Data Contract
318
325
 
319
- Job payloads and persisted return values are stored in JSON columns, so they
320
- must be JSON round-trippable.
326
+ Job payloads and persisted return values are stored in JSON columns, so they must be JSON round-trippable.
321
327
 
322
328
  - enqueueing args or kwargs that cannot round-trip through JSON fails immediately
323
329
  - returning a non-JSON-serializable value marks the job failed instead of
324
330
  leaving it claimed forever
325
331
 
326
- If you need to pass model instances, files, or custom objects, store them
327
- elsewhere and pass identifiers or serialized data instead.
332
+ If you need to pass model instances, files, or custom objects, store them elsewhere and pass identifiers or serialized data instead.
328
333
 
329
- ## Errors When Enqueuing
330
-
331
- `DjQueueBackend.enqueue()` raises `dj_queue.exceptions.EnqueueError` for
332
- backend-side validation failures instead of silently dropping work.
333
-
334
- Common reasons include:
334
+ ## Database Support
335
335
 
336
- - args or kwargs are not JSON round-trippable
337
- - `concurrency_key` is set without `concurrency_limit`
338
- - `concurrency_key` cannot be resolved from the enqueue arguments
339
- - `concurrency_key` does not resolve to a non-empty string up to 255 chars
340
- - `on_conflict` is not `"block"` or `"discard"`
336
+ | Backend | Support level | Notes |
337
+ |---|---|---|
338
+ | PostgreSQL | first-class | polling, `SKIP LOCKED`, and optional `LISTEN/NOTIFY` |
339
+ | MySQL 8+ | supported | polling plus `SKIP LOCKED` |
340
+ | MariaDB 10.6+ | supported | polling plus `SKIP LOCKED` |
341
+ | SQLite | supported with limits | polling only, serialized writes, no `SKIP LOCKED`, no `LISTEN/NOTIFY`; practical for development, CI, and smaller deployments |
341
342
 
342
- ```python
343
- from dj_queue.exceptions import EnqueueError
343
+ For MySQL or MariaDB, install and configure a Django-compatible driver following Django's database docs.
344
344
 
345
- try:
346
- sync_account.enqueue(account_id, "refresh")
347
- except EnqueueError as exc:
348
- handle_enqueue_error(exc)
349
- ```
345
+ Polling is the portability path everywhere. Backend-specific features improve latency and throughput but are not correctness requirements.
350
346
 
351
- Task execution errors are different: they become failed jobs and stay
352
- inspectable in the queue database.
347
+ For production PostgreSQL operational guidance, see [Postgres Queue Health](#postgres-queue-health).
353
348
 
354
349
  ## Recurring Tasks
355
350
 
@@ -406,9 +401,10 @@ config.
406
401
  The scheduler is part of the normal `dj_queue` runtime. You do not run a
407
402
  separate recurring service.
408
403
 
409
- Recurring notes:
404
+ Notes:
410
405
 
411
406
  - schedules are cron expressions
407
+ - recurring task keys are scoped per backend alias
412
408
  - only dynamic tasks can be unscheduled at runtime; unscheduling a static task returns `0`
413
409
  - Django admin exposes the same unschedule operation on recurring-task list and detail views
414
410
  - multiple schedulers sharing the same recurring config dedupe firing in the database
@@ -442,6 +438,10 @@ With this configuration:
442
438
  - later jobs for the same key can block until capacity is released
443
439
  - `on_conflict = "discard"` turns the same pattern into singleton-style work
444
440
 
441
+ Semaphore rows remain shared on the queue database. If you want per-backend
442
+ isolation for a limit, express that in the `concurrency_key` itself rather than
443
+ expecting one semaphore namespace per backend alias.
444
+
445
445
  ## Queue Operations
446
446
 
447
447
  `QueueInfo` exposes operational queue controls without bypassing the queue
@@ -464,6 +464,7 @@ orders.clear()
464
464
  Queue control notes:
465
465
 
466
466
  - pausing a queue stops future claims, not enqueueing or already-claimed work
467
+ - pause rows are scoped per backend alias
467
468
  - `clear()` discards ready jobs only
468
469
  - pass `backend_alias=` when you want to target a non-default `TASKS` alias
469
470
 
@@ -523,6 +524,31 @@ except UndiscardableError:
523
524
 
524
525
  Failures stay inspectable until you act on them.
525
526
 
527
+ ## Errors When Enqueuing
528
+
529
+ `DjQueueBackend.enqueue()` raises `dj_queue.exceptions.EnqueueError` for
530
+ backend-side validation failures instead of silently dropping work.
531
+
532
+ Common reasons include:
533
+
534
+ - args or kwargs are not JSON round-trippable
535
+ - `concurrency_key` is set without `concurrency_limit`
536
+ - `concurrency_key` cannot be resolved from the enqueue arguments
537
+ - `concurrency_key` does not resolve to a non-empty string up to 255 chars
538
+ - `on_conflict` is not `"block"` or `"discard"`
539
+
540
+ ```python
541
+ from dj_queue.exceptions import EnqueueError
542
+
543
+ try:
544
+ sync_account.enqueue(account_id, "refresh")
545
+ except EnqueueError as exc:
546
+ handle_enqueue_error(exc)
547
+ ```
548
+
549
+ Task execution errors are different: they become failed jobs and stay
550
+ inspectable in the queue database.
551
+
526
552
  ## Multi-Database Setup
527
553
 
528
554
  `dj_queue` can keep queue tables on a dedicated database alias.
@@ -565,6 +591,28 @@ python manage.py migrate dj_queue --database queue
565
591
  With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
566
592
  database.
567
593
 
594
+ ## Postgres Queue Health
595
+
596
+ Operational and configuration guidance for scaling with `dj_queue` in
597
+ production PostgreSQL deployments, covering dedicated database setup, retention
598
+ policy, and autovacuum tuning.
599
+
600
+ - Use a dedicated queue database via `database_alias`. Keep reporting and
601
+ long-running transactions off the queue database.
602
+ - Keep retention short. Set `preserve_finished_jobs = False` if you do not need
603
+ successful results. Otherwise use bounded `clear_finished_jobs_after`,
604
+ `clear_failed_jobs_after`, and `clear_recurring_executions_after` values.
605
+ - Run `python manage.py dj_queue_prune` regularly for stricter cleanup.
606
+ - Keep `use_skip_locked = True` and `listen_notify = True` unless you have a
607
+ specific reason not to.
608
+ - Tune autovacuum for `dj_queue_jobs` and the high-churn
609
+ `dj_queue_*_executions` tables, often default OLTP settings are too
610
+ conservative for queue workloads.
611
+ - Keep transactions short across workers and the rest of your app. Long-lived
612
+ transactions pin dead tuples and delay vacuum.
613
+ - Monitor dead tuples, autovacuum frequency, and long-running queries before
614
+ reaching for partitioning or bulk-ingest paths.
615
+
568
616
  ## Embedded Server Mode
569
617
 
570
618
  `dj_queue` can run inside an existing server process via embedded async
@@ -672,9 +720,13 @@ python manage.py dj_queue --config /etc/dj_queue.yml
672
720
  DJ_QUEUE_CONFIG=/etc/dj_queue.yml python manage.py dj_queue
673
721
  ```
674
722
 
675
- The YAML file should contain a single mapping of backend option values. It uses
676
- the same shape as `TASKS[backend_alias]["OPTIONS"]`, not the full Django
677
- `TASKS` structure:
723
+ The YAML file is an overlay on `TASKS[backend_alias]["OPTIONS"]`. It supports
724
+ two shapes:
725
+
726
+ - a flat mapping of option values for the selected backend alias
727
+ - a `backends` mapping keyed by backend alias, where only the selected alias is applied
728
+
729
+ Flat mapping example:
678
730
 
679
731
  ```yaml
680
732
  mode: async
@@ -711,8 +763,28 @@ recurring:
711
763
  description: nightly cleanup
712
764
  ```
713
765
 
714
- This file is merged on top of `TASKS[backend_alias]["OPTIONS"]`, then any
715
- environment-variable and CLI overrides win after that.
766
+ Multi-backend overlay example:
767
+
768
+ ```yaml
769
+ backends:
770
+ default:
771
+ mode: async
772
+ database_alias: default
773
+ workers:
774
+ - queues: ["default", "email*"]
775
+ threads: 8
776
+ processes: 1
777
+ polling_interval: 0.1
778
+
779
+ critical:
780
+ mode: fork
781
+ database_alias: queue
782
+ workers:
783
+ - queues: ["alerts", "critical-review"]
784
+ threads: 2
785
+ processes: 1
786
+ polling_interval: 0.05
787
+ ```
716
788
 
717
789
  Environment overrides currently supported by `dj_queue` itself:
718
790
 
@@ -777,6 +849,40 @@ such as hook failures, heartbeat failures, notify-watcher failures, and managed
777
849
  runner crashes. It is not used for exceptions raised by your task code; those
778
850
  become failed jobs instead.
779
851
 
852
+ ## Monitoring
853
+
854
+ Queue statistics are available in JSON via `/dj_queue/stats.json` and in
855
+ Prometheus text format via `/dj_queue/metrics`.
856
+
857
+ Include `dj_queue.urls` to expose them:
858
+
859
+ ```python
860
+ urlpatterns += [path("dj_queue/", include("dj_queue.urls"))]
861
+ ```
862
+
863
+ The `/dj_queue/metrics` endpoint requires the `prometheus` extra:
864
+
865
+ ```bash
866
+ pip install "dj-queue[prometheus]"
867
+ ```
868
+
869
+ Exported metric families:
870
+
871
+ - `dj_queue_queue_jobs{backend,queue,state}`
872
+ - `dj_queue_queue_paused{backend,queue}`
873
+ - `dj_queue_queue_latency_seconds{backend,queue}`
874
+ - `dj_queue_queue_live_workers{backend,queue}`
875
+ - `dj_queue_runner_processes{backend,status}`
876
+ - `dj_queue_runner_processes_by_kind{backend,kind,status}`
877
+ - `dj_queue_recurring_tasks{backend}`
878
+ - `dj_queue_semaphores{queue_database}`
879
+ - `dj_queue_process_rows{backend}`
880
+
881
+ Both endpoints support bearer token authentication. Set
882
+ `DJ_QUEUE_OBSERVABILITY_TOKEN` in `settings.py` and include it as
883
+ `Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
884
+ the network or proxy layer.
885
+
780
886
  ## License
781
887
 
782
888
  MIT
@@ -1,29 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: dj-queue
3
- Version: 0.4.0
4
- Summary: Database-backed task queue backend for Django's django.tasks framework
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Classifier: Development Status :: 4 - Beta
8
- Classifier: Framework :: Django
9
- Classifier: Framework :: Django :: 6.0
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
- Classifier: Programming Language :: Python :: 3.14
15
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Requires-Dist: croniter>=6.2.2
17
- Requires-Dist: django>=6.0.0
18
- Requires-Dist: pyyaml>=6.0.3
19
- Requires-Dist: psycopg>=3.3.3 ; extra == 'postgres'
20
- Requires-Python: >=3.12
21
- Project-URL: Homepage, https://github.com/coriocactus/dj_queue
22
- Project-URL: Repository, https://github.com/coriocactus/dj_queue
23
- Project-URL: Issues, https://github.com/coriocactus/dj_queue/issues
24
- Provides-Extra: postgres
25
- Description-Content-Type: text/markdown
26
-
27
1
  # dj_queue
28
2
 
29
3
  [![CI](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml/badge.svg)](https://github.com/coriocactus/dj_queue/actions/workflows/ci.yml)
@@ -43,7 +17,7 @@ It keeps the queue, live execution state, runtime metadata, and task results in
43
17
  - immediate, scheduled, recurring, and concurrency-limited work
44
18
 
45
19
  `dj_queue` is inspired by Rails' [Solid Queue](https://github.com/rails/solid_queue),
46
- but shaped to fit Django's [task backend API](https://docs.djangoproject.com/en/6.0/topics/tasks/).
20
+ shaped to fit Django's [task backend API](https://docs.djangoproject.com/en/6.0/topics/tasks/).
47
21
 
48
22
  ## Why dj_queue
49
23
 
@@ -71,20 +45,13 @@ Install the package:
71
45
  pip install dj-queue
72
46
  ```
73
47
 
74
- Backend-specific extras are available when you want `dj_queue` to install a
75
- database adapter for you:
48
+ Optional extras:
76
49
 
77
50
  ```bash
78
- pip install "dj-queue[postgres]"
51
+ pip install "dj-queue[postgres]" # psycopg for PostgreSQL + LISTEN/NOTIFY
52
+ pip install "dj-queue[prometheus]" # prometheus_client for /dj_queue/metrics
79
53
  ```
80
54
 
81
- Notes:
82
-
83
- - `postgres` installs `psycopg`, which Django's PostgreSQL backend and
84
- `dj_queue`'s optional `LISTEN/NOTIFY` wakeups use
85
- - for MySQL or MariaDB, install and configure a Django-compatible driver in
86
- your application following Django's database docs
87
-
88
55
  Add `dj_queue` to `INSTALLED_APPS`, register the router, and point Django's task
89
56
  backend at `DjQueueBackend`:
90
57
 
@@ -164,12 +131,10 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
164
131
  - queue, process, recurring-task, and semaphore overview
165
132
  - backend-aware dashboard and raw changelists
166
133
  - queue controls: pause, resume, clear ready
167
- - job detail action: enqueue a fresh copy of any stored job
168
- - recurring-task actions: unschedule from list and detail views
169
- - pause detail action: resume the paused queue from the raw pause row
170
- - failed-job actions: retry and discard from list and detail views
134
+ - job actions: enqueue a fresh copy of any stored job
135
+ - failed jobs: retry and discard from list and detail views
136
+ - unschedule dynamic recurring tasks
171
137
  - queue drill-down pages for state-specific inspection
172
- - queue drill-down actions: discard ready, scheduled, and blocked jobs; retry or discard failed jobs; enqueue finished jobs again
173
138
 
174
139
  **Dashboard overview**
175
140
 
@@ -328,54 +293,30 @@ Runners heartbeat into the queue database. If claimed work is left behind,
328
293
  Use `python manage.py dj_queue_health` to check whether any fresh runtime
329
294
  process rows exist for a backend.
330
295
 
331
- ## Database Support
332
-
333
- | Backend | Support level | Notes |
334
- |---|---|---|
335
- | PostgreSQL | first-class | polling, `SKIP LOCKED`, and optional `LISTEN/NOTIFY` |
336
- | MySQL 8+ | supported | polling plus `SKIP LOCKED` |
337
- | MariaDB 10.6+ | supported | polling plus `SKIP LOCKED` |
338
- | SQLite | supported with limits | polling only, serialized writes, no `SKIP LOCKED`, no `LISTEN/NOTIFY`; practical for development, CI, and smaller deployments |
339
-
340
- Polling is the portability path everywhere. Backend-specific features improve
341
- latency and throughput but are not correctness requirements.
342
-
343
- ## Data Contract
296
+ ### Data Contract
344
297
 
345
- Job payloads and persisted return values are stored in JSON columns, so they
346
- must be JSON round-trippable.
298
+ Job payloads and persisted return values are stored in JSON columns, so they must be JSON round-trippable.
347
299
 
348
300
  - enqueueing args or kwargs that cannot round-trip through JSON fails immediately
349
301
  - returning a non-JSON-serializable value marks the job failed instead of
350
302
  leaving it claimed forever
351
303
 
352
- If you need to pass model instances, files, or custom objects, store them
353
- elsewhere and pass identifiers or serialized data instead.
304
+ If you need to pass model instances, files, or custom objects, store them elsewhere and pass identifiers or serialized data instead.
354
305
 
355
- ## Errors When Enqueuing
356
-
357
- `DjQueueBackend.enqueue()` raises `dj_queue.exceptions.EnqueueError` for
358
- backend-side validation failures instead of silently dropping work.
359
-
360
- Common reasons include:
306
+ ## Database Support
361
307
 
362
- - args or kwargs are not JSON round-trippable
363
- - `concurrency_key` is set without `concurrency_limit`
364
- - `concurrency_key` cannot be resolved from the enqueue arguments
365
- - `concurrency_key` does not resolve to a non-empty string up to 255 chars
366
- - `on_conflict` is not `"block"` or `"discard"`
308
+ | Backend | Support level | Notes |
309
+ |---|---|---|
310
+ | PostgreSQL | first-class | polling, `SKIP LOCKED`, and optional `LISTEN/NOTIFY` |
311
+ | MySQL 8+ | supported | polling plus `SKIP LOCKED` |
312
+ | MariaDB 10.6+ | supported | polling plus `SKIP LOCKED` |
313
+ | SQLite | supported with limits | polling only, serialized writes, no `SKIP LOCKED`, no `LISTEN/NOTIFY`; practical for development, CI, and smaller deployments |
367
314
 
368
- ```python
369
- from dj_queue.exceptions import EnqueueError
315
+ For MySQL or MariaDB, install and configure a Django-compatible driver following Django's database docs.
370
316
 
371
- try:
372
- sync_account.enqueue(account_id, "refresh")
373
- except EnqueueError as exc:
374
- handle_enqueue_error(exc)
375
- ```
317
+ Polling is the portability path everywhere. Backend-specific features improve latency and throughput but are not correctness requirements.
376
318
 
377
- Task execution errors are different: they become failed jobs and stay
378
- inspectable in the queue database.
319
+ For production PostgreSQL operational guidance, see [Postgres Queue Health](#postgres-queue-health).
379
320
 
380
321
  ## Recurring Tasks
381
322
 
@@ -432,9 +373,10 @@ config.
432
373
  The scheduler is part of the normal `dj_queue` runtime. You do not run a
433
374
  separate recurring service.
434
375
 
435
- Recurring notes:
376
+ Notes:
436
377
 
437
378
  - schedules are cron expressions
379
+ - recurring task keys are scoped per backend alias
438
380
  - only dynamic tasks can be unscheduled at runtime; unscheduling a static task returns `0`
439
381
  - Django admin exposes the same unschedule operation on recurring-task list and detail views
440
382
  - multiple schedulers sharing the same recurring config dedupe firing in the database
@@ -468,6 +410,10 @@ With this configuration:
468
410
  - later jobs for the same key can block until capacity is released
469
411
  - `on_conflict = "discard"` turns the same pattern into singleton-style work
470
412
 
413
+ Semaphore rows remain shared on the queue database. If you want per-backend
414
+ isolation for a limit, express that in the `concurrency_key` itself rather than
415
+ expecting one semaphore namespace per backend alias.
416
+
471
417
  ## Queue Operations
472
418
 
473
419
  `QueueInfo` exposes operational queue controls without bypassing the queue
@@ -490,6 +436,7 @@ orders.clear()
490
436
  Queue control notes:
491
437
 
492
438
  - pausing a queue stops future claims, not enqueueing or already-claimed work
439
+ - pause rows are scoped per backend alias
493
440
  - `clear()` discards ready jobs only
494
441
  - pass `backend_alias=` when you want to target a non-default `TASKS` alias
495
442
 
@@ -549,6 +496,31 @@ except UndiscardableError:
549
496
 
550
497
  Failures stay inspectable until you act on them.
551
498
 
499
+ ## Errors When Enqueuing
500
+
501
+ `DjQueueBackend.enqueue()` raises `dj_queue.exceptions.EnqueueError` for
502
+ backend-side validation failures instead of silently dropping work.
503
+
504
+ Common reasons include:
505
+
506
+ - args or kwargs are not JSON round-trippable
507
+ - `concurrency_key` is set without `concurrency_limit`
508
+ - `concurrency_key` cannot be resolved from the enqueue arguments
509
+ - `concurrency_key` does not resolve to a non-empty string up to 255 chars
510
+ - `on_conflict` is not `"block"` or `"discard"`
511
+
512
+ ```python
513
+ from dj_queue.exceptions import EnqueueError
514
+
515
+ try:
516
+ sync_account.enqueue(account_id, "refresh")
517
+ except EnqueueError as exc:
518
+ handle_enqueue_error(exc)
519
+ ```
520
+
521
+ Task execution errors are different: they become failed jobs and stay
522
+ inspectable in the queue database.
523
+
552
524
  ## Multi-Database Setup
553
525
 
554
526
  `dj_queue` can keep queue tables on a dedicated database alias.
@@ -591,6 +563,28 @@ python manage.py migrate dj_queue --database queue
591
563
  With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
592
564
  database.
593
565
 
566
+ ## Postgres Queue Health
567
+
568
+ Operational and configuration guidance for scaling with `dj_queue` in
569
+ production PostgreSQL deployments, covering dedicated database setup, retention
570
+ policy, and autovacuum tuning.
571
+
572
+ - Use a dedicated queue database via `database_alias`. Keep reporting and
573
+ long-running transactions off the queue database.
574
+ - Keep retention short. Set `preserve_finished_jobs = False` if you do not need
575
+ successful results. Otherwise use bounded `clear_finished_jobs_after`,
576
+ `clear_failed_jobs_after`, and `clear_recurring_executions_after` values.
577
+ - Run `python manage.py dj_queue_prune` regularly for stricter cleanup.
578
+ - Keep `use_skip_locked = True` and `listen_notify = True` unless you have a
579
+ specific reason not to.
580
+ - Tune autovacuum for `dj_queue_jobs` and the high-churn
581
+ `dj_queue_*_executions` tables, often default OLTP settings are too
582
+ conservative for queue workloads.
583
+ - Keep transactions short across workers and the rest of your app. Long-lived
584
+ transactions pin dead tuples and delay vacuum.
585
+ - Monitor dead tuples, autovacuum frequency, and long-running queries before
586
+ reaching for partitioning or bulk-ingest paths.
587
+
594
588
  ## Embedded Server Mode
595
589
 
596
590
  `dj_queue` can run inside an existing server process via embedded async
@@ -698,9 +692,13 @@ python manage.py dj_queue --config /etc/dj_queue.yml
698
692
  DJ_QUEUE_CONFIG=/etc/dj_queue.yml python manage.py dj_queue
699
693
  ```
700
694
 
701
- The YAML file should contain a single mapping of backend option values. It uses
702
- the same shape as `TASKS[backend_alias]["OPTIONS"]`, not the full Django
703
- `TASKS` structure:
695
+ The YAML file is an overlay on `TASKS[backend_alias]["OPTIONS"]`. It supports
696
+ two shapes:
697
+
698
+ - a flat mapping of option values for the selected backend alias
699
+ - a `backends` mapping keyed by backend alias, where only the selected alias is applied
700
+
701
+ Flat mapping example:
704
702
 
705
703
  ```yaml
706
704
  mode: async
@@ -737,8 +735,28 @@ recurring:
737
735
  description: nightly cleanup
738
736
  ```
739
737
 
740
- This file is merged on top of `TASKS[backend_alias]["OPTIONS"]`, then any
741
- environment-variable and CLI overrides win after that.
738
+ Multi-backend overlay example:
739
+
740
+ ```yaml
741
+ backends:
742
+ default:
743
+ mode: async
744
+ database_alias: default
745
+ workers:
746
+ - queues: ["default", "email*"]
747
+ threads: 8
748
+ processes: 1
749
+ polling_interval: 0.1
750
+
751
+ critical:
752
+ mode: fork
753
+ database_alias: queue
754
+ workers:
755
+ - queues: ["alerts", "critical-review"]
756
+ threads: 2
757
+ processes: 1
758
+ polling_interval: 0.05
759
+ ```
742
760
 
743
761
  Environment overrides currently supported by `dj_queue` itself:
744
762
 
@@ -803,6 +821,40 @@ such as hook failures, heartbeat failures, notify-watcher failures, and managed
803
821
  runner crashes. It is not used for exceptions raised by your task code; those
804
822
  become failed jobs instead.
805
823
 
824
+ ## Monitoring
825
+
826
+ Queue statistics are available in JSON via `/dj_queue/stats.json` and in
827
+ Prometheus text format via `/dj_queue/metrics`.
828
+
829
+ Include `dj_queue.urls` to expose them:
830
+
831
+ ```python
832
+ urlpatterns += [path("dj_queue/", include("dj_queue.urls"))]
833
+ ```
834
+
835
+ The `/dj_queue/metrics` endpoint requires the `prometheus` extra:
836
+
837
+ ```bash
838
+ pip install "dj-queue[prometheus]"
839
+ ```
840
+
841
+ Exported metric families:
842
+
843
+ - `dj_queue_queue_jobs{backend,queue,state}`
844
+ - `dj_queue_queue_paused{backend,queue}`
845
+ - `dj_queue_queue_latency_seconds{backend,queue}`
846
+ - `dj_queue_queue_live_workers{backend,queue}`
847
+ - `dj_queue_runner_processes{backend,status}`
848
+ - `dj_queue_runner_processes_by_kind{backend,kind,status}`
849
+ - `dj_queue_recurring_tasks{backend}`
850
+ - `dj_queue_semaphores{queue_database}`
851
+ - `dj_queue_process_rows{backend}`
852
+
853
+ Both endpoints support bearer token authentication. Set
854
+ `DJ_QUEUE_OBSERVABILITY_TOKEN` in `settings.py` and include it as
855
+ `Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
856
+ the network or proxy layer.
857
+
806
858
  ## License
807
859
 
808
860
  MIT