dj-queue 0.12.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 (96) hide show
  1. {dj_queue-0.12.0 → dj_queue-0.13.0}/PKG-INFO +16 -4
  2. {dj_queue-0.12.0 → dj_queue-0.13.0}/README.md +15 -3
  3. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/admin.py +6 -2
  4. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/api.py +2 -0
  5. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/config.py +19 -11
  6. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/db.py +10 -1
  7. dj_queue-0.13.0/dj_queue/management/commands/dj_queue_postgres_autovacuum.py +26 -0
  8. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/metrics.py +59 -0
  9. dj_queue-0.13.0/dj_queue/migrations/0013_failedexecution_retry_at_and_more.py +21 -0
  10. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/models/jobs.py +5 -1
  11. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/observability.py +333 -7
  12. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/concurrency.py +165 -155
  13. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/jobs.py +224 -80
  14. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/queue_state.py +15 -22
  15. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/base.py +13 -0
  16. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/dispatcher.py +11 -2
  17. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/worker.py +3 -0
  18. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/common.py +67 -21
  19. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/mysql.py +13 -1
  20. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/postgres.py +135 -1
  21. {dj_queue-0.12.0 → dj_queue-0.13.0}/pyproject.toml +1 -1
  22. {dj_queue-0.12.0 → dj_queue-0.13.0}/LICENSE +0 -0
  23. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/__init__.py +0 -0
  24. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/apps.py +0 -0
  25. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/backend.py +0 -0
  26. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/contrib/__init__.py +0 -0
  27. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/contrib/asgi.py +0 -0
  28. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/contrib/gunicorn.py +0 -0
  29. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/contrib/prometheus.py +0 -0
  30. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/cron.py +0 -0
  31. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/dashboard.py +0 -0
  32. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/dashboard_actions.py +0 -0
  33. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/exceptions.py +0 -0
  34. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/hooks.py +0 -0
  35. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/log.py +0 -0
  36. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/management/__init__.py +0 -0
  37. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/management/commands/__init__.py +0 -0
  38. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue.py +0 -0
  39. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue_health.py +0 -0
  40. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  41. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0001_initial.py +0 -0
  42. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  43. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  44. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  45. {dj_queue-0.12.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
  46. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
  47. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  48. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
  49. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
  50. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
  51. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/0011_remove_blockedexecution_djq_bl_b_conc_idx_and_more.py +0 -0
  52. {dj_queue-0.12.0 → 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 +0 -0
  53. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/migrations/__init__.py +0 -0
  54. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/models/__init__.py +0 -0
  55. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/models/recurring.py +0 -0
  56. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/models/runtime.py +0 -0
  57. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/__init__.py +0 -0
  58. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/_helpers.py +0 -0
  59. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/_insert.py +0 -0
  60. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/cleanup.py +0 -0
  61. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/queues.py +0 -0
  62. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/operations/recurring.py +0 -0
  63. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/queue_selectors.py +0 -0
  64. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/routers.py +0 -0
  65. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/__init__.py +0 -0
  66. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/connection_budget.py +0 -0
  67. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/errors.py +0 -0
  68. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/interruptible.py +0 -0
  69. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/notify.py +0 -0
  70. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/pidfile.py +0 -0
  71. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/pool.py +0 -0
  72. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/procline.py +0 -0
  73. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/scheduler.py +0 -0
  74. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/supervisor.py +0 -0
  75. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/runtime/topology.py +0 -0
  76. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/__init__.py +0 -0
  77. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/sqlite.py +0 -0
  78. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/sql/state.py +0 -0
  79. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/task_results.py +0 -0
  80. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  81. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  82. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  83. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  84. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  85. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  86. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  87. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  88. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  89. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  90. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  91. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  92. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templatetags/__init__.py +0 -0
  93. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  94. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/urls.py +0 -0
  95. {dj_queue-0.12.0 → dj_queue-0.13.0}/dj_queue/views.py +0 -0
  96. {dj_queue-0.12.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.12.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
@@ -839,7 +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 |
842
- | `prefetch_multiplier` | `2` | max claimed running/queued jobs per worker thread |
850
+ | `prefetch_multiplier` | `2` | upper bound on running plus queued claimed work; workers only poll when at least one thread is idle |
843
851
 
844
852
  Dispatcher entry options:
845
853
 
@@ -1020,6 +1028,10 @@ Exported metric families:
1020
1028
  - `dj_queue_recurring_tasks{backend}`
1021
1029
  - `dj_queue_semaphores{queue_database}`
1022
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}`
1023
1035
 
1024
1036
  Both endpoints support bearer token authentication. Set
1025
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
@@ -813,7 +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 |
816
- | `prefetch_multiplier` | `2` | max claimed running/queued jobs per worker thread |
824
+ | `prefetch_multiplier` | `2` | upper bound on running plus queued claimed work; workers only poll when at least one thread is idle |
817
825
 
818
826
  Dispatcher entry options:
819
827
 
@@ -994,6 +1002,10 @@ Exported metric families:
994
1002
  - `dj_queue_recurring_tasks{backend}`
995
1003
  - `dj_queue_semaphores{queue_database}`
996
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}`
997
1009
 
998
1010
  Both endpoints support bearer token authentication. Set
999
1011
  `DJ_QUEUE_OBSERVABILITY_TOKEN` in `settings.py` and include it as
@@ -716,14 +716,14 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
716
716
  class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
717
717
  backend_filter_field = "job__backend_alias"
718
718
  ignored_filter_params = ("job__backend_alias",)
719
- list_display = ("job", "exception_class", "message", "display_created_at")
719
+ list_display = ("job", "exception_class", "message", "display_retry_at", "display_created_at")
720
720
  list_filter = (
721
721
  ("job__queue_name", admin.AllValuesFieldListFilter),
722
722
  "exception_class",
723
723
  )
724
724
  list_select_related = ("job",)
725
725
  actions = ("retry_jobs", "discard_jobs")
726
- readonly_fields = ("job", "exception_class", "message", "traceback", "created_at")
726
+ readonly_fields = ("job", "exception_class", "message", "traceback", "retry_at", "created_at")
727
727
  search_fields = ("job__id", "job__task_path", "message", "exception_class")
728
728
 
729
729
  @admin.action(description="Retry selected failed jobs")
@@ -756,6 +756,10 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
756
756
  def display_created_at(self, obj):
757
757
  return _format_admin_datetime(obj.created_at)
758
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
+
759
763
  def get_change_actions(self, request, obj):
760
764
  if obj is None:
761
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
  ]
@@ -157,21 +157,26 @@ def load_backend_config(
157
157
 
158
158
  ensure_dj_queue_backend_alias(tasks_settings, backend_alias)
159
159
  backend_block = _backend_block(tasks_settings, backend_alias)
160
- 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)
161
161
  cache_key = (
162
162
  backend_alias,
163
- _cache_key(cli_overrides),
164
- _cache_key(env_values),
163
+ _EMPTY_CLI_CACHE_KEY if not cli_overrides else _cache_key(cli_overrides),
164
+ env_key,
165
165
  _cache_key(backend_block),
166
166
  )
167
- if cache_key not in _BACKEND_CONFIG_CACHE:
168
- _BACKEND_CONFIG_CACHE[cache_key] = _load_backend_config_uncached(
169
- backend_alias,
170
- cli_overrides,
171
- env_values,
172
- tasks_settings,
173
- )
174
- 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
175
180
 
176
181
 
177
182
  def load_allowed_queues(
@@ -776,3 +781,6 @@ def _cache_key(value: Any) -> str:
776
781
  return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
777
782
  except (TypeError, ValueError) as exc:
778
783
  raise ImproperlyConfigured("dj_queue config values must be JSON-serializable") from exc
784
+
785
+
786
+ _EMPTY_CLI_CACHE_KEY = _cache_key({})
@@ -32,17 +32,26 @@ def locked_queryset(qs, use_skip_locked: bool = True):
32
32
  return qs.select_for_update(**select_for_update_kwargs)
33
33
 
34
34
 
35
+ _CAPABILITIES_CACHE: dict[str, DatabaseCapabilities] = {}
36
+
37
+
35
38
  def database_capabilities(alias: str) -> DatabaseCapabilities:
39
+ cached = _CAPABILITIES_CACHE.get(alias)
40
+ if cached is not None:
41
+ return cached
42
+
36
43
  connection = connections[alias]
37
44
  backend_family = _backend_family(connection)
38
45
  supports_skip_locked_flag = bool(connection.features.has_select_for_update_skip_locked)
39
46
 
40
- return DatabaseCapabilities(
47
+ capabilities = DatabaseCapabilities(
41
48
  backend_family=backend_family,
42
49
  supports_skip_locked=supports_skip_locked_flag,
43
50
  supports_listen_notify=backend_family == "postgresql",
44
51
  uses_serialized_writes=backend_family == "sqlite",
45
52
  )
53
+ _CAPABILITIES_CACHE[alias] = capabilities
54
+ return capabilities
46
55
 
47
56
 
48
57
  def supports_skip_locked(alias: str) -> bool:
@@ -0,0 +1,26 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.core.management.base import CommandError
3
+ from django.db import connections
4
+
5
+ from dj_queue import observability
6
+ from dj_queue.config import load_backend_config
7
+ from dj_queue.db import database_capabilities
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = "Print PostgreSQL autovacuum storage-parameter SQL for dj_queue tables"
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument("--backend", default="default")
15
+
16
+ def handle(self, *args, **options):
17
+ backend_alias = options["backend"]
18
+ database_alias = load_backend_config(backend_alias).database_alias
19
+ if database_capabilities(database_alias).backend_family != "postgresql":
20
+ raise CommandError("dj_queue_postgres_autovacuum requires PostgreSQL")
21
+
22
+ connection = connections[database_alias]
23
+ self.stdout.write("-- dj_queue PostgreSQL autovacuum guidance")
24
+ self.stdout.write("-- review before applying; dj_queue does not apply these in migrations")
25
+ for statement in observability.postgres_autovacuum_sql(connection):
26
+ self.stdout.write(statement)
@@ -32,6 +32,10 @@ def metric_families(*, snapshots=None):
32
32
  recurring_tasks = []
33
33
  semaphores = []
34
34
  process_rows = []
35
+ failed_jobs = []
36
+ failed_oldest_age = []
37
+ failed_retention = []
38
+ failed_over_retention = []
35
39
  seen_queue_databases = set()
36
40
 
37
41
  for snapshot in snapshots:
@@ -95,6 +99,33 @@ def metric_families(*, snapshots=None):
95
99
  value=len(snapshot.process_rows),
96
100
  )
97
101
  )
102
+ if snapshot.failed_metrics is not None:
103
+ failed_jobs.append(
104
+ MetricSample(
105
+ labels=(backend_alias,),
106
+ value=snapshot.failed_metrics["count"],
107
+ )
108
+ )
109
+ if snapshot.failed_metrics["oldest_age_seconds"] is not None:
110
+ failed_oldest_age.append(
111
+ MetricSample(
112
+ labels=(backend_alias,),
113
+ value=snapshot.failed_metrics["oldest_age_seconds"],
114
+ )
115
+ )
116
+ if snapshot.failed_metrics["retention_seconds"] is not None:
117
+ failed_retention.append(
118
+ MetricSample(
119
+ labels=(backend_alias,),
120
+ value=snapshot.failed_metrics["retention_seconds"],
121
+ )
122
+ )
123
+ failed_over_retention.append(
124
+ MetricSample(
125
+ labels=(backend_alias,),
126
+ value=snapshot.failed_metrics["over_retention_count"],
127
+ )
128
+ )
98
129
 
99
130
  if queue_database_alias in seen_queue_databases:
100
131
  continue
@@ -170,4 +201,32 @@ def metric_families(*, snapshots=None):
170
201
  labels=("backend",),
171
202
  samples=tuple(process_rows),
172
203
  ),
204
+ MetricFamily(
205
+ name="dj_queue_failed_jobs",
206
+ help_text="Current failed job count by backend",
207
+ metric_type="gauge",
208
+ labels=("backend",),
209
+ samples=tuple(failed_jobs),
210
+ ),
211
+ MetricFamily(
212
+ name="dj_queue_failed_job_oldest_age_seconds",
213
+ help_text="Age of the oldest failed job by backend",
214
+ metric_type="gauge",
215
+ labels=("backend",),
216
+ samples=tuple(failed_oldest_age),
217
+ ),
218
+ MetricFamily(
219
+ name="dj_queue_failed_job_retention_seconds",
220
+ help_text="Configured failed-job retention window by backend",
221
+ metric_type="gauge",
222
+ labels=("backend",),
223
+ samples=tuple(failed_retention),
224
+ ),
225
+ MetricFamily(
226
+ name="dj_queue_failed_jobs_over_retention",
227
+ help_text="Current failed job count older than configured retention by backend",
228
+ metric_type="gauge",
229
+ labels=("backend",),
230
+ samples=tuple(failed_over_retention),
231
+ ),
173
232
  )
@@ -0,0 +1,21 @@
1
+ # Generated by Django 6.0.5 on 2026-06-06 09:11
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("dj_queue", "0012_job_djq_jobs_b_queue_id_idx_job_djq_jobs_b_conc_idx_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="failedexecution",
14
+ name="retry_at",
15
+ field=models.DateTimeField(blank=True, null=True),
16
+ ),
17
+ migrations.AddIndex(
18
+ model_name="failedexecution",
19
+ index=models.Index(fields=["retry_at", "job"], name="dj_queue_fa_retry_a_2ff769_idx"),
20
+ ),
21
+ ]
@@ -281,8 +281,12 @@ class FailedExecution(models.Model):
281
281
  exception_class = models.CharField(max_length=255)
282
282
  message = models.TextField(default="")
283
283
  traceback = models.TextField(default="")
284
+ retry_at = models.DateTimeField(null=True, blank=True)
284
285
  created_at = models.DateTimeField(auto_now_add=True)
285
286
 
286
287
  class Meta:
287
288
  db_table = "dj_queue_failed_executions"
288
- indexes = [models.Index(fields=["created_at", "job"])]
289
+ indexes = [
290
+ models.Index(fields=["created_at", "job"]),
291
+ models.Index(fields=["retry_at", "job"]),
292
+ ]