dj-queue 0.11.0__tar.gz → 0.13.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 (99) hide show
  1. {dj_queue-0.11.0 → dj_queue-0.13.0}/PKG-INFO +18 -4
  2. {dj_queue-0.11.0 → dj_queue-0.13.0}/README.md +17 -3
  3. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/admin.py +19 -6
  4. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/api.py +2 -0
  5. dj_queue-0.13.0/dj_queue/apps.py +17 -0
  6. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/backend.py +29 -4
  7. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/config.py +37 -11
  8. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/dashboard.py +299 -71
  9. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/db.py +10 -1
  10. dj_queue-0.13.0/dj_queue/management/commands/dj_queue_postgres_autovacuum.py +26 -0
  11. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/metrics.py +59 -0
  12. dj_queue-0.13.0/dj_queue/migrations/0012_job_djq_jobs_b_queue_id_idx_job_djq_jobs_b_conc_idx_and_more.py +33 -0
  13. dj_queue-0.13.0/dj_queue/migrations/0013_failedexecution_retry_at_and_more.py +21 -0
  14. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/models/jobs.py +7 -1
  15. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/models/runtime.py +4 -0
  16. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/observability.py +367 -12
  17. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/_helpers.py +31 -32
  18. dj_queue-0.13.0/dj_queue/operations/_insert.py +32 -0
  19. dj_queue-0.13.0/dj_queue/operations/concurrency.py +810 -0
  20. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/jobs.py +614 -337
  21. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/queues.py +18 -21
  22. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/recurring.py +90 -22
  23. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/queue_state.py +125 -7
  24. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/base.py +37 -0
  25. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/connection_budget.py +2 -7
  26. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/dispatcher.py +11 -2
  27. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/notify.py +10 -7
  28. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/scheduler.py +34 -6
  29. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/worker.py +16 -1
  30. dj_queue-0.13.0/dj_queue/sql/__init__.py +12 -0
  31. dj_queue-0.13.0/dj_queue/sql/common.py +220 -0
  32. dj_queue-0.13.0/dj_queue/sql/mysql.py +93 -0
  33. dj_queue-0.13.0/dj_queue/sql/postgres.py +526 -0
  34. dj_queue-0.13.0/dj_queue/sql/sqlite.py +8 -0
  35. dj_queue-0.13.0/dj_queue/sql/state.py +37 -0
  36. {dj_queue-0.11.0 → dj_queue-0.13.0}/pyproject.toml +1 -1
  37. dj_queue-0.11.0/dj_queue/apps.py +0 -6
  38. dj_queue-0.11.0/dj_queue/operations/_insert.py +0 -43
  39. dj_queue-0.11.0/dj_queue/operations/concurrency.py +0 -865
  40. {dj_queue-0.11.0 → dj_queue-0.13.0}/LICENSE +0 -0
  41. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/__init__.py +0 -0
  42. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/contrib/__init__.py +0 -0
  43. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/contrib/asgi.py +0 -0
  44. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/contrib/gunicorn.py +0 -0
  45. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/contrib/prometheus.py +0 -0
  46. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/cron.py +0 -0
  47. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/dashboard_actions.py +0 -0
  48. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/exceptions.py +0 -0
  49. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/hooks.py +0 -0
  50. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/log.py +0 -0
  51. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/management/__init__.py +0 -0
  52. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/management/commands/__init__.py +0 -0
  53. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue.py +0 -0
  54. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue_health.py +0 -0
  55. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  56. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0001_initial.py +0 -0
  57. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  58. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  59. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  60. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
  61. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
  62. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  63. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
  64. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
  65. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
  66. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/0011_remove_blockedexecution_djq_bl_b_conc_idx_and_more.py +0 -0
  67. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/migrations/__init__.py +0 -0
  68. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/models/__init__.py +0 -0
  69. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/models/recurring.py +0 -0
  70. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/__init__.py +0 -0
  71. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/operations/cleanup.py +0 -0
  72. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/queue_selectors.py +0 -0
  73. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/routers.py +0 -0
  74. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/__init__.py +0 -0
  75. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/errors.py +0 -0
  76. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/interruptible.py +0 -0
  77. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/pidfile.py +0 -0
  78. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/pool.py +0 -0
  79. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/procline.py +0 -0
  80. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/supervisor.py +0 -0
  81. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/runtime/topology.py +0 -0
  82. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/task_results.py +0 -0
  83. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  84. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  85. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  86. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  87. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  88. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  89. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  90. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  91. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  92. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  93. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  94. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  95. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templatetags/__init__.py +0 -0
  96. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  97. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/urls.py +0 -0
  98. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/views.py +0 -0
  99. {dj_queue-0.11.0 → dj_queue-0.13.0}/dj_queue/wakeup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: Database-backed task queue backend for Django’s Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -517,9 +517,10 @@ python manage.py dj_queue_prune --failed-older-than 604800
517
517
  python manage.py dj_queue_prune --recurring-older-than 2592000
518
518
  python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
519
519
  python manage.py dj_queue_prune --task-key nightly_cleanup
520
+ python manage.py dj_queue_postgres_autovacuum
520
521
  ```
521
522
 
522
- The health and prune commands both accept `--backend` to target a non-default backend alias.
523
+ The health, prune, and PostgreSQL autovacuum commands accept `--backend` to target a non-default backend alias.
523
524
 
524
525
  For `dj_queue_prune`, `--task-path` filters finished and failed job cleanup by
525
526
  task import path, while `--task-key` filters recurring execution cleanup by
@@ -543,9 +544,11 @@ from dj_queue.api import (
543
544
  discard_scheduled_jobs,
544
545
  retry_failed_job,
545
546
  retry_failed_jobs,
547
+ schedule_failed_job_retry,
546
548
  )
547
549
 
548
550
  retry_failed_job(job_id)
551
+ schedule_failed_job_retry(job_id, retry_at=retry_at)
549
552
  discard_failed_job(job_id)
550
553
  retry_failed_jobs(job_ids=[job_id_a, job_id_b], batch_size=2)
551
554
  discard_ready_jobs(job_ids=[ready_job_id], batch_size=1)
@@ -554,7 +557,9 @@ discard_scheduled_jobs(job_ids=[scheduled_job_id], batch_size=1)
554
557
  discard_blocked_jobs(job_ids=[blocked_job_id], batch_size=1)
555
558
  ```
556
559
 
557
- Failures stay inspectable until you act on them.
560
+ Failures stay inspectable until you act on them. Scheduled failed retries stay
561
+ failed until a dispatcher promotes rows whose `retry_at` is due; `dj_queue` does
562
+ not add an automatic retry policy or backoff engine.
558
563
 
559
564
  ## Errors When Enqueuing
560
565
 
@@ -714,6 +719,9 @@ policy, and autovacuum tuning.
714
719
  - Tune autovacuum for `dj_queue_jobs` and the high-churn
715
720
  `dj_queue_*_executions` tables, often default OLTP settings are too
716
721
  conservative for queue workloads.
722
+ - Use `python manage.py dj_queue_postgres_autovacuum` to print reviewable
723
+ `ALTER TABLE ... SET (...)` SQL for queue-table autovacuum storage
724
+ parameters. `dj_queue` does not apply these settings in migrations.
717
725
  - Keep transactions short across workers and the rest of your app. Long-lived
718
726
  transactions pin dead tuples and delay vacuum.
719
727
  - Monitor dead tuples, autovacuum frequency, and long-running queries before
@@ -812,7 +820,7 @@ Global options:
812
820
  | Option | Default | Meaning |
813
821
  |---|---|---|
814
822
  | `mode` | `"fork"` | standalone runtime mode, either `"fork"` or `"async"` |
815
- | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1}]` | worker topology and queue selectors |
823
+ | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1, "prefetch_multiplier": 2}]` | worker topology and queue selectors |
816
824
  | `dispatchers` | `[{"batch_size": 500, "polling_interval": 1, "concurrency_maintenance": true, "concurrency_maintenance_interval": 600}]` | scheduled promotion and concurrency maintenance |
817
825
  | `scheduler` | `{"dynamic_tasks_enabled": false, "polling_interval": 5}` | dynamic recurring polling; the scheduler only starts when recurring or cleanup work exists |
818
826
  | `recurring` | `{}` | static recurring task definitions keyed by name |
@@ -839,6 +847,7 @@ Worker entry options:
839
847
  | `threads` | `3` | worker threads per worker process |
840
848
  | `processes` | `1` | worker processes in `fork` mode; normalized to `1` in `async` mode |
841
849
  | `polling_interval` | `0.1` | seconds between worker polls |
850
+ | `prefetch_multiplier` | `2` | upper bound on running plus queued claimed work; workers only poll when at least one thread is idle |
842
851
 
843
852
  Dispatcher entry options:
844
853
 
@@ -914,6 +923,7 @@ queues = ["default", "email*"]
914
923
  threads = 8
915
924
  processes = 1
916
925
  polling_interval = 0.1
926
+ prefetch_multiplier = 2
917
927
 
918
928
  [[dispatchers]]
919
929
  batch_size = 500
@@ -1018,6 +1028,10 @@ Exported metric families:
1018
1028
  - `dj_queue_recurring_tasks{backend}`
1019
1029
  - `dj_queue_semaphores{queue_database}`
1020
1030
  - `dj_queue_process_rows{backend}`
1031
+ - `dj_queue_failed_jobs{backend}`
1032
+ - `dj_queue_failed_job_oldest_age_seconds{backend}`
1033
+ - `dj_queue_failed_job_retention_seconds{backend}`
1034
+ - `dj_queue_failed_jobs_over_retention{backend}`
1021
1035
 
1022
1036
  Both endpoints support bearer token authentication. Set
1023
1037
  `DJ_QUEUE_OBSERVABILITY_TOKEN` in `settings.py` and include it as
@@ -491,9 +491,10 @@ python manage.py dj_queue_prune --failed-older-than 604800
491
491
  python manage.py dj_queue_prune --recurring-older-than 2592000
492
492
  python manage.py dj_queue_prune --task-path myapp.tasks.cleanup
493
493
  python manage.py dj_queue_prune --task-key nightly_cleanup
494
+ python manage.py dj_queue_postgres_autovacuum
494
495
  ```
495
496
 
496
- The health and prune commands both accept `--backend` to target a non-default backend alias.
497
+ The health, prune, and PostgreSQL autovacuum commands accept `--backend` to target a non-default backend alias.
497
498
 
498
499
  For `dj_queue_prune`, `--task-path` filters finished and failed job cleanup by
499
500
  task import path, while `--task-key` filters recurring execution cleanup by
@@ -517,9 +518,11 @@ from dj_queue.api import (
517
518
  discard_scheduled_jobs,
518
519
  retry_failed_job,
519
520
  retry_failed_jobs,
521
+ schedule_failed_job_retry,
520
522
  )
521
523
 
522
524
  retry_failed_job(job_id)
525
+ schedule_failed_job_retry(job_id, retry_at=retry_at)
523
526
  discard_failed_job(job_id)
524
527
  retry_failed_jobs(job_ids=[job_id_a, job_id_b], batch_size=2)
525
528
  discard_ready_jobs(job_ids=[ready_job_id], batch_size=1)
@@ -528,7 +531,9 @@ discard_scheduled_jobs(job_ids=[scheduled_job_id], batch_size=1)
528
531
  discard_blocked_jobs(job_ids=[blocked_job_id], batch_size=1)
529
532
  ```
530
533
 
531
- Failures stay inspectable until you act on them.
534
+ Failures stay inspectable until you act on them. Scheduled failed retries stay
535
+ failed until a dispatcher promotes rows whose `retry_at` is due; `dj_queue` does
536
+ not add an automatic retry policy or backoff engine.
532
537
 
533
538
  ## Errors When Enqueuing
534
539
 
@@ -688,6 +693,9 @@ policy, and autovacuum tuning.
688
693
  - Tune autovacuum for `dj_queue_jobs` and the high-churn
689
694
  `dj_queue_*_executions` tables, often default OLTP settings are too
690
695
  conservative for queue workloads.
696
+ - Use `python manage.py dj_queue_postgres_autovacuum` to print reviewable
697
+ `ALTER TABLE ... SET (...)` SQL for queue-table autovacuum storage
698
+ parameters. `dj_queue` does not apply these settings in migrations.
691
699
  - Keep transactions short across workers and the rest of your app. Long-lived
692
700
  transactions pin dead tuples and delay vacuum.
693
701
  - Monitor dead tuples, autovacuum frequency, and long-running queries before
@@ -786,7 +794,7 @@ Global options:
786
794
  | Option | Default | Meaning |
787
795
  |---|---|---|
788
796
  | `mode` | `"fork"` | standalone runtime mode, either `"fork"` or `"async"` |
789
- | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1}]` | worker topology and queue selectors |
797
+ | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1, "prefetch_multiplier": 2}]` | worker topology and queue selectors |
790
798
  | `dispatchers` | `[{"batch_size": 500, "polling_interval": 1, "concurrency_maintenance": true, "concurrency_maintenance_interval": 600}]` | scheduled promotion and concurrency maintenance |
791
799
  | `scheduler` | `{"dynamic_tasks_enabled": false, "polling_interval": 5}` | dynamic recurring polling; the scheduler only starts when recurring or cleanup work exists |
792
800
  | `recurring` | `{}` | static recurring task definitions keyed by name |
@@ -813,6 +821,7 @@ Worker entry options:
813
821
  | `threads` | `3` | worker threads per worker process |
814
822
  | `processes` | `1` | worker processes in `fork` mode; normalized to `1` in `async` mode |
815
823
  | `polling_interval` | `0.1` | seconds between worker polls |
824
+ | `prefetch_multiplier` | `2` | upper bound on running plus queued claimed work; workers only poll when at least one thread is idle |
816
825
 
817
826
  Dispatcher entry options:
818
827
 
@@ -888,6 +897,7 @@ queues = ["default", "email*"]
888
897
  threads = 8
889
898
  processes = 1
890
899
  polling_interval = 0.1
900
+ prefetch_multiplier = 2
891
901
 
892
902
  [[dispatchers]]
893
903
  batch_size = 500
@@ -992,6 +1002,10 @@ Exported metric families:
992
1002
  - `dj_queue_recurring_tasks{backend}`
993
1003
  - `dj_queue_semaphores{queue_database}`
994
1004
  - `dj_queue_process_rows{backend}`
1005
+ - `dj_queue_failed_jobs{backend}`
1006
+ - `dj_queue_failed_job_oldest_age_seconds{backend}`
1007
+ - `dj_queue_failed_job_retention_seconds{backend}`
1008
+ - `dj_queue_failed_jobs_over_retention{backend}`
995
1009
 
996
1010
  Both endpoints support bearer token authentication. Set
997
1011
  `DJ_QUEUE_OBSERVABILITY_TOKEN` in `settings.py` and include it as
@@ -82,9 +82,6 @@ def _install_dj_queue_admin_site(site):
82
82
  site._dj_queue_dashboard_installed = True
83
83
 
84
84
 
85
- _install_dj_queue_admin_site(admin.site)
86
-
87
-
88
85
  def _format_admin_datetime(value):
89
86
  if value is None:
90
87
  return None
@@ -530,7 +527,19 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
530
527
 
531
528
  def get_queryset(self, request):
532
529
  queryset = super().get_queryset(request)
533
- return queryset.annotate(status_rank=status_rank_expression())
530
+ if self._orders_by_status(request):
531
+ return queryset.annotate(status_rank=status_rank_expression())
532
+ return queryset
533
+
534
+ def _orders_by_status(self, request):
535
+ ordering = request.GET.get("o")
536
+ if not ordering:
537
+ return False
538
+ try:
539
+ status_index = self.get_list_display(request).index("display_status") + 1
540
+ except ValueError:
541
+ return False
542
+ return any(part.lstrip("-") == str(status_index) for part in ordering.split("."))
534
543
 
535
544
  @admin.display(description="status", ordering="status_rank")
536
545
  def display_status(self, obj):
@@ -707,14 +716,14 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
707
716
  class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
708
717
  backend_filter_field = "job__backend_alias"
709
718
  ignored_filter_params = ("job__backend_alias",)
710
- list_display = ("job", "exception_class", "message", "display_created_at")
719
+ list_display = ("job", "exception_class", "message", "display_retry_at", "display_created_at")
711
720
  list_filter = (
712
721
  ("job__queue_name", admin.AllValuesFieldListFilter),
713
722
  "exception_class",
714
723
  )
715
724
  list_select_related = ("job",)
716
725
  actions = ("retry_jobs", "discard_jobs")
717
- readonly_fields = ("job", "exception_class", "message", "traceback", "created_at")
726
+ readonly_fields = ("job", "exception_class", "message", "traceback", "retry_at", "created_at")
718
727
  search_fields = ("job__id", "job__task_path", "message", "exception_class")
719
728
 
720
729
  @admin.action(description="Retry selected failed jobs")
@@ -747,6 +756,10 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
747
756
  def display_created_at(self, obj):
748
757
  return _format_admin_datetime(obj.created_at)
749
758
 
759
+ @admin.display(description="retry at", ordering="retry_at")
760
+ def display_retry_at(self, obj):
761
+ return _format_admin_datetime(obj.retry_at)
762
+
750
763
  def get_change_actions(self, request, obj):
751
764
  if obj is None:
752
765
  return ()
@@ -15,6 +15,7 @@ from dj_queue.operations.jobs import (
15
15
  execute_claimed_job,
16
16
  retry_failed_job,
17
17
  retry_failed_jobs,
18
+ schedule_failed_job_retry,
18
19
  )
19
20
  from dj_queue.operations.queues import pause_queue, resume_queue
20
21
  from dj_queue.operations.recurring import schedule_recurring_task, unschedule_recurring_task
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "execute_claimed_job",
33
34
  "retry_failed_job",
34
35
  "retry_failed_jobs",
36
+ "schedule_failed_job_retry",
35
37
  "schedule_recurring_task",
36
38
  "unschedule_recurring_task",
37
39
  ]
@@ -0,0 +1,17 @@
1
+ from django.apps import AppConfig
2
+ from django.apps import apps
3
+
4
+
5
+ class DjQueueConfig(AppConfig):
6
+ name = "dj_queue"
7
+ verbose_name = "dj_queue"
8
+
9
+ def ready(self):
10
+ if not apps.is_installed("django.contrib.admin"):
11
+ return
12
+
13
+ from django.contrib import admin
14
+
15
+ from dj_queue.admin import _install_dj_queue_admin_site
16
+
17
+ _install_dj_queue_admin_site(admin.site)
@@ -3,6 +3,7 @@ from django.db import close_old_connections, connections
3
3
  from django.tasks.backends.base import BaseTaskBackend
4
4
  from django.tasks.exceptions import TaskResultDoesNotExist
5
5
 
6
+ from dj_queue.config import load_backend_config
6
7
  from dj_queue.db import get_database_alias
7
8
  from dj_queue.models import Job
8
9
  from dj_queue.operations.jobs import (
@@ -47,8 +48,13 @@ class DjQueueBackend(BaseTaskBackend):
47
48
  )
48
49
 
49
50
  async def aenqueue(self, task, args, kwargs):
50
- return await sync_to_async(_async_backend_call, thread_sensitive=True)(
51
+ config = load_backend_config(self.alias)
52
+ return await sync_to_async(
53
+ _async_backend_call,
54
+ thread_sensitive=config.async_thread_sensitive,
55
+ )(
51
56
  self.enqueue,
57
+ close_connections=config.async_close_connections,
52
58
  task=task,
53
59
  args=args,
54
60
  kwargs=kwargs,
@@ -70,6 +76,17 @@ class DjQueueBackend(BaseTaskBackend):
70
76
  for job, task, dispatch_outcome in created_jobs
71
77
  ]
72
78
 
79
+ async def aenqueue_all(self, task_calls):
80
+ config = load_backend_config(self.alias)
81
+ return await sync_to_async(
82
+ _async_backend_call,
83
+ thread_sensitive=config.async_thread_sensitive,
84
+ )(
85
+ self.enqueue_all,
86
+ close_connections=config.async_close_connections,
87
+ task_calls=task_calls,
88
+ )
89
+
73
90
  def get_result(self, result_id):
74
91
  alias = get_database_alias(self.alias)
75
92
  try:
@@ -90,15 +107,23 @@ class DjQueueBackend(BaseTaskBackend):
90
107
  return task_result_from_job(job)
91
108
 
92
109
  async def aget_result(self, result_id):
93
- return await sync_to_async(_async_backend_call, thread_sensitive=True)(
110
+ config = load_backend_config(self.alias)
111
+ return await sync_to_async(
112
+ _async_backend_call,
113
+ thread_sensitive=config.async_thread_sensitive,
114
+ )(
94
115
  self.get_result,
116
+ close_connections=config.async_close_connections,
95
117
  result_id=result_id,
96
118
  )
97
119
 
98
120
 
99
- def _async_backend_call(method, /, **kwargs):
121
+ def _async_backend_call(method, /, *, close_connections=False, **kwargs):
100
122
  close_old_connections()
101
123
  try:
102
124
  return method(**kwargs)
103
125
  finally:
104
- connections.close_all()
126
+ if close_connections:
127
+ connections.close_all()
128
+ else:
129
+ close_old_connections()
@@ -19,6 +19,7 @@ DEFAULT_WORKER = {
19
19
  "threads": 3,
20
20
  "processes": 1,
21
21
  "polling_interval": 0.1,
22
+ "prefetch_multiplier": 2,
22
23
  }
23
24
 
24
25
  DEFAULT_DISPATCHER = {
@@ -52,6 +53,8 @@ DEFAULT_OPTIONS = {
52
53
  "use_skip_locked": True,
53
54
  "listen_notify": True,
54
55
  "silence_polling": True,
56
+ "async_thread_sensitive": False,
57
+ "async_close_connections": False,
55
58
  "on_thread_error": None,
56
59
  }
57
60
 
@@ -75,6 +78,7 @@ class WorkerConfig(ConfigValue):
75
78
  threads: int = 3
76
79
  processes: int = 1
77
80
  polling_interval: float = 0.1
81
+ prefetch_multiplier: int = 2
78
82
 
79
83
 
80
84
  @dataclass(frozen=True, slots=True)
@@ -125,6 +129,8 @@ class BackendConfig(ConfigValue):
125
129
  use_skip_locked: bool = True
126
130
  listen_notify: bool = True
127
131
  silence_polling: bool = True
132
+ async_thread_sensitive: bool = False
133
+ async_close_connections: bool = False
128
134
  on_thread_error: str | None = None
129
135
  skip_recurring: bool = False
130
136
  only_work: bool = False
@@ -151,21 +157,26 @@ def load_backend_config(
151
157
 
152
158
  ensure_dj_queue_backend_alias(tasks_settings, backend_alias)
153
159
  backend_block = _backend_block(tasks_settings, backend_alias)
154
- env_values = {key: env.get(key) for key in CONFIG_ENV_KEYS if env.get(key) is not None}
160
+ env_key = tuple(env.get(key) for key in CONFIG_ENV_KEYS)
155
161
  cache_key = (
156
162
  backend_alias,
157
- _cache_key(cli_overrides),
158
- _cache_key(env_values),
163
+ _EMPTY_CLI_CACHE_KEY if not cli_overrides else _cache_key(cli_overrides),
164
+ env_key,
159
165
  _cache_key(backend_block),
160
166
  )
161
- if cache_key not in _BACKEND_CONFIG_CACHE:
162
- _BACKEND_CONFIG_CACHE[cache_key] = _load_backend_config_uncached(
163
- backend_alias,
164
- cli_overrides,
165
- env_values,
166
- tasks_settings,
167
- )
168
- return _BACKEND_CONFIG_CACHE[cache_key]
167
+ cached = _BACKEND_CONFIG_CACHE.get(cache_key)
168
+ if cached is not None:
169
+ return cached
170
+
171
+ env_values = {key: value for key, value in zip(CONFIG_ENV_KEYS, env_key) if value is not None}
172
+ config = _load_backend_config_uncached(
173
+ backend_alias,
174
+ cli_overrides,
175
+ env_values,
176
+ tasks_settings,
177
+ )
178
+ _BACKEND_CONFIG_CACHE[cache_key] = config
179
+ return config
169
180
 
170
181
 
171
182
  def load_allowed_queues(
@@ -272,6 +283,12 @@ def _load_backend_config_uncached(
272
283
  use_skip_locked=_bool_option(resolved_options["use_skip_locked"], "use_skip_locked"),
273
284
  listen_notify=_bool_option(resolved_options["listen_notify"], "listen_notify"),
274
285
  silence_polling=_bool_option(resolved_options["silence_polling"], "silence_polling"),
286
+ async_thread_sensitive=_bool_option(
287
+ resolved_options["async_thread_sensitive"], "async_thread_sensitive"
288
+ ),
289
+ async_close_connections=_bool_option(
290
+ resolved_options["async_close_connections"], "async_close_connections"
291
+ ),
275
292
  on_thread_error=on_thread_error,
276
293
  skip_recurring=skip_recurring,
277
294
  only_work=only_work,
@@ -370,6 +387,8 @@ def _load_toml_options(config_path: Any, *, backend_alias: str) -> dict[str, Any
370
387
 
371
388
  try:
372
389
  config_payload = tomllib.loads(Path(config_path).read_text(encoding="utf-8"))
390
+ except OSError as exc:
391
+ raise ImproperlyConfigured(f"DJ_QUEUE_CONFIG could not be read: {exc}") from exc
373
392
  except tomllib.TOMLDecodeError as exc:
374
393
  raise ImproperlyConfigured(f"DJ_QUEUE_CONFIG TOML is invalid: {exc}") from exc
375
394
  if not isinstance(config_payload, dict):
@@ -475,6 +494,10 @@ def _build_worker_configs(raw_workers: Any, mode: str) -> tuple[WorkerConfig, ..
475
494
  raw_worker.get("polling_interval", DEFAULT_WORKER["polling_interval"]),
476
495
  f"workers[{index}].polling_interval",
477
496
  ),
497
+ prefetch_multiplier=_positive_int(
498
+ raw_worker.get("prefetch_multiplier", DEFAULT_WORKER["prefetch_multiplier"]),
499
+ f"workers[{index}].prefetch_multiplier",
500
+ ),
478
501
  )
479
502
 
480
503
  if mode == "async" and worker.processes > 1:
@@ -758,3 +781,6 @@ def _cache_key(value: Any) -> str:
758
781
  return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
759
782
  except (TypeError, ValueError) as exc:
760
783
  raise ImproperlyConfigured("dj_queue config values must be JSON-serializable") from exc
784
+
785
+
786
+ _EMPTY_CLI_CACHE_KEY = _cache_key({})