dj-queue 0.2.4__tar.gz → 0.4.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 (66) hide show
  1. {dj_queue-0.2.4 → dj_queue-0.4.0}/PKG-INFO +180 -10
  2. {dj_queue-0.2.4 → dj_queue-0.4.0}/README.md +179 -9
  3. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/admin.py +133 -48
  4. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/config.py +17 -1
  5. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/dashboard.py +276 -184
  6. dj_queue-0.4.0/dj_queue/management/commands/dj_queue_prune.py +44 -0
  7. dj_queue-0.4.0/dj_queue/operations/cleanup.py +103 -0
  8. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/operations/jobs.py +28 -2
  9. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/base.py +39 -13
  10. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/notify.py +2 -2
  11. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/scheduler.py +32 -12
  12. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/supervisor.py +30 -5
  13. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +14 -0
  14. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +13 -0
  15. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +23 -0
  16. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +9 -0
  17. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_paginator.html +19 -0
  18. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_queue_controls.html +18 -0
  19. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +13 -0
  20. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/templates/admin/dj_queue/change_form.html +6 -0
  21. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/templates/admin/dj_queue/dashboard.html +9 -279
  22. dj_queue-0.4.0/dj_queue/templates/admin/dj_queue/includes/fieldset.html +43 -0
  23. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +14 -48
  24. dj_queue-0.4.0/dj_queue/templatetags/__init__.py +0 -0
  25. dj_queue-0.4.0/dj_queue/templatetags/dj_queue_admin.py +45 -0
  26. {dj_queue-0.2.4 → dj_queue-0.4.0}/pyproject.toml +1 -1
  27. dj_queue-0.2.4/dj_queue/management/commands/dj_queue_prune.py +0 -22
  28. dj_queue-0.2.4/dj_queue/operations/cleanup.py +0 -37
  29. {dj_queue-0.2.4 → dj_queue-0.4.0}/LICENSE +0 -0
  30. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/__init__.py +0 -0
  31. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/api.py +0 -0
  32. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/apps.py +0 -0
  33. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/backend.py +0 -0
  34. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/contrib/__init__.py +0 -0
  35. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/contrib/asgi.py +0 -0
  36. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/contrib/gunicorn.py +0 -0
  37. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/db.py +0 -0
  38. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/exceptions.py +0 -0
  39. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/hooks.py +0 -0
  40. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/log.py +0 -0
  41. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/management/__init__.py +0 -0
  42. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/management/commands/__init__.py +0 -0
  43. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/management/commands/dj_queue.py +0 -0
  44. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/management/commands/dj_queue_health.py +0 -0
  45. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/migrations/0001_initial.py +0 -0
  46. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  47. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  48. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  49. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/migrations/__init__.py +0 -0
  50. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/models/__init__.py +0 -0
  51. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/models/jobs.py +0 -0
  52. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/models/recurring.py +0 -0
  53. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/models/runtime.py +0 -0
  54. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/operations/__init__.py +0 -0
  55. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/operations/concurrency.py +0 -0
  56. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/operations/recurring.py +0 -0
  57. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/routers.py +0 -0
  58. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/__init__.py +0 -0
  59. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/dispatcher.py +0 -0
  60. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/errors.py +0 -0
  61. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/interruptible.py +0 -0
  62. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/pidfile.py +0 -0
  63. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/pool.py +0 -0
  64. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/procline.py +0 -0
  65. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/runtime/worker.py +0 -0
  66. {dj_queue-0.2.4 → dj_queue-0.4.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: Database-backed task queue backend for Django's django.tasks framework
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -165,9 +165,11 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
165
165
  - backend-aware dashboard and raw changelists
166
166
  - queue controls: pause, resume, clear ready
167
167
  - job detail action: enqueue a fresh copy of any stored job
168
+ - recurring-task actions: unschedule from list and detail views
168
169
  - pause detail action: resume the paused queue from the raw pause row
169
170
  - failed-job actions: retry and discard from list and detail views
170
171
  - 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
171
173
 
172
174
  **Dashboard overview**
173
175
 
@@ -217,8 +219,9 @@ results = process_item.get_backend().enqueue_all(
217
219
 
218
220
  ### Enqueue after commit
219
221
 
220
- `enqueue()` writes immediately. If a task depends on rows that are still inside
221
- the current transaction, use `enqueue_on_commit()`:
222
+ `enqueue()` writes immediately and returns a real persisted task result ID. If a
223
+ task depends on rows that are still inside the current transaction, use
224
+ `enqueue_on_commit()`:
222
225
 
223
226
  ```python
224
227
  from django.db import transaction
@@ -230,6 +233,10 @@ with transaction.atomic():
230
233
  enqueue_on_commit(send_receipt, order.id)
231
234
  ```
232
235
 
236
+ `dj_queue` does not defer inserts implicitly or return placeholder result IDs for
237
+ uncommitted work. Use the helper above or `transaction.on_commit()` directly
238
+ when the job must not exist before commit.
239
+
233
240
  ### Examples
234
241
 
235
242
  The repository ships real runnable examples in `examples/`.
@@ -274,14 +281,18 @@ python manage.py dj_queue --skip-recurring
274
281
  Mode and topology notes:
275
282
 
276
283
  - `fork` is the default standalone mode
277
- - `async` runs supervised actors in threads inside one process
284
+ - `async` is also supported as a standalone mode and runs supervised actors in threads inside one process
278
285
  - `--only-work` starts workers without dispatchers or scheduler
279
286
  - `--only-dispatch` starts dispatchers without workers or scheduler
280
287
  - `--skip-recurring` starts without the scheduler
281
288
 
282
289
  `fork` runs each worker, dispatcher, and scheduler as a separate OS process.
283
290
  `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.
291
+ Default is `fork`. Use standalone `async` when you want one-process supervision
292
+ with lower memory use and less isolation, or embedded `async` when `dj_queue`
293
+ should live inside an ASGI or Gunicorn server process.
294
+
295
+ In `async` mode, worker `processes > 1` is ignored and normalized to `1`.
285
296
 
286
297
  ### Claiming order
287
298
 
@@ -294,6 +305,29 @@ For example, a worker configured with `queues: ["email", "default"]` will
294
305
  prefer ready work from `email` before `default`, even if `default` contains
295
306
  higher-priority rows.
296
307
 
308
+ If you combine queue order with priorities, queue selector order still wins
309
+ across queues. Prefer one primary scheduling mechanism per worker when you can.
310
+
311
+ ### Signals and recovery
312
+
313
+ In standalone mode, both `fork` and `async` `python manage.py dj_queue`
314
+ supervisors own runtime signal handling:
315
+
316
+ - `SIGTERM` and `SIGINT` request graceful shutdown
317
+ - `SIGQUIT` takes the immediate hard-exit path
318
+ - `shutdown_timeout` controls how long the runtime waits for in-flight work to drain
319
+ - `supervisor_pidfile` can prevent duplicate standalone supervisors on one host
320
+
321
+ Runners heartbeat into the queue database. If claimed work is left behind,
322
+ `dj_queue` preserves it as failed work that operators can inspect and retry:
323
+
324
+ - `ProcessExitError`: a supervised runner exited unexpectedly
325
+ - `ProcessPrunedError`: a runner heartbeat expired and the process was pruned
326
+ - `ProcessMissingError`: claimed work was found without its registered process
327
+
328
+ Use `python manage.py dj_queue_health` to check whether any fresh runtime
329
+ process rows exist for a backend.
330
+
297
331
  ## Database Support
298
332
 
299
333
  | Backend | Support level | Notes |
@@ -318,6 +352,31 @@ must be JSON round-trippable.
318
352
  If you need to pass model instances, files, or custom objects, store them
319
353
  elsewhere and pass identifiers or serialized data instead.
320
354
 
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:
361
+
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"`
367
+
368
+ ```python
369
+ from dj_queue.exceptions import EnqueueError
370
+
371
+ try:
372
+ sync_account.enqueue(account_id, "refresh")
373
+ except EnqueueError as exc:
374
+ handle_enqueue_error(exc)
375
+ ```
376
+
377
+ Task execution errors are different: they become failed jobs and stay
378
+ inspectable in the queue database.
379
+
321
380
  ## Recurring Tasks
322
381
 
323
382
  `dj_queue` supports both static recurring tasks from settings and dynamic
@@ -373,6 +432,16 @@ config.
373
432
  The scheduler is part of the normal `dj_queue` runtime. You do not run a
374
433
  separate recurring service.
375
434
 
435
+ Recurring notes:
436
+
437
+ - schedules are cron expressions
438
+ - only dynamic tasks can be unscheduled at runtime; unscheduling a static task returns `0`
439
+ - Django admin exposes the same unschedule operation on recurring-task list and detail views
440
+ - multiple schedulers sharing the same recurring config dedupe firing in the database
441
+ - finished-job cleanup runs as internal scheduler maintenance when `preserve_finished_jobs=True` and `clear_finished_jobs_after` is set
442
+ - failed-job cleanup can run as internal scheduler maintenance when `clear_failed_jobs_after` is set
443
+ - recurring execution reservation cleanup can run as internal scheduler maintenance when `clear_recurring_executions_after` is set
444
+
376
445
  ## Concurrency Controls
377
446
 
378
447
  Tasks can opt into database-backed concurrency limits.
@@ -418,15 +487,31 @@ orders.resume()
418
487
  orders.clear()
419
488
  ```
420
489
 
490
+ Queue control notes:
491
+
492
+ - pausing a queue stops future claims, not enqueueing or already-claimed work
493
+ - `clear()` discards ready jobs only
494
+ - pass `backend_alias=` when you want to target a non-default `TASKS` alias
495
+
421
496
  Operational commands:
422
497
 
423
498
  ```bash
424
499
  python manage.py dj_queue_health
425
500
  python manage.py dj_queue_health --max-age 120
426
501
  python manage.py dj_queue_prune --older-than 86400
502
+ python manage.py dj_queue_prune --failed-older-than 604800
503
+ python manage.py dj_queue_prune --recurring-older-than 2592000
427
504
  python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
505
+ python manage.py dj_queue_prune --task-key nightly_cleanup
428
506
  ```
429
507
 
508
+ The runtime, health, and prune commands all accept `--backend` to target a
509
+ non-default backend alias.
510
+
511
+ For `dj_queue_prune`, `--task-path` filters finished and failed job cleanup by
512
+ task import path, while `--task-key` filters recurring execution cleanup by
513
+ recurring task key.
514
+
430
515
  ## Failed Jobs
431
516
 
432
517
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
@@ -443,6 +528,25 @@ retry_failed_job(job_id)
443
528
  discard_failed_job(job_id)
444
529
  ```
445
530
 
531
+ Model helpers are available too:
532
+
533
+ ```python
534
+ from dj_queue.exceptions import UndiscardableError
535
+ from dj_queue.models import ClaimedExecution, FailedExecution
536
+
537
+ failed = FailedExecution.objects.get(job_id=job_id)
538
+ failed.retry()
539
+ failed.discard()
540
+
541
+ FailedExecution.retry_all(FailedExecution.objects.order_by("job_id"))
542
+ FailedExecution.discard_all_in_batches()
543
+
544
+ try:
545
+ ClaimedExecution.discard_all_in_batches()
546
+ except UndiscardableError:
547
+ pass
548
+ ```
549
+
446
550
  Failures stay inspectable until you act on them.
447
551
 
448
552
  ## Multi-Database Setup
@@ -557,12 +661,19 @@ Start with these options:
557
661
  - `dispatchers`: scheduled promotion and concurrency maintenance settings
558
662
  - `scheduler`: dynamic recurring polling settings
559
663
  - `database_alias`: database alias for queue tables and runtime activity
560
- - `preserve_finished_jobs` and `clear_finished_jobs_after`: result retention and cleanup
664
+ - `preserve_finished_jobs` and `clear_finished_jobs_after`: successful result retention and cleanup
665
+ - `clear_failed_jobs_after`: optional failed-job retention window
666
+ - `clear_recurring_executions_after`: optional recurring reservation retention window
561
667
 
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`.
668
+ Additional operational tuning is available when needed:
669
+
670
+ - `use_skip_locked`: use `SKIP LOCKED` when the active backend supports it
671
+ - `listen_notify`: PostgreSQL-only worker wakeup optimization layered on top of polling
672
+ - `silence_polling`: suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger
673
+ - `process_heartbeat_interval` and `process_alive_threshold`: process liveness reporting and stale-runner detection
674
+ - `shutdown_timeout`: graceful drain window before standalone shutdown gives up on waiting
675
+ - `supervisor_pidfile`: optional pidfile guard for standalone supervisors
676
+ - `on_thread_error`: dotted callback path for runtime infrastructure exceptions
566
677
 
567
678
  On PostgreSQL, `listen_notify` uses the same Django PostgreSQL driver
568
679
  configuration as the main database connection. Install a compatible driver in
@@ -596,6 +707,8 @@ mode: async
596
707
  database_alias: queue
597
708
  preserve_finished_jobs: true
598
709
  clear_finished_jobs_after: 86400
710
+ clear_failed_jobs_after: null
711
+ clear_recurring_executions_after: null
599
712
  listen_notify: true
600
713
  silence_polling: true
601
714
 
@@ -633,6 +746,63 @@ Environment overrides currently supported by `dj_queue` itself:
633
746
  - `DJ_QUEUE_MODE`
634
747
  - `DJ_QUEUE_SKIP_RECURRING`
635
748
 
749
+ ## Lifecycle Hooks
750
+
751
+ Register hooks before starting the runtime, typically during Django startup.
752
+ Each callback receives the live supervisor or runner instance.
753
+
754
+ ```python
755
+ from dj_queue.hooks import on_start, on_worker_start, register_hook
756
+
757
+ @on_start
758
+ def supervisor_started(process):
759
+ print(process.name)
760
+
761
+ @on_worker_start
762
+ def worker_started(process):
763
+ print(process.metadata)
764
+
765
+ @register_hook("scheduler.exit")
766
+ def scheduler_exited(process):
767
+ print(process.name)
768
+ ```
769
+
770
+ Available hook helpers:
771
+
772
+ - supervisor: `on_start`, `on_stop`, `on_exit`
773
+ - worker: `on_worker_start`, `on_worker_stop`, `on_worker_exit`
774
+ - dispatcher: `on_dispatcher_start`, `on_dispatcher_stop`, `on_dispatcher_exit`
775
+ - scheduler: `on_scheduler_start`, `on_scheduler_stop`, `on_scheduler_exit`
776
+ - generic events: `register_hook("worker.start")`, `register_hook("dispatcher.stop")`, and so on
777
+
778
+ Hook notes:
779
+
780
+ - hooks fire in registration order
781
+ - hook failures do not block later hooks
782
+ - hook failures are isolated and routed through `on_thread_error`
783
+
784
+ ### Runtime infrastructure errors
785
+
786
+ Set `on_thread_error` to a dotted callable path when you want custom handling
787
+ for queue-runtime exceptions:
788
+
789
+ ```python
790
+ TASKS = {
791
+ "default": {
792
+ "BACKEND": "dj_queue.backend.DjQueueBackend",
793
+ "QUEUES": [],
794
+ "OPTIONS": {
795
+ "on_thread_error": "myapp.queue.report_runtime_error",
796
+ },
797
+ },
798
+ }
799
+ ```
800
+
801
+ The callback receives the raised exception object for background runtime issues
802
+ such as hook failures, heartbeat failures, notify-watcher failures, and managed
803
+ runner crashes. It is not used for exceptions raised by your task code; those
804
+ become failed jobs instead.
805
+
636
806
  ## License
637
807
 
638
808
  MIT
@@ -139,9 +139,11 @@ If Django admin is installed, `dj_queue` adds an operator dashboard at
139
139
  - backend-aware dashboard and raw changelists
140
140
  - queue controls: pause, resume, clear ready
141
141
  - job detail action: enqueue a fresh copy of any stored job
142
+ - recurring-task actions: unschedule from list and detail views
142
143
  - pause detail action: resume the paused queue from the raw pause row
143
144
  - failed-job actions: retry and discard from list and detail views
144
145
  - 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
145
147
 
146
148
  **Dashboard overview**
147
149
 
@@ -191,8 +193,9 @@ results = process_item.get_backend().enqueue_all(
191
193
 
192
194
  ### Enqueue after commit
193
195
 
194
- `enqueue()` writes immediately. If a task depends on rows that are still inside
195
- the current transaction, use `enqueue_on_commit()`:
196
+ `enqueue()` writes immediately and returns a real persisted task result ID. If a
197
+ task depends on rows that are still inside the current transaction, use
198
+ `enqueue_on_commit()`:
196
199
 
197
200
  ```python
198
201
  from django.db import transaction
@@ -204,6 +207,10 @@ with transaction.atomic():
204
207
  enqueue_on_commit(send_receipt, order.id)
205
208
  ```
206
209
 
210
+ `dj_queue` does not defer inserts implicitly or return placeholder result IDs for
211
+ uncommitted work. Use the helper above or `transaction.on_commit()` directly
212
+ when the job must not exist before commit.
213
+
207
214
  ### Examples
208
215
 
209
216
  The repository ships real runnable examples in `examples/`.
@@ -248,14 +255,18 @@ python manage.py dj_queue --skip-recurring
248
255
  Mode and topology notes:
249
256
 
250
257
  - `fork` is the default standalone mode
251
- - `async` runs supervised actors in threads inside one process
258
+ - `async` is also supported as a standalone mode and runs supervised actors in threads inside one process
252
259
  - `--only-work` starts workers without dispatchers or scheduler
253
260
  - `--only-dispatch` starts dispatchers without workers or scheduler
254
261
  - `--skip-recurring` starts without the scheduler
255
262
 
256
263
  `fork` runs each worker, dispatcher, and scheduler as a separate OS process.
257
264
  `async` runs them as threads in one process, i.e., lower memory, less isolation.
258
- Default is `fork`. Use `async` for embedded mode or memory-constrained environments.
265
+ Default is `fork`. Use standalone `async` when you want one-process supervision
266
+ with lower memory use and less isolation, or embedded `async` when `dj_queue`
267
+ should live inside an ASGI or Gunicorn server process.
268
+
269
+ In `async` mode, worker `processes > 1` is ignored and normalized to `1`.
259
270
 
260
271
  ### Claiming order
261
272
 
@@ -268,6 +279,29 @@ For example, a worker configured with `queues: ["email", "default"]` will
268
279
  prefer ready work from `email` before `default`, even if `default` contains
269
280
  higher-priority rows.
270
281
 
282
+ If you combine queue order with priorities, queue selector order still wins
283
+ across queues. Prefer one primary scheduling mechanism per worker when you can.
284
+
285
+ ### Signals and recovery
286
+
287
+ In standalone mode, both `fork` and `async` `python manage.py dj_queue`
288
+ supervisors own runtime signal handling:
289
+
290
+ - `SIGTERM` and `SIGINT` request graceful shutdown
291
+ - `SIGQUIT` takes the immediate hard-exit path
292
+ - `shutdown_timeout` controls how long the runtime waits for in-flight work to drain
293
+ - `supervisor_pidfile` can prevent duplicate standalone supervisors on one host
294
+
295
+ Runners heartbeat into the queue database. If claimed work is left behind,
296
+ `dj_queue` preserves it as failed work that operators can inspect and retry:
297
+
298
+ - `ProcessExitError`: a supervised runner exited unexpectedly
299
+ - `ProcessPrunedError`: a runner heartbeat expired and the process was pruned
300
+ - `ProcessMissingError`: claimed work was found without its registered process
301
+
302
+ Use `python manage.py dj_queue_health` to check whether any fresh runtime
303
+ process rows exist for a backend.
304
+
271
305
  ## Database Support
272
306
 
273
307
  | Backend | Support level | Notes |
@@ -292,6 +326,31 @@ must be JSON round-trippable.
292
326
  If you need to pass model instances, files, or custom objects, store them
293
327
  elsewhere and pass identifiers or serialized data instead.
294
328
 
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:
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"`
341
+
342
+ ```python
343
+ from dj_queue.exceptions import EnqueueError
344
+
345
+ try:
346
+ sync_account.enqueue(account_id, "refresh")
347
+ except EnqueueError as exc:
348
+ handle_enqueue_error(exc)
349
+ ```
350
+
351
+ Task execution errors are different: they become failed jobs and stay
352
+ inspectable in the queue database.
353
+
295
354
  ## Recurring Tasks
296
355
 
297
356
  `dj_queue` supports both static recurring tasks from settings and dynamic
@@ -347,6 +406,16 @@ config.
347
406
  The scheduler is part of the normal `dj_queue` runtime. You do not run a
348
407
  separate recurring service.
349
408
 
409
+ Recurring notes:
410
+
411
+ - schedules are cron expressions
412
+ - only dynamic tasks can be unscheduled at runtime; unscheduling a static task returns `0`
413
+ - Django admin exposes the same unschedule operation on recurring-task list and detail views
414
+ - multiple schedulers sharing the same recurring config dedupe firing in the database
415
+ - finished-job cleanup runs as internal scheduler maintenance when `preserve_finished_jobs=True` and `clear_finished_jobs_after` is set
416
+ - failed-job cleanup can run as internal scheduler maintenance when `clear_failed_jobs_after` is set
417
+ - recurring execution reservation cleanup can run as internal scheduler maintenance when `clear_recurring_executions_after` is set
418
+
350
419
  ## Concurrency Controls
351
420
 
352
421
  Tasks can opt into database-backed concurrency limits.
@@ -392,15 +461,31 @@ orders.resume()
392
461
  orders.clear()
393
462
  ```
394
463
 
464
+ Queue control notes:
465
+
466
+ - pausing a queue stops future claims, not enqueueing or already-claimed work
467
+ - `clear()` discards ready jobs only
468
+ - pass `backend_alias=` when you want to target a non-default `TASKS` alias
469
+
395
470
  Operational commands:
396
471
 
397
472
  ```bash
398
473
  python manage.py dj_queue_health
399
474
  python manage.py dj_queue_health --max-age 120
400
475
  python manage.py dj_queue_prune --older-than 86400
476
+ python manage.py dj_queue_prune --failed-older-than 604800
477
+ python manage.py dj_queue_prune --recurring-older-than 2592000
401
478
  python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
479
+ python manage.py dj_queue_prune --task-key nightly_cleanup
402
480
  ```
403
481
 
482
+ The runtime, health, and prune commands all accept `--backend` to target a
483
+ non-default backend alias.
484
+
485
+ For `dj_queue_prune`, `--task-path` filters finished and failed job cleanup by
486
+ task import path, while `--task-key` filters recurring execution cleanup by
487
+ recurring task key.
488
+
404
489
  ## Failed Jobs
405
490
 
406
491
  When a task raises, `dj_queue` keeps the job and its failed execution row in the
@@ -417,6 +502,25 @@ retry_failed_job(job_id)
417
502
  discard_failed_job(job_id)
418
503
  ```
419
504
 
505
+ Model helpers are available too:
506
+
507
+ ```python
508
+ from dj_queue.exceptions import UndiscardableError
509
+ from dj_queue.models import ClaimedExecution, FailedExecution
510
+
511
+ failed = FailedExecution.objects.get(job_id=job_id)
512
+ failed.retry()
513
+ failed.discard()
514
+
515
+ FailedExecution.retry_all(FailedExecution.objects.order_by("job_id"))
516
+ FailedExecution.discard_all_in_batches()
517
+
518
+ try:
519
+ ClaimedExecution.discard_all_in_batches()
520
+ except UndiscardableError:
521
+ pass
522
+ ```
523
+
420
524
  Failures stay inspectable until you act on them.
421
525
 
422
526
  ## Multi-Database Setup
@@ -531,12 +635,19 @@ Start with these options:
531
635
  - `dispatchers`: scheduled promotion and concurrency maintenance settings
532
636
  - `scheduler`: dynamic recurring polling settings
533
637
  - `database_alias`: database alias for queue tables and runtime activity
534
- - `preserve_finished_jobs` and `clear_finished_jobs_after`: result retention and cleanup
638
+ - `preserve_finished_jobs` and `clear_finished_jobs_after`: successful result retention and cleanup
639
+ - `clear_failed_jobs_after`: optional failed-job retention window
640
+ - `clear_recurring_executions_after`: optional recurring reservation retention window
535
641
 
536
- Additional operational tuning is available when needed, including
537
- `use_skip_locked`, `listen_notify`, `silence_polling`,
538
- `process_heartbeat_interval`, `process_alive_threshold`, `shutdown_timeout`, and
539
- `on_thread_error`.
642
+ Additional operational tuning is available when needed:
643
+
644
+ - `use_skip_locked`: use `SKIP LOCKED` when the active backend supports it
645
+ - `listen_notify`: PostgreSQL-only worker wakeup optimization layered on top of polling
646
+ - `silence_polling`: suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger
647
+ - `process_heartbeat_interval` and `process_alive_threshold`: process liveness reporting and stale-runner detection
648
+ - `shutdown_timeout`: graceful drain window before standalone shutdown gives up on waiting
649
+ - `supervisor_pidfile`: optional pidfile guard for standalone supervisors
650
+ - `on_thread_error`: dotted callback path for runtime infrastructure exceptions
540
651
 
541
652
  On PostgreSQL, `listen_notify` uses the same Django PostgreSQL driver
542
653
  configuration as the main database connection. Install a compatible driver in
@@ -570,6 +681,8 @@ mode: async
570
681
  database_alias: queue
571
682
  preserve_finished_jobs: true
572
683
  clear_finished_jobs_after: 86400
684
+ clear_failed_jobs_after: null
685
+ clear_recurring_executions_after: null
573
686
  listen_notify: true
574
687
  silence_polling: true
575
688
 
@@ -607,6 +720,63 @@ Environment overrides currently supported by `dj_queue` itself:
607
720
  - `DJ_QUEUE_MODE`
608
721
  - `DJ_QUEUE_SKIP_RECURRING`
609
722
 
723
+ ## Lifecycle Hooks
724
+
725
+ Register hooks before starting the runtime, typically during Django startup.
726
+ Each callback receives the live supervisor or runner instance.
727
+
728
+ ```python
729
+ from dj_queue.hooks import on_start, on_worker_start, register_hook
730
+
731
+ @on_start
732
+ def supervisor_started(process):
733
+ print(process.name)
734
+
735
+ @on_worker_start
736
+ def worker_started(process):
737
+ print(process.metadata)
738
+
739
+ @register_hook("scheduler.exit")
740
+ def scheduler_exited(process):
741
+ print(process.name)
742
+ ```
743
+
744
+ Available hook helpers:
745
+
746
+ - supervisor: `on_start`, `on_stop`, `on_exit`
747
+ - worker: `on_worker_start`, `on_worker_stop`, `on_worker_exit`
748
+ - dispatcher: `on_dispatcher_start`, `on_dispatcher_stop`, `on_dispatcher_exit`
749
+ - scheduler: `on_scheduler_start`, `on_scheduler_stop`, `on_scheduler_exit`
750
+ - generic events: `register_hook("worker.start")`, `register_hook("dispatcher.stop")`, and so on
751
+
752
+ Hook notes:
753
+
754
+ - hooks fire in registration order
755
+ - hook failures do not block later hooks
756
+ - hook failures are isolated and routed through `on_thread_error`
757
+
758
+ ### Runtime infrastructure errors
759
+
760
+ Set `on_thread_error` to a dotted callable path when you want custom handling
761
+ for queue-runtime exceptions:
762
+
763
+ ```python
764
+ TASKS = {
765
+ "default": {
766
+ "BACKEND": "dj_queue.backend.DjQueueBackend",
767
+ "QUEUES": [],
768
+ "OPTIONS": {
769
+ "on_thread_error": "myapp.queue.report_runtime_error",
770
+ },
771
+ },
772
+ }
773
+ ```
774
+
775
+ The callback receives the raised exception object for background runtime issues
776
+ such as hook failures, heartbeat failures, notify-watcher failures, and managed
777
+ runner crashes. It is not used for exceptions raised by your task code; those
778
+ become failed jobs instead.
779
+
610
780
  ## License
611
781
 
612
782
  MIT