dj-queue 0.9.1__tar.gz → 0.10.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 (89) hide show
  1. {dj_queue-0.9.1 → dj_queue-0.10.0}/PKG-INFO +69 -22
  2. {dj_queue-0.9.1 → dj_queue-0.10.0}/README.md +68 -21
  3. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/admin.py +18 -24
  4. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/api.py +15 -12
  5. dj_queue-0.10.0/dj_queue/backend.py +93 -0
  6. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/config.py +94 -28
  7. dj_queue-0.10.0/dj_queue/contrib/asgi.py +148 -0
  8. dj_queue-0.10.0/dj_queue/contrib/gunicorn.py +154 -0
  9. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/dashboard.py +63 -13
  10. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/db.py +6 -2
  11. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/log.py +13 -2
  12. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/metrics.py +4 -3
  13. dj_queue-0.10.0/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +36 -0
  14. dj_queue-0.10.0/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +23 -0
  15. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/models/recurring.py +0 -12
  16. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/models/runtime.py +13 -2
  17. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/observability.py +15 -23
  18. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/_helpers.py +89 -1
  19. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/cleanup.py +24 -12
  20. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/concurrency.py +132 -34
  21. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/jobs.py +359 -192
  22. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/recurring.py +100 -29
  23. dj_queue-0.10.0/dj_queue/queue_selectors.py +62 -0
  24. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/queue_state.py +26 -6
  25. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/base.py +52 -20
  26. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/connection_budget.py +3 -2
  27. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/dispatcher.py +2 -0
  28. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/notify.py +5 -18
  29. dj_queue-0.10.0/dj_queue/runtime/pidfile.py +68 -0
  30. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/scheduler.py +2 -0
  31. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/supervisor.py +56 -18
  32. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/worker.py +4 -1
  33. dj_queue-0.10.0/dj_queue/task_results.py +128 -0
  34. dj_queue-0.10.0/dj_queue/wakeup.py +24 -0
  35. {dj_queue-0.9.1 → dj_queue-0.10.0}/pyproject.toml +2 -1
  36. dj_queue-0.9.1/dj_queue/backend.py +0 -170
  37. dj_queue-0.9.1/dj_queue/contrib/asgi.py +0 -47
  38. dj_queue-0.9.1/dj_queue/contrib/gunicorn.py +0 -45
  39. dj_queue-0.9.1/dj_queue/runtime/pidfile.py +0 -39
  40. {dj_queue-0.9.1 → dj_queue-0.10.0}/LICENSE +0 -0
  41. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/__init__.py +0 -0
  42. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/apps.py +0 -0
  43. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/contrib/__init__.py +0 -0
  44. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/contrib/prometheus.py +0 -0
  45. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/cron.py +0 -0
  46. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/exceptions.py +0 -0
  47. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/hooks.py +0 -0
  48. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/management/__init__.py +0 -0
  49. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/management/commands/__init__.py +0 -0
  50. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/management/commands/dj_queue.py +0 -0
  51. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/management/commands/dj_queue_health.py +0 -0
  52. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  53. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0001_initial.py +0 -0
  54. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  55. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  56. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0004_dashboard.py +0 -0
  57. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
  58. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
  59. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  60. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
  61. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/migrations/__init__.py +0 -0
  62. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/models/__init__.py +0 -0
  63. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/models/jobs.py +0 -0
  64. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/__init__.py +0 -0
  65. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/_insert.py +0 -0
  66. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/operations/queues.py +0 -0
  67. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/routers.py +0 -0
  68. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/__init__.py +0 -0
  69. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/errors.py +0 -0
  70. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/interruptible.py +0 -0
  71. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/pool.py +0 -0
  72. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/procline.py +0 -0
  73. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/runtime/topology.py +0 -0
  74. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  75. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  76. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  77. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  78. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  79. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  80. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  81. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  82. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  83. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  84. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  85. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  86. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templatetags/__init__.py +0 -0
  87. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  88. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/urls.py +0 -0
  89. {dj_queue-0.9.1 → dj_queue-0.10.0}/dj_queue/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.9.1
3
+ Version: 0.10.0
4
4
  Summary: Database-backed task queue backend for Django’s Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -713,9 +713,12 @@ from django.core.asgi import get_asgi_application
713
713
  from dj_queue.contrib.asgi import DjQueueLifespan
714
714
 
715
715
  django_application = get_asgi_application()
716
- application = DjQueueLifespan(django_application)
716
+ application = DjQueueLifespan(django_application, forward_wrapped_lifespan=False)
717
717
  ```
718
718
 
719
+ Set `forward_wrapped_lifespan=False` when the wrapped app does not implement the
720
+ ASGI lifespan protocol itself, such as Django's plain `get_asgi_application()`.
721
+
719
722
  ### Gunicorn
720
723
 
721
724
  Import the provided hooks in your Gunicorn config:
@@ -780,26 +783,70 @@ concurrency-maintenance throughput.
780
783
 
781
784
  The main configuration lives in `TASKS[backend_alias]["OPTIONS"]`.
782
785
 
783
- Start with these options:
784
-
785
- - `mode`: `"fork"` or `"async"`
786
- - `workers`: queue selectors, thread counts, and process counts
787
- - `dispatchers`: scheduled promotion and concurrency maintenance settings
788
- - `scheduler`: dynamic recurring polling settings
789
- - `database_alias`: database alias for queue tables and runtime activity
790
- - `preserve_finished_jobs` and `clear_finished_jobs_after`: successful result retention and cleanup
791
- - `clear_failed_jobs_after`: optional failed-job retention window
792
- - `clear_recurring_executions_after`: optional recurring reservation retention window
793
-
794
- Additional operational tuning is available when needed:
795
-
796
- - `use_skip_locked`: use `SKIP LOCKED` when the active backend supports it
797
- - `listen_notify`: PostgreSQL-only worker wakeup optimization layered on top of polling
798
- - `silence_polling`: suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger
799
- - `process_heartbeat_interval` and `process_alive_threshold`: process liveness reporting and stale-runner detection
800
- - `shutdown_timeout`: graceful drain window before standalone shutdown gives up on waiting
801
- - `supervisor_pidfile`: optional pidfile guard for standalone supervisors
802
- - `on_thread_error`: dotted callback path for runtime infrastructure exceptions
786
+ No top-level `OPTIONS` key is required. Omit a key to use its default. Static
787
+ `recurring` entries are the exception: each named recurring task requires
788
+ `task_path` and `schedule`.
789
+
790
+ Global options:
791
+
792
+ | Option | Default | Meaning |
793
+ |---|---|---|
794
+ | `mode` | `"fork"` | standalone runtime mode, either `"fork"` or `"async"` |
795
+ | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1}]` | worker topology and queue selectors |
796
+ | `dispatchers` | `[{"batch_size": 500, "polling_interval": 1, "concurrency_maintenance": true, "concurrency_maintenance_interval": 600}]` | scheduled promotion and concurrency maintenance |
797
+ | `scheduler` | `{"dynamic_tasks_enabled": false, "polling_interval": 5}` | dynamic recurring polling; the scheduler only starts when recurring or cleanup work exists |
798
+ | `recurring` | `{}` | static recurring task definitions keyed by name |
799
+ | `database_alias` | `"default"` | database alias for queue tables and runtime activity |
800
+ | `preserve_finished_jobs` | `true` | keep successful jobs for result lookup and retention cleanup |
801
+ | `clear_finished_jobs_after` | `86400` | seconds before finished successful jobs are cleaned up |
802
+ | `clear_failed_jobs_after` | `null` | optional failed-job retention window in seconds |
803
+ | `clear_recurring_executions_after` | `null` | optional recurring reservation retention window in seconds |
804
+ | `default_concurrency_duration` | `180` | default semaphore TTL in seconds |
805
+ | `use_skip_locked` | `true` | use `SKIP LOCKED` when the active backend supports it |
806
+ | `listen_notify` | `true` | PostgreSQL-only worker wakeup optimization layered on top of polling |
807
+ | `silence_polling` | `true` | suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger |
808
+ | `process_heartbeat_interval` | `60` | seconds between process heartbeat writes |
809
+ | `process_alive_threshold` | `300` | seconds before a process row is stale |
810
+ | `shutdown_timeout` | `5` | graceful drain window before standalone shutdown gives up on waiting |
811
+ | `supervisor_pidfile` | `null` | optional pidfile guard for standalone supervisors |
812
+ | `on_thread_error` | `null` | dotted callback path for runtime infrastructure exceptions |
813
+
814
+ Worker entry options:
815
+
816
+ | Option | Default | Meaning |
817
+ |---|---|---|
818
+ | `queues` | `"*"` | queue selector or selectors; `"*"` means all queues |
819
+ | `threads` | `3` | worker threads per worker process |
820
+ | `processes` | `1` | worker processes in `fork` mode; normalized to `1` in `async` mode |
821
+ | `polling_interval` | `0.1` | seconds between worker polls |
822
+
823
+ Dispatcher entry options:
824
+
825
+ | Option | Default | Meaning |
826
+ |---|---|---|
827
+ | `batch_size` | `500` | maximum scheduled or blocked rows to promote per batch |
828
+ | `polling_interval` | `1` | seconds between dispatcher polls |
829
+ | `concurrency_maintenance` | `true` | run expired semaphore and blocked-job maintenance |
830
+ | `concurrency_maintenance_interval` | `600` | seconds between maintenance passes; `0` means every poll |
831
+
832
+ Scheduler entry options:
833
+
834
+ | Option | Default | Meaning |
835
+ |---|---|---|
836
+ | `dynamic_tasks_enabled` | `false` | load dynamic recurring tasks from the database |
837
+ | `polling_interval` | `5` | seconds between scheduler polls |
838
+
839
+ Recurring entry options:
840
+
841
+ | Option | Default | Meaning |
842
+ |---|---|---|
843
+ | `task_path` | none | required dotted import path for the task to enqueue |
844
+ | `schedule` | none | required cron or supported Fugit-style cronish schedule |
845
+ | `args` | `[]` | positional arguments for the task |
846
+ | `kwargs` | `{}` | keyword arguments for the task |
847
+ | `queue_name` | `"default"` | queue used for jobs created from this recurring task |
848
+ | `priority` | `0` | priority used for jobs created from this recurring task |
849
+ | `description` | `""` | operator-facing description |
803
850
 
804
851
  On PostgreSQL, `listen_notify` uses the same Django PostgreSQL driver
805
852
  configuration as the main database connection. Install a compatible driver in
@@ -687,9 +687,12 @@ from django.core.asgi import get_asgi_application
687
687
  from dj_queue.contrib.asgi import DjQueueLifespan
688
688
 
689
689
  django_application = get_asgi_application()
690
- application = DjQueueLifespan(django_application)
690
+ application = DjQueueLifespan(django_application, forward_wrapped_lifespan=False)
691
691
  ```
692
692
 
693
+ Set `forward_wrapped_lifespan=False` when the wrapped app does not implement the
694
+ ASGI lifespan protocol itself, such as Django's plain `get_asgi_application()`.
695
+
693
696
  ### Gunicorn
694
697
 
695
698
  Import the provided hooks in your Gunicorn config:
@@ -754,26 +757,70 @@ concurrency-maintenance throughput.
754
757
 
755
758
  The main configuration lives in `TASKS[backend_alias]["OPTIONS"]`.
756
759
 
757
- Start with these options:
758
-
759
- - `mode`: `"fork"` or `"async"`
760
- - `workers`: queue selectors, thread counts, and process counts
761
- - `dispatchers`: scheduled promotion and concurrency maintenance settings
762
- - `scheduler`: dynamic recurring polling settings
763
- - `database_alias`: database alias for queue tables and runtime activity
764
- - `preserve_finished_jobs` and `clear_finished_jobs_after`: successful result retention and cleanup
765
- - `clear_failed_jobs_after`: optional failed-job retention window
766
- - `clear_recurring_executions_after`: optional recurring reservation retention window
767
-
768
- Additional operational tuning is available when needed:
769
-
770
- - `use_skip_locked`: use `SKIP LOCKED` when the active backend supports it
771
- - `listen_notify`: PostgreSQL-only worker wakeup optimization layered on top of polling
772
- - `silence_polling`: suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger
773
- - `process_heartbeat_interval` and `process_alive_threshold`: process liveness reporting and stale-runner detection
774
- - `shutdown_timeout`: graceful drain window before standalone shutdown gives up on waiting
775
- - `supervisor_pidfile`: optional pidfile guard for standalone supervisors
776
- - `on_thread_error`: dotted callback path for runtime infrastructure exceptions
760
+ No top-level `OPTIONS` key is required. Omit a key to use its default. Static
761
+ `recurring` entries are the exception: each named recurring task requires
762
+ `task_path` and `schedule`.
763
+
764
+ Global options:
765
+
766
+ | Option | Default | Meaning |
767
+ |---|---|---|
768
+ | `mode` | `"fork"` | standalone runtime mode, either `"fork"` or `"async"` |
769
+ | `workers` | `[{"queues": "*", "threads": 3, "processes": 1, "polling_interval": 0.1}]` | worker topology and queue selectors |
770
+ | `dispatchers` | `[{"batch_size": 500, "polling_interval": 1, "concurrency_maintenance": true, "concurrency_maintenance_interval": 600}]` | scheduled promotion and concurrency maintenance |
771
+ | `scheduler` | `{"dynamic_tasks_enabled": false, "polling_interval": 5}` | dynamic recurring polling; the scheduler only starts when recurring or cleanup work exists |
772
+ | `recurring` | `{}` | static recurring task definitions keyed by name |
773
+ | `database_alias` | `"default"` | database alias for queue tables and runtime activity |
774
+ | `preserve_finished_jobs` | `true` | keep successful jobs for result lookup and retention cleanup |
775
+ | `clear_finished_jobs_after` | `86400` | seconds before finished successful jobs are cleaned up |
776
+ | `clear_failed_jobs_after` | `null` | optional failed-job retention window in seconds |
777
+ | `clear_recurring_executions_after` | `null` | optional recurring reservation retention window in seconds |
778
+ | `default_concurrency_duration` | `180` | default semaphore TTL in seconds |
779
+ | `use_skip_locked` | `true` | use `SKIP LOCKED` when the active backend supports it |
780
+ | `listen_notify` | `true` | PostgreSQL-only worker wakeup optimization layered on top of polling |
781
+ | `silence_polling` | `true` | suppress `dj_queue`'s own poll-cycle noise without mutating Django's global SQL logger |
782
+ | `process_heartbeat_interval` | `60` | seconds between process heartbeat writes |
783
+ | `process_alive_threshold` | `300` | seconds before a process row is stale |
784
+ | `shutdown_timeout` | `5` | graceful drain window before standalone shutdown gives up on waiting |
785
+ | `supervisor_pidfile` | `null` | optional pidfile guard for standalone supervisors |
786
+ | `on_thread_error` | `null` | dotted callback path for runtime infrastructure exceptions |
787
+
788
+ Worker entry options:
789
+
790
+ | Option | Default | Meaning |
791
+ |---|---|---|
792
+ | `queues` | `"*"` | queue selector or selectors; `"*"` means all queues |
793
+ | `threads` | `3` | worker threads per worker process |
794
+ | `processes` | `1` | worker processes in `fork` mode; normalized to `1` in `async` mode |
795
+ | `polling_interval` | `0.1` | seconds between worker polls |
796
+
797
+ Dispatcher entry options:
798
+
799
+ | Option | Default | Meaning |
800
+ |---|---|---|
801
+ | `batch_size` | `500` | maximum scheduled or blocked rows to promote per batch |
802
+ | `polling_interval` | `1` | seconds between dispatcher polls |
803
+ | `concurrency_maintenance` | `true` | run expired semaphore and blocked-job maintenance |
804
+ | `concurrency_maintenance_interval` | `600` | seconds between maintenance passes; `0` means every poll |
805
+
806
+ Scheduler entry options:
807
+
808
+ | Option | Default | Meaning |
809
+ |---|---|---|
810
+ | `dynamic_tasks_enabled` | `false` | load dynamic recurring tasks from the database |
811
+ | `polling_interval` | `5` | seconds between scheduler polls |
812
+
813
+ Recurring entry options:
814
+
815
+ | Option | Default | Meaning |
816
+ |---|---|---|
817
+ | `task_path` | none | required dotted import path for the task to enqueue |
818
+ | `schedule` | none | required cron or supported Fugit-style cronish schedule |
819
+ | `args` | `[]` | positional arguments for the task |
820
+ | `kwargs` | `{}` | keyword arguments for the task |
821
+ | `queue_name` | `"default"` | queue used for jobs created from this recurring task |
822
+ | `priority` | `0` | priority used for jobs created from this recurring task |
823
+ | `description` | `""` | operator-facing description |
777
824
 
778
825
  On PostgreSQL, `listen_notify` uses the same Django PostgreSQL driver
779
826
  configuration as the main database connection. Install a compatible driver in
@@ -29,13 +29,19 @@ from dj_queue.models import (
29
29
  Semaphore,
30
30
  )
31
31
  from dj_queue.operations.jobs import (
32
+ DispatchOutcome,
32
33
  discard_failed_job,
33
34
  dispatch_scheduled_job_now,
34
35
  enqueue_job_again,
35
36
  retry_failed_job,
36
37
  retry_failed_jobs,
37
38
  )
38
- from dj_queue.queue_state import status_rank_expression
39
+ from dj_queue.queue_state import (
40
+ QUEUE_STATES,
41
+ filter_queue_state,
42
+ is_queue_state,
43
+ status_rank_expression,
44
+ )
39
45
 
40
46
 
41
47
  class DjQueueFirstAdminSite(admin.AdminSite):
@@ -328,29 +334,12 @@ class JobStatusListFilter(admin.SimpleListFilter):
328
334
  parameter_name = "status"
329
335
 
330
336
  def lookups(self, request, model_admin):
331
- return (
332
- ("ready", "ready"),
333
- ("scheduled", "scheduled"),
334
- ("claimed", "claimed"),
335
- ("blocked", "blocked"),
336
- ("failed", "failed"),
337
- ("finished", "finished"),
338
- )
337
+ return QUEUE_STATES
339
338
 
340
339
  def queryset(self, request, queryset):
341
340
  value = self.value()
342
- if value == "ready":
343
- return queryset.ready()
344
- if value == "scheduled":
345
- return queryset.scheduled()
346
- if value == "claimed":
347
- return queryset.claimed()
348
- if value == "blocked":
349
- return queryset.blocked()
350
- if value == "failed":
351
- return queryset.failed()
352
- if value == "finished":
353
- return queryset.finished()
341
+ if is_queue_state(value):
342
+ return filter_queue_state(queryset, value)
354
343
  return queryset
355
344
 
356
345
 
@@ -400,8 +389,10 @@ class JobConcurrencyKeyListFilter(admin.SimpleListFilter):
400
389
 
401
390
  def lookups(self, request, model_admin):
402
391
  alias = model_admin._backend_database_alias(request)
392
+ backend_alias = model_admin._backend_alias(request)
403
393
  return tuple(
404
394
  Job.objects.using(alias)
395
+ .filter(backend_alias=backend_alias)
405
396
  .exclude(concurrency_key__isnull=True)
406
397
  .exclude(concurrency_key="")
407
398
  .order_by("concurrency_key")
@@ -590,15 +581,17 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
590
581
  def handle_change_action(self, request, obj, action):
591
582
  if action == "run_now":
592
583
  try:
593
- _job, dispatched_as = dispatch_scheduled_job_now(obj.pk, backend_alias=obj.backend_alias)
584
+ _job, dispatch_outcome = dispatch_scheduled_job_now(
585
+ obj.pk, backend_alias=obj.backend_alias
586
+ )
594
587
  except (EnqueueError, ImportError, AttributeError) as exc:
595
588
  self.message_user(request, f"Could not dispatch job now: {exc}", level=messages.ERROR)
596
589
  return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
597
590
 
598
591
  message = "Dispatched scheduled job for immediate execution"
599
- if dispatched_as == "blocked":
592
+ if dispatch_outcome is DispatchOutcome.BLOCKED:
600
593
  message = "Dispatched scheduled job immediately and it is now blocked"
601
- if dispatched_as == "discarded":
594
+ if dispatch_outcome is DispatchOutcome.DISCARDED:
602
595
  message = "Dispatched scheduled job immediately and it was discarded"
603
596
  self.message_user(request, message, level=messages.SUCCESS)
604
597
  return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
@@ -722,6 +715,7 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
722
715
 
723
716
  @admin.register(Process)
724
717
  class ProcessAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
718
+ backend_filter_field = "backend_alias"
725
719
  list_display = (
726
720
  "name",
727
721
  "backend_alias",
@@ -1,9 +1,12 @@
1
+ from datetime import timedelta
1
2
  from functools import partial
2
3
 
3
4
  from django.db.models.functions import Coalesce
4
5
  from django.db import transaction
5
6
  from django.utils import timezone
6
7
 
8
+ from dj_queue import observability
9
+ from dj_queue.config import load_backend_config
7
10
  from dj_queue.db import get_database_alias
8
11
  from dj_queue.models import Pause, ReadyExecution
9
12
  from dj_queue.operations.jobs import (
@@ -50,6 +53,9 @@ class QueueInfo:
50
53
 
51
54
  @property
52
55
  def latency(self):
56
+ if self.paused:
57
+ return None
58
+
53
59
  oldest = (
54
60
  self._ready_queryset()
55
61
  .annotate(latency_at=Coalesce("latency_started_at", "created_at"))
@@ -59,7 +65,7 @@ class QueueInfo:
59
65
  )
60
66
  if oldest is None:
61
67
  return 0.0
62
- return (timezone.now() - oldest).total_seconds()
68
+ return max((timezone.now() - oldest).total_seconds(), 0.0)
63
69
 
64
70
  @property
65
71
  def paused(self):
@@ -93,18 +99,15 @@ class QueueInfo:
93
99
 
94
100
  @classmethod
95
101
  def all(cls, *, backend_alias="default"):
96
- alias = get_database_alias(backend_alias)
97
- queue_names = (
98
- ReadyExecution.objects.using(alias)
99
- .filter(backend_alias=backend_alias)
100
- .order_by("queue_name")
101
- .values_list(
102
- "queue_name",
103
- flat=True,
104
- )
105
- .distinct()
102
+ now = timezone.now()
103
+ config = load_backend_config(backend_alias)
104
+ process_cutoff = now - timedelta(seconds=config.process_alive_threshold)
105
+ queue_rows = observability.queue_rows(
106
+ backend_alias=backend_alias,
107
+ now=now,
108
+ process_cutoff=process_cutoff,
106
109
  )
107
- return [cls(queue_name, backend_alias=backend_alias) for queue_name in queue_names]
110
+ return [cls(row["name"], backend_alias=backend_alias) for row in queue_rows]
108
111
 
109
112
  def _ready_queryset(self):
110
113
  alias = get_database_alias(self.backend_alias)
@@ -0,0 +1,93 @@
1
+ from asgiref.sync import sync_to_async
2
+ from django.db import close_old_connections, connections
3
+ from django.tasks.backends.base import BaseTaskBackend
4
+ from django.tasks.exceptions import TaskResultDoesNotExist
5
+
6
+ from dj_queue.db import get_database_alias
7
+ from dj_queue.models import Job
8
+ from dj_queue.operations.jobs import (
9
+ DispatchOutcome,
10
+ enqueue_job_with_dispatch,
11
+ enqueue_jobs_bulk,
12
+ validate_priority,
13
+ validate_queue_allowed,
14
+ )
15
+ from dj_queue.task_results import task_result_from_enqueued_job, task_result_from_job
16
+
17
+
18
+ class DjQueueBackend(BaseTaskBackend):
19
+ supports_async_task = True
20
+ supports_defer = True
21
+ supports_get_result = True
22
+ supports_priority = True
23
+
24
+ def validate_task(self, task):
25
+ validate_queue_allowed(task.queue_name, backend_alias=self.alias)
26
+ validate_priority(task.priority)
27
+ return super().validate_task(task)
28
+
29
+ def enqueue(self, task, args, kwargs):
30
+ self.validate_task(task)
31
+ job, dispatch_outcome = enqueue_job_with_dispatch(task, args, kwargs, backend_alias=self.alias)
32
+ return task_result_from_enqueued_job(
33
+ job,
34
+ task,
35
+ successful=dispatch_outcome is DispatchOutcome.DISCARDED,
36
+ )
37
+
38
+ async def aenqueue(self, task, args, kwargs):
39
+ return await sync_to_async(_async_backend_call, thread_sensitive=True)(
40
+ self.enqueue,
41
+ task=task,
42
+ args=args,
43
+ kwargs=kwargs,
44
+ )
45
+
46
+ def enqueue_all(self, task_calls):
47
+ jobs = []
48
+ for task, args, kwargs in task_calls:
49
+ self.validate_task(task)
50
+ jobs.append((task, args, kwargs))
51
+
52
+ created_jobs = enqueue_jobs_bulk(jobs, backend_alias=self.alias)
53
+ return [
54
+ task_result_from_enqueued_job(
55
+ job,
56
+ task,
57
+ successful=dispatch_outcome is DispatchOutcome.DISCARDED,
58
+ )
59
+ for job, task, dispatch_outcome in created_jobs
60
+ ]
61
+
62
+ def get_result(self, result_id):
63
+ alias = get_database_alias(self.alias)
64
+ try:
65
+ job = (
66
+ Job.objects.using(alias)
67
+ .select_related(
68
+ "ready_execution",
69
+ "scheduled_execution",
70
+ "claimed_execution__process",
71
+ "blocked_execution",
72
+ "failed_execution",
73
+ )
74
+ .get(pk=result_id, backend_alias=self.alias)
75
+ )
76
+ except Job.DoesNotExist as exc:
77
+ raise TaskResultDoesNotExist(str(result_id)) from exc
78
+
79
+ return task_result_from_job(job)
80
+
81
+ async def aget_result(self, result_id):
82
+ return await sync_to_async(_async_backend_call, thread_sensitive=True)(
83
+ self.get_result,
84
+ result_id=result_id,
85
+ )
86
+
87
+
88
+ def _async_backend_call(method, /, **kwargs):
89
+ close_old_connections()
90
+ try:
91
+ return method(**kwargs)
92
+ finally:
93
+ connections.close_all()