dj-queue 0.3.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 (71) hide show
  1. {dj_queue-0.3.0 → dj_queue-0.5.0}/PKG-INFO +292 -42
  2. {dj_queue-0.3.0 → dj_queue-0.5.0}/README.md +288 -40
  3. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/admin.py +178 -24
  4. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/api.py +55 -8
  5. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/backend.py +4 -4
  6. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/config.py +45 -5
  7. dj_queue-0.5.0/dj_queue/contrib/prometheus.py +129 -0
  8. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/dashboard.py +71 -430
  9. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/management/commands/dj_queue_health.py +8 -1
  10. dj_queue-0.5.0/dj_queue/management/commands/dj_queue_prune.py +44 -0
  11. 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
  12. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/models/jobs.py +11 -11
  13. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/models/recurring.py +13 -4
  14. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/models/runtime.py +11 -1
  15. dj_queue-0.5.0/dj_queue/observability.py +476 -0
  16. dj_queue-0.5.0/dj_queue/operations/cleanup.py +112 -0
  17. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/operations/concurrency.py +33 -11
  18. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/operations/jobs.py +123 -37
  19. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/operations/recurring.py +12 -3
  20. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/base.py +5 -1
  21. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/notify.py +2 -2
  22. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/scheduler.py +35 -13
  23. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/supervisor.py +20 -5
  24. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +1 -0
  25. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +4 -2
  26. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/change_form.html +17 -0
  27. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/dashboard.html +4 -48
  28. dj_queue-0.5.0/dj_queue/templates/admin/dj_queue/includes/fieldset.html +43 -0
  29. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +11 -2
  30. dj_queue-0.5.0/dj_queue/templatetags/__init__.py +0 -0
  31. dj_queue-0.5.0/dj_queue/templatetags/dj_queue_admin.py +45 -0
  32. dj_queue-0.5.0/dj_queue/urls.py +16 -0
  33. dj_queue-0.5.0/dj_queue/views.py +38 -0
  34. {dj_queue-0.3.0 → dj_queue-0.5.0}/pyproject.toml +4 -2
  35. dj_queue-0.3.0/dj_queue/management/commands/dj_queue_prune.py +0 -22
  36. dj_queue-0.3.0/dj_queue/operations/cleanup.py +0 -37
  37. {dj_queue-0.3.0 → dj_queue-0.5.0}/LICENSE +0 -0
  38. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/__init__.py +0 -0
  39. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/apps.py +0 -0
  40. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/contrib/__init__.py +0 -0
  41. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/contrib/asgi.py +0 -0
  42. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/contrib/gunicorn.py +0 -0
  43. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/db.py +0 -0
  44. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/exceptions.py +0 -0
  45. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/hooks.py +0 -0
  46. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/log.py +0 -0
  47. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/management/__init__.py +0 -0
  48. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/management/commands/__init__.py +0 -0
  49. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/management/commands/dj_queue.py +0 -0
  50. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/migrations/0001_initial.py +0 -0
  51. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  52. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  53. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  54. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/migrations/__init__.py +0 -0
  55. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/models/__init__.py +0 -0
  56. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/operations/__init__.py +0 -0
  57. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/routers.py +0 -0
  58. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/__init__.py +0 -0
  59. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/dispatcher.py +0 -0
  60. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/errors.py +0 -0
  61. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/interruptible.py +0 -0
  62. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/pidfile.py +0 -0
  63. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/pool.py +0 -0
  64. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/procline.py +0 -0
  65. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/runtime/worker.py +0 -0
  66. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  67. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  68. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  69. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  70. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  71. {dj_queue-0.3.0 → dj_queue-0.5.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.3.0
4
- Summary: Database-backed task queue backend for Django's django.tasks framework
3
+ Version: 0.5.0
4
+ Summary: Database-backed task queue backend for Djangos Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Classifier: Development Status :: 4 - Beta
@@ -17,11 +17,13 @@ Requires-Dist: croniter>=6.2.2
17
17
  Requires-Dist: django>=6.0.0
18
18
  Requires-Dist: pyyaml>=6.0.3
19
19
  Requires-Dist: psycopg>=3.3.3 ; extra == 'postgres'
20
+ Requires-Dist: prometheus-client>=0.4.0 ; extra == 'prometheus'
20
21
  Requires-Python: >=3.12
21
22
  Project-URL: Homepage, https://github.com/coriocactus/dj_queue
22
23
  Project-URL: Repository, https://github.com/coriocactus/dj_queue
23
24
  Project-URL: Issues, https://github.com/coriocactus/dj_queue/issues
24
25
  Provides-Extra: postgres
26
+ Provides-Extra: prometheus
25
27
  Description-Content-Type: text/markdown
26
28
 
27
29
  # dj_queue
@@ -43,7 +45,7 @@ It keeps the queue, live execution state, runtime metadata, and task results in
43
45
  - immediate, scheduled, recurring, and concurrency-limited work
44
46
 
45
47
  `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/).
48
+ shaped to fit Django's [task backend API](https://docs.djangoproject.com/en/6.0/topics/tasks/).
47
49
 
48
50
  ## Why dj_queue
49
51
 
@@ -71,20 +73,13 @@ Install the package:
71
73
  pip install dj-queue
72
74
  ```
73
75
 
74
- Backend-specific extras are available when you want `dj_queue` to install a
75
- database adapter for you:
76
+ Optional extras:
76
77
 
77
78
  ```bash
78
- 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
79
81
  ```
80
82
 
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
83
  Add `dj_queue` to `INSTALLED_APPS`, register the router, and point Django's task
89
84
  backend at `DjQueueBackend`:
90
85
 
@@ -164,9 +159,9 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
164
159
  - queue, process, recurring-task, and semaphore overview
165
160
  - backend-aware dashboard and raw changelists
166
161
  - queue controls: pause, resume, clear ready
167
- - job detail action: enqueue a fresh copy of any stored job
168
- - pause detail action: resume the paused queue from the raw pause row
169
- - 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
170
165
  - queue drill-down pages for state-specific inspection
171
166
 
172
167
  **Dashboard overview**
@@ -217,8 +212,9 @@ results = process_item.get_backend().enqueue_all(
217
212
 
218
213
  ### Enqueue after commit
219
214
 
220
- `enqueue()` writes immediately. If a task depends on rows that are still inside
221
- the current transaction, use `enqueue_on_commit()`:
215
+ `enqueue()` writes immediately and returns a real persisted task result ID. If a
216
+ task depends on rows that are still inside the current transaction, use
217
+ `enqueue_on_commit()`:
222
218
 
223
219
  ```python
224
220
  from django.db import transaction
@@ -230,6 +226,10 @@ with transaction.atomic():
230
226
  enqueue_on_commit(send_receipt, order.id)
231
227
  ```
232
228
 
229
+ `dj_queue` does not defer inserts implicitly or return placeholder result IDs for
230
+ uncommitted work. Use the helper above or `transaction.on_commit()` directly
231
+ when the job must not exist before commit.
232
+
233
233
  ### Examples
234
234
 
235
235
  The repository ships real runnable examples in `examples/`.
@@ -274,14 +274,18 @@ python manage.py dj_queue --skip-recurring
274
274
  Mode and topology notes:
275
275
 
276
276
  - `fork` is the default standalone mode
277
- - `async` runs supervised actors in threads inside one process
277
+ - `async` is also supported as a standalone mode and runs supervised actors in threads inside one process
278
278
  - `--only-work` starts workers without dispatchers or scheduler
279
279
  - `--only-dispatch` starts dispatchers without workers or scheduler
280
280
  - `--skip-recurring` starts without the scheduler
281
281
 
282
282
  `fork` runs each worker, dispatcher, and scheduler as a separate OS process.
283
283
  `async` runs them as threads in one process, i.e., lower memory, less isolation.
284
- Default is `fork`. Use `async` for embedded mode or memory-constrained environments.
284
+ Default is `fork`. Use standalone `async` when you want one-process supervision
285
+ with lower memory use and less isolation, or embedded `async` when `dj_queue`
286
+ should live inside an ASGI or Gunicorn server process.
287
+
288
+ In `async` mode, worker `processes > 1` is ignored and normalized to `1`.
285
289
 
286
290
  ### Claiming order
287
291
 
@@ -294,6 +298,39 @@ For example, a worker configured with `queues: ["email", "default"]` will
294
298
  prefer ready work from `email` before `default`, even if `default` contains
295
299
  higher-priority rows.
296
300
 
301
+ If you combine queue order with priorities, queue selector order still wins
302
+ across queues. Prefer one primary scheduling mechanism per worker when you can.
303
+
304
+ ### Signals and recovery
305
+
306
+ In standalone mode, both `fork` and `async` `python manage.py dj_queue`
307
+ supervisors own runtime signal handling:
308
+
309
+ - `SIGTERM` and `SIGINT` request graceful shutdown
310
+ - `SIGQUIT` takes the immediate hard-exit path
311
+ - `shutdown_timeout` controls how long the runtime waits for in-flight work to drain
312
+ - `supervisor_pidfile` can prevent duplicate standalone supervisors on one host
313
+
314
+ Runners heartbeat into the queue database. If claimed work is left behind,
315
+ `dj_queue` preserves it as failed work that operators can inspect and retry:
316
+
317
+ - `ProcessExitError`: a supervised runner exited unexpectedly
318
+ - `ProcessPrunedError`: a runner heartbeat expired and the process was pruned
319
+ - `ProcessMissingError`: claimed work was found without its registered process
320
+
321
+ Use `python manage.py dj_queue_health` to check whether any fresh runtime
322
+ process rows exist for a backend.
323
+
324
+ ### Data Contract
325
+
326
+ Job payloads and persisted return values are stored in JSON columns, so they must be JSON round-trippable.
327
+
328
+ - enqueueing args or kwargs that cannot round-trip through JSON fails immediately
329
+ - returning a non-JSON-serializable value marks the job failed instead of
330
+ leaving it claimed forever
331
+
332
+ If you need to pass model instances, files, or custom objects, store them elsewhere and pass identifiers or serialized data instead.
333
+
297
334
  ## Database Support
298
335
 
299
336
  | Backend | Support level | Notes |
@@ -303,20 +340,11 @@ higher-priority rows.
303
340
  | MariaDB 10.6+ | supported | polling plus `SKIP LOCKED` |
304
341
  | SQLite | supported with limits | polling only, serialized writes, no `SKIP LOCKED`, no `LISTEN/NOTIFY`; practical for development, CI, and smaller deployments |
305
342
 
306
- Polling is the portability path everywhere. Backend-specific features improve
307
- latency and throughput but are not correctness requirements.
308
-
309
- ## Data Contract
310
-
311
- Job payloads and persisted return values are stored in JSON columns, so they
312
- must be JSON round-trippable.
343
+ For MySQL or MariaDB, install and configure a Django-compatible driver following Django's database docs.
313
344
 
314
- - enqueueing args or kwargs that cannot round-trip through JSON fails immediately
315
- - returning a non-JSON-serializable value marks the job failed instead of
316
- leaving it claimed forever
345
+ Polling is the portability path everywhere. Backend-specific features improve latency and throughput but are not correctness requirements.
317
346
 
318
- If you need to pass model instances, files, or custom objects, store them
319
- elsewhere and pass identifiers or serialized data instead.
347
+ For production PostgreSQL operational guidance, see [Postgres Queue Health](#postgres-queue-health).
320
348
 
321
349
  ## Recurring Tasks
322
350
 
@@ -373,6 +401,17 @@ config.
373
401
  The scheduler is part of the normal `dj_queue` runtime. You do not run a
374
402
  separate recurring service.
375
403
 
404
+ Notes:
405
+
406
+ - schedules are cron expressions
407
+ - recurring task keys are scoped per backend alias
408
+ - only dynamic tasks can be unscheduled at runtime; unscheduling a static task returns `0`
409
+ - Django admin exposes the same unschedule operation on recurring-task list and detail views
410
+ - multiple schedulers sharing the same recurring config dedupe firing in the database
411
+ - finished-job cleanup runs as internal scheduler maintenance when `preserve_finished_jobs=True` and `clear_finished_jobs_after` is set
412
+ - failed-job cleanup can run as internal scheduler maintenance when `clear_failed_jobs_after` is set
413
+ - recurring execution reservation cleanup can run as internal scheduler maintenance when `clear_recurring_executions_after` is set
414
+
376
415
  ## Concurrency Controls
377
416
 
378
417
  Tasks can opt into database-backed concurrency limits.
@@ -399,6 +438,10 @@ With this configuration:
399
438
  - later jobs for the same key can block until capacity is released
400
439
  - `on_conflict = "discard"` turns the same pattern into singleton-style work
401
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
+
402
445
  ## Queue Operations
403
446
 
404
447
  `QueueInfo` exposes operational queue controls without bypassing the queue
@@ -418,15 +461,32 @@ orders.resume()
418
461
  orders.clear()
419
462
  ```
420
463
 
464
+ Queue control notes:
465
+
466
+ - pausing a queue stops future claims, not enqueueing or already-claimed work
467
+ - pause rows are scoped per backend alias
468
+ - `clear()` discards ready jobs only
469
+ - pass `backend_alias=` when you want to target a non-default `TASKS` alias
470
+
421
471
  Operational commands:
422
472
 
423
473
  ```bash
424
474
  python manage.py dj_queue_health
425
475
  python manage.py dj_queue_health --max-age 120
426
476
  python manage.py dj_queue_prune --older-than 86400
477
+ python manage.py dj_queue_prune --failed-older-than 604800
478
+ python manage.py dj_queue_prune --recurring-older-than 2592000
427
479
  python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
480
+ python manage.py dj_queue_prune --task-key nightly_cleanup
428
481
  ```
429
482
 
483
+ The runtime, health, and prune commands all accept `--backend` to target a
484
+ non-default backend alias.
485
+
486
+ For `dj_queue_prune`, `--task-path` filters finished and failed job cleanup by
487
+ task import path, while `--task-key` filters recurring execution cleanup by
488
+ recurring task key.
489
+
430
490
  ## Failed Jobs
431
491
 
432
492
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
@@ -443,8 +503,52 @@ retry_failed_job(job_id)
443
503
  discard_failed_job(job_id)
444
504
  ```
445
505
 
506
+ Model helpers are available too:
507
+
508
+ ```python
509
+ from dj_queue.exceptions import UndiscardableError
510
+ from dj_queue.models import ClaimedExecution, FailedExecution
511
+
512
+ failed = FailedExecution.objects.get(job_id=job_id)
513
+ failed.retry()
514
+ failed.discard()
515
+
516
+ FailedExecution.retry_all(FailedExecution.objects.order_by("job_id"))
517
+ FailedExecution.discard_all_in_batches()
518
+
519
+ try:
520
+ ClaimedExecution.discard_all_in_batches()
521
+ except UndiscardableError:
522
+ pass
523
+ ```
524
+
446
525
  Failures stay inspectable until you act on them.
447
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
+
448
552
  ## Multi-Database Setup
449
553
 
450
554
  `dj_queue` can keep queue tables on a dedicated database alias.
@@ -487,6 +591,28 @@ python manage.py migrate dj_queue --database queue
487
591
  With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
488
592
  database.
489
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
+
490
616
  ## Embedded Server Mode
491
617
 
492
618
  `dj_queue` can run inside an existing server process via embedded async
@@ -557,12 +683,19 @@ Start with these options:
557
683
  - `dispatchers`: scheduled promotion and concurrency maintenance settings
558
684
  - `scheduler`: dynamic recurring polling settings
559
685
  - `database_alias`: database alias for queue tables and runtime activity
560
- - `preserve_finished_jobs` and `clear_finished_jobs_after`: result retention and cleanup
686
+ - `preserve_finished_jobs` and `clear_finished_jobs_after`: successful result retention and cleanup
687
+ - `clear_failed_jobs_after`: optional failed-job retention window
688
+ - `clear_recurring_executions_after`: optional recurring reservation retention window
561
689
 
562
- Additional operational tuning is available when needed, including
563
- `use_skip_locked`, `listen_notify`, `silence_polling`,
564
- `process_heartbeat_interval`, `process_alive_threshold`, `shutdown_timeout`, and
565
- `on_thread_error`.
690
+ Additional operational tuning is available when needed:
691
+
692
+ - `use_skip_locked`: use `SKIP LOCKED` when the active backend supports it
693
+ - `listen_notify`: PostgreSQL-only worker wakeup optimization layered on top of polling
694
+ - `silence_polling`: suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger
695
+ - `process_heartbeat_interval` and `process_alive_threshold`: process liveness reporting and stale-runner detection
696
+ - `shutdown_timeout`: graceful drain window before standalone shutdown gives up on waiting
697
+ - `supervisor_pidfile`: optional pidfile guard for standalone supervisors
698
+ - `on_thread_error`: dotted callback path for runtime infrastructure exceptions
566
699
 
567
700
  On PostgreSQL, `listen_notify` uses the same Django PostgreSQL driver
568
701
  configuration as the main database connection. Install a compatible driver in
@@ -587,15 +720,21 @@ python manage.py dj_queue --config /etc/dj_queue.yml
587
720
  DJ_QUEUE_CONFIG=/etc/dj_queue.yml python manage.py dj_queue
588
721
  ```
589
722
 
590
- The YAML file should contain a single mapping of backend option values. It uses
591
- the same shape as `TASKS[backend_alias]["OPTIONS"]`, not the full Django
592
- `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:
593
730
 
594
731
  ```yaml
595
732
  mode: async
596
733
  database_alias: queue
597
734
  preserve_finished_jobs: true
598
735
  clear_finished_jobs_after: 86400
736
+ clear_failed_jobs_after: null
737
+ clear_recurring_executions_after: null
599
738
  listen_notify: true
600
739
  silence_polling: true
601
740
 
@@ -624,8 +763,28 @@ recurring:
624
763
  description: nightly cleanup
625
764
  ```
626
765
 
627
- This file is merged on top of `TASKS[backend_alias]["OPTIONS"]`, then any
628
- 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
+ ```
629
788
 
630
789
  Environment overrides currently supported by `dj_queue` itself:
631
790
 
@@ -633,6 +792,97 @@ Environment overrides currently supported by `dj_queue` itself:
633
792
  - `DJ_QUEUE_MODE`
634
793
  - `DJ_QUEUE_SKIP_RECURRING`
635
794
 
795
+ ## Lifecycle Hooks
796
+
797
+ Register hooks before starting the runtime, typically during Django startup.
798
+ Each callback receives the live supervisor or runner instance.
799
+
800
+ ```python
801
+ from dj_queue.hooks import on_start, on_worker_start, register_hook
802
+
803
+ @on_start
804
+ def supervisor_started(process):
805
+ print(process.name)
806
+
807
+ @on_worker_start
808
+ def worker_started(process):
809
+ print(process.metadata)
810
+
811
+ @register_hook("scheduler.exit")
812
+ def scheduler_exited(process):
813
+ print(process.name)
814
+ ```
815
+
816
+ Available hook helpers:
817
+
818
+ - supervisor: `on_start`, `on_stop`, `on_exit`
819
+ - worker: `on_worker_start`, `on_worker_stop`, `on_worker_exit`
820
+ - dispatcher: `on_dispatcher_start`, `on_dispatcher_stop`, `on_dispatcher_exit`
821
+ - scheduler: `on_scheduler_start`, `on_scheduler_stop`, `on_scheduler_exit`
822
+ - generic events: `register_hook("worker.start")`, `register_hook("dispatcher.stop")`, and so on
823
+
824
+ Hook notes:
825
+
826
+ - hooks fire in registration order
827
+ - hook failures do not block later hooks
828
+ - hook failures are isolated and routed through `on_thread_error`
829
+
830
+ ### Runtime infrastructure errors
831
+
832
+ Set `on_thread_error` to a dotted callable path when you want custom handling
833
+ for queue-runtime exceptions:
834
+
835
+ ```python
836
+ TASKS = {
837
+ "default": {
838
+ "BACKEND": "dj_queue.backend.DjQueueBackend",
839
+ "QUEUES": [],
840
+ "OPTIONS": {
841
+ "on_thread_error": "myapp.queue.report_runtime_error",
842
+ },
843
+ },
844
+ }
845
+ ```
846
+
847
+ The callback receives the raised exception object for background runtime issues
848
+ such as hook failures, heartbeat failures, notify-watcher failures, and managed
849
+ runner crashes. It is not used for exceptions raised by your task code; those
850
+ become failed jobs instead.
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
+
636
886
  ## License
637
887
 
638
888
  MIT