dj-queue 0.10.5__tar.gz → 0.10.6__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 (87) hide show
  1. {dj_queue-0.10.5 → dj_queue-0.10.6}/PKG-INFO +1 -1
  2. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/config.py +3 -1
  3. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/asgi.py +6 -1
  4. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/gunicorn.py +27 -5
  5. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/dashboard.py +6 -6
  6. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/metrics.py +7 -7
  7. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/observability.py +57 -139
  8. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/_helpers.py +62 -10
  9. dj_queue-0.10.6/dj_queue/operations/_insert.py +36 -0
  10. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/cleanup.py +11 -5
  11. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/concurrency.py +49 -24
  12. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/jobs.py +73 -60
  13. dj_queue-0.10.6/dj_queue/queue_state.py +270 -0
  14. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/base.py +7 -1
  15. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/errors.py +0 -1
  16. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/notify.py +2 -1
  17. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/supervisor.py +5 -3
  18. {dj_queue-0.10.5 → dj_queue-0.10.6}/pyproject.toml +1 -1
  19. dj_queue-0.10.5/dj_queue/operations/_insert.py +0 -24
  20. dj_queue-0.10.5/dj_queue/queue_state.py +0 -138
  21. {dj_queue-0.10.5 → dj_queue-0.10.6}/LICENSE +0 -0
  22. {dj_queue-0.10.5 → dj_queue-0.10.6}/README.md +0 -0
  23. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/__init__.py +0 -0
  24. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/admin.py +0 -0
  25. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/api.py +0 -0
  26. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/apps.py +0 -0
  27. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/backend.py +0 -0
  28. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/__init__.py +0 -0
  29. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/prometheus.py +0 -0
  30. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/cron.py +0 -0
  31. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/db.py +0 -0
  32. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/exceptions.py +0 -0
  33. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/hooks.py +0 -0
  34. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/log.py +0 -0
  35. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/__init__.py +0 -0
  36. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/__init__.py +0 -0
  37. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue.py +0 -0
  38. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_health.py +0 -0
  39. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  40. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0001_initial.py +0 -0
  41. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  42. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  43. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0004_dashboard.py +0 -0
  44. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
  45. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
  46. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  47. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
  48. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
  49. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
  50. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/__init__.py +0 -0
  51. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/__init__.py +0 -0
  52. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/jobs.py +0 -0
  53. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/recurring.py +0 -0
  54. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/runtime.py +0 -0
  55. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/__init__.py +0 -0
  56. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/queues.py +0 -0
  57. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/recurring.py +0 -0
  58. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/queue_selectors.py +0 -0
  59. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/routers.py +0 -0
  60. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/__init__.py +0 -0
  61. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/connection_budget.py +0 -0
  62. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/dispatcher.py +0 -0
  63. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/interruptible.py +0 -0
  64. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/pidfile.py +0 -0
  65. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/pool.py +0 -0
  66. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/procline.py +0 -0
  67. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/scheduler.py +0 -0
  68. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/topology.py +0 -0
  69. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/worker.py +0 -0
  70. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/task_results.py +0 -0
  71. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  72. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  73. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  74. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  75. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  76. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  77. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  78. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  79. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  80. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  81. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  82. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  83. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templatetags/__init__.py +0 -0
  84. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  85. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/urls.py +0 -0
  86. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/views.py +0 -0
  87. {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/wakeup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.10.5
3
+ Version: 0.10.6
4
4
  Summary: Database-backed task queue backend for Django’s Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -149,12 +149,14 @@ def load_backend_config(
149
149
  if tasks_settings is None:
150
150
  tasks_settings = getattr(settings, "TASKS", {})
151
151
 
152
+ ensure_dj_queue_backend_alias(tasks_settings, backend_alias)
153
+ backend_block = _backend_block(tasks_settings, backend_alias)
152
154
  env_values = {key: env.get(key) for key in CONFIG_ENV_KEYS if env.get(key) is not None}
153
155
  cache_key = (
154
156
  backend_alias,
155
157
  _cache_key(cli_overrides),
156
158
  _cache_key(env_values),
157
- _cache_key(tasks_settings),
159
+ _cache_key(backend_block),
158
160
  )
159
161
  if cache_key not in _BACKEND_CONFIG_CACHE:
160
162
  _BACKEND_CONFIG_CACHE[cache_key] = _load_backend_config_uncached(
@@ -24,13 +24,18 @@ class DjQueueLifespan:
24
24
  self.supervisor is not None and self._poll_stop is not None and not self._poll_stop.is_set()
25
25
  ):
26
26
  try:
27
- await asyncio.to_thread(self.supervisor.poll_once)
27
+ poll_once = getattr(self.supervisor, "poll_once_if_running", self.supervisor.poll_once)
28
+ keep_polling = await asyncio.to_thread(poll_once)
28
29
  except Exception as error:
29
30
  handle_thread_error(
30
31
  error,
31
32
  context="supervisor.run",
32
33
  backend_alias=self.supervisor.backend_alias,
33
34
  )
35
+ keep_polling = True
36
+
37
+ if keep_polling is False:
38
+ return
34
39
 
35
40
  if self.supervisor is None or self._poll_stop is None or self._poll_stop.is_set():
36
41
  return
@@ -21,11 +21,17 @@ def _set_supervisor_state(worker, **state):
21
21
 
22
22
 
23
23
  def _start_embedded_supervisor(worker, *, backend_alias="default"):
24
+ if getattr(worker, "_dj_queue_supervisor_exiting", False):
25
+ return None
26
+
24
27
  lock_file = _try_acquire_supervisor_lock(backend_alias=backend_alias)
25
28
  if lock_file is None:
26
29
  return None
27
30
 
28
31
  try:
32
+ if getattr(worker, "_dj_queue_supervisor_exiting", False):
33
+ _release_supervisor_lock(lock_file)
34
+ return None
29
35
  supervisor = build_supervisor(backend_alias=backend_alias)
30
36
  poll_stop = threading.Event()
31
37
  _set_supervisor_state(
@@ -35,6 +41,17 @@ def _start_embedded_supervisor(worker, *, backend_alias="default"):
35
41
  supervisor_poll_stop=poll_stop,
36
42
  )
37
43
  supervisor.start()
44
+ if getattr(worker, "_dj_queue_supervisor_exiting", False):
45
+ supervisor.stop()
46
+ _release_supervisor_lock(lock_file)
47
+ _set_supervisor_state(
48
+ worker,
49
+ supervisor_lock=None,
50
+ supervisor=None,
51
+ supervisor_poll_stop=None,
52
+ supervisor_poll_thread=None,
53
+ )
54
+ return None
38
55
  except Exception:
39
56
  _release_supervisor_lock(lock_file)
40
57
  _set_supervisor_state(
@@ -50,7 +67,9 @@ def _start_embedded_supervisor(worker, *, backend_alias="default"):
50
67
  stop_event = worker._dj_queue_supervisor_poll_stop
51
68
  while stop_event.wait(supervisor.polling_interval) is False:
52
69
  try:
53
- supervisor.poll_once()
70
+ poll_once = getattr(supervisor, "poll_once_if_running", supervisor.poll_once)
71
+ if poll_once() is False:
72
+ return
54
73
  except Exception as error:
55
74
  handle_thread_error(
56
75
  error,
@@ -97,6 +116,7 @@ def post_fork(server, worker):
97
116
  supervisor_poll_thread=None,
98
117
  supervisor_retry_stop=None,
99
118
  supervisor_retry_thread=None,
119
+ supervisor_exiting=False,
100
120
  )
101
121
  supervisor = _start_embedded_supervisor(worker, backend_alias=backend_alias)
102
122
  if supervisor is not None:
@@ -107,10 +127,7 @@ def post_fork(server, worker):
107
127
 
108
128
 
109
129
  def worker_exit(_server, worker):
110
- supervisor = getattr(worker, "_dj_queue_supervisor", None)
111
- lock_file = getattr(worker, "_dj_queue_supervisor_lock", None)
112
- stop_event = getattr(worker, "_dj_queue_supervisor_poll_stop", None)
113
- poll_thread = getattr(worker, "_dj_queue_supervisor_poll_thread", None)
130
+ worker._dj_queue_supervisor_exiting = True
114
131
  retry_stop = getattr(worker, "_dj_queue_supervisor_retry_stop", None)
115
132
  retry_thread = getattr(worker, "_dj_queue_supervisor_retry_thread", None)
116
133
 
@@ -121,6 +138,11 @@ def worker_exit(_server, worker):
121
138
  worker._dj_queue_supervisor_retry_thread = None
122
139
  worker._dj_queue_supervisor_retry_stop = None
123
140
 
141
+ supervisor = getattr(worker, "_dj_queue_supervisor", None)
142
+ lock_file = getattr(worker, "_dj_queue_supervisor_lock", None)
143
+ stop_event = getattr(worker, "_dj_queue_supervisor_poll_stop", None)
144
+ poll_thread = getattr(worker, "_dj_queue_supervisor_poll_thread", None)
145
+
124
146
  if stop_event is not None:
125
147
  stop_event.set()
126
148
  if poll_thread is not None:
@@ -268,8 +268,8 @@ def dashboard_context(*, backend_alias, query_params=None):
268
268
  query_params = {}
269
269
 
270
270
  snapshot = observability.backend_snapshot(backend_alias=backend_alias)
271
- queue_rows = snapshot["queue_rows"]
272
- process_rows = snapshot["process_rows"]
271
+ queue_rows = snapshot.queue_rows
272
+ process_rows = snapshot.process_rows
273
273
  recurring_rows = [
274
274
  {
275
275
  **row,
@@ -278,7 +278,7 @@ def dashboard_context(*, backend_alias, query_params=None):
278
278
  recurring_task_key=row["key"],
279
279
  ),
280
280
  }
281
- for row in snapshot["recurring_rows"]
281
+ for row in snapshot.recurring_rows
282
282
  ]
283
283
  semaphore_rows = [
284
284
  {
@@ -288,14 +288,14 @@ def dashboard_context(*, backend_alias, query_params=None):
288
288
  concurrency_key=row["key"],
289
289
  ),
290
290
  }
291
- for row in snapshot["semaphore_rows"]
291
+ for row in snapshot.semaphore_rows
292
292
  ]
293
293
 
294
294
  return {
295
295
  "backend_alias": backend_alias,
296
296
  "backend_choices": backend_choices(),
297
297
  "config": config,
298
- "queue_database_alias": snapshot["queue_database_alias"],
298
+ "queue_database_alias": snapshot.queue_database_alias,
299
299
  "summary_cards": _summary_cards(
300
300
  backend_alias=backend_alias,
301
301
  queue_rows=queue_rows,
@@ -305,7 +305,7 @@ def dashboard_context(*, backend_alias, query_params=None):
305
305
  ),
306
306
  "backend_facts": _backend_facts(
307
307
  config=config,
308
- queue_database_alias=snapshot["queue_database_alias"],
308
+ queue_database_alias=snapshot.queue_database_alias,
309
309
  recurring_count=len(recurring_rows),
310
310
  semaphore_count=len(semaphore_rows),
311
311
  ),
@@ -35,11 +35,11 @@ def metric_families(*, snapshots=None):
35
35
  seen_queue_databases = set()
36
36
 
37
37
  for snapshot in snapshots:
38
- backend_alias = snapshot["backend_alias"]
39
- queue_database_alias = snapshot["queue_database_alias"]
40
- runner_metrics = snapshot["runner_metrics"]
38
+ backend_alias = snapshot.backend_alias
39
+ queue_database_alias = snapshot.queue_database_alias
40
+ runner_metrics = snapshot.runner_metrics
41
41
 
42
- for queue in snapshot["queue_rows"]:
42
+ for queue in snapshot.queue_rows:
43
43
  for definition in QUEUE_STATE_DEFINITIONS:
44
44
  queue_jobs.append(
45
45
  MetricSample(
@@ -86,13 +86,13 @@ def metric_families(*, snapshots=None):
86
86
  recurring_tasks.append(
87
87
  MetricSample(
88
88
  labels=(backend_alias,),
89
- value=len(snapshot["recurring_rows"]),
89
+ value=len(snapshot.recurring_rows),
90
90
  )
91
91
  )
92
92
  process_rows.append(
93
93
  MetricSample(
94
94
  labels=(backend_alias,),
95
- value=len(snapshot["process_rows"]),
95
+ value=len(snapshot.process_rows),
96
96
  )
97
97
  )
98
98
 
@@ -102,7 +102,7 @@ def metric_families(*, snapshots=None):
102
102
  semaphores.append(
103
103
  MetricSample(
104
104
  labels=(queue_database_alias,),
105
- value=len(snapshot["semaphore_rows"]),
105
+ value=len(snapshot.semaphore_rows),
106
106
  )
107
107
  )
108
108
 
@@ -4,8 +4,7 @@ from dataclasses import dataclass
4
4
  from datetime import timedelta
5
5
 
6
6
  from django.conf import settings
7
- from django.db.models import Count, Max, Min
8
- from django.db.models.functions import Coalesce
7
+ from django.db.models import Count, Max
9
8
  from django.utils import timezone
10
9
 
11
10
  from dj_queue.config import configured_backend_aliases as configured_dj_queue_backend_aliases
@@ -14,19 +13,18 @@ from dj_queue.cron import next_cron_run
14
13
  from dj_queue.db import get_database_alias
15
14
  from dj_queue.models import (
16
15
  BlockedExecution,
17
- ClaimedExecution,
18
- FailedExecution,
19
- Job,
20
16
  Pause,
21
17
  Process,
22
- ReadyExecution,
23
18
  RecurringExecution,
24
19
  RecurringTask,
25
- ScheduledExecution,
26
20
  Semaphore,
27
21
  )
28
22
  from dj_queue.queue_selectors import queue_matches_selectors
29
- from dj_queue.queue_state import queue_state_count_fields, queue_state_counts
23
+ from dj_queue.queue_state import (
24
+ empty_queue_state_summary,
25
+ queue_state_summaries_by_queue,
26
+ queue_state_summary,
27
+ )
30
28
 
31
29
 
32
30
  _NOT_PROVIDED = object()
@@ -38,6 +36,35 @@ class BackendChoice:
38
36
  database_alias: str
39
37
 
40
38
 
39
+ @dataclass(frozen=True, slots=True)
40
+ class BackendSnapshot:
41
+ backend_alias: str
42
+ queue_database_alias: str
43
+ process_alive_threshold: int
44
+ queue_rows: tuple[dict, ...]
45
+ process_rows: tuple[dict, ...]
46
+ recurring_rows: tuple[dict, ...]
47
+ semaphore_rows: tuple[dict, ...]
48
+ runner_metrics: dict
49
+
50
+ def __getitem__(self, key):
51
+ try:
52
+ return getattr(self, key)
53
+ except AttributeError as exc:
54
+ raise KeyError(key) from exc
55
+
56
+ def stats_row(self):
57
+ return {
58
+ "backend_alias": self.backend_alias,
59
+ "queue_database_alias": self.queue_database_alias,
60
+ "process_alive_threshold": self.process_alive_threshold,
61
+ "queues": self.queue_rows,
62
+ "runner_metrics": self.runner_metrics,
63
+ "recurring": self.recurring_rows,
64
+ "semaphores": self.semaphore_rows,
65
+ }
66
+
67
+
41
68
  def configured_backend_aliases():
42
69
  return configured_dj_queue_backend_aliases(getattr(settings, "TASKS", {}))
43
70
 
@@ -70,16 +97,16 @@ def backend_snapshot(*, backend_alias, now=None):
70
97
  semaphore_rows = semaphore_rows_for_backend(backend_alias=backend_alias)
71
98
  runner_metrics = process_counts(backend_process_rows)
72
99
 
73
- return {
74
- "backend_alias": backend_alias,
75
- "queue_database_alias": queue_database_alias,
76
- "process_alive_threshold": config.process_alive_threshold,
77
- "queue_rows": queue_state_rows,
78
- "process_rows": backend_process_rows,
79
- "recurring_rows": recurring_rows,
80
- "semaphore_rows": semaphore_rows,
81
- "runner_metrics": runner_metrics,
82
- }
100
+ return BackendSnapshot(
101
+ backend_alias=backend_alias,
102
+ queue_database_alias=queue_database_alias,
103
+ process_alive_threshold=config.process_alive_threshold,
104
+ queue_rows=tuple(queue_state_rows),
105
+ process_rows=tuple(backend_process_rows),
106
+ recurring_rows=tuple(recurring_rows),
107
+ semaphore_rows=tuple(semaphore_rows),
108
+ runner_metrics=runner_metrics,
109
+ )
83
110
 
84
111
 
85
112
  def all_backend_snapshots(*, now=None):
@@ -90,20 +117,7 @@ def all_backend_snapshots(*, now=None):
90
117
 
91
118
  def stats_payload(*, now=None):
92
119
  snapshots = all_backend_snapshots(now=now)
93
- return {
94
- "backends": [
95
- {
96
- "backend_alias": snapshot["backend_alias"],
97
- "queue_database_alias": snapshot["queue_database_alias"],
98
- "process_alive_threshold": snapshot["process_alive_threshold"],
99
- "queues": snapshot["queue_rows"],
100
- "runner_metrics": snapshot["runner_metrics"],
101
- "recurring": snapshot["recurring_rows"],
102
- "semaphores": snapshot["semaphore_rows"],
103
- }
104
- for snapshot in snapshots
105
- ]
106
- }
120
+ return {"backends": [snapshot.stats_row() for snapshot in snapshots]}
107
121
 
108
122
 
109
123
  def process_counts(process_rows):
@@ -123,32 +137,8 @@ def process_counts(process_rows):
123
137
 
124
138
  def queue_rows(*, backend_alias, now, process_cutoff):
125
139
  alias = get_database_alias(backend_alias)
126
- queue_names = set()
127
-
128
- ready_counts = _counts_by_value(
129
- ReadyExecution.objects.using(alias).filter(backend_alias=backend_alias),
130
- field_name="queue_name",
131
- )
132
- claimed_counts = _counts_by_value(
133
- ClaimedExecution.objects.using(alias).filter(job__backend_alias=backend_alias),
134
- field_name="job__queue_name",
135
- )
136
- scheduled_counts = _counts_by_value(
137
- ScheduledExecution.objects.using(alias).filter(backend_alias=backend_alias),
138
- field_name="queue_name",
139
- )
140
- blocked_counts = _counts_by_value(
141
- BlockedExecution.objects.using(alias).filter(backend_alias=backend_alias),
142
- field_name="queue_name",
143
- )
144
- failed_counts = _counts_by_value(
145
- FailedExecution.objects.using(alias).filter(job__backend_alias=backend_alias),
146
- field_name="job__queue_name",
147
- )
148
- finished_counts = _counts_by_value(
149
- Job.objects.using(alias).filter(backend_alias=backend_alias, finished_at__isnull=False),
150
- field_name="queue_name",
151
- )
140
+ state_summaries = queue_state_summaries_by_queue(backend_alias=backend_alias)
141
+ queue_names = set(state_summaries)
152
142
  paused_queues = set(
153
143
  Pause.objects.using(alias)
154
144
  .filter(backend_alias=backend_alias)
@@ -160,40 +150,12 @@ def queue_rows(*, backend_alias, now, process_cutoff):
160
150
  .values_list("queue_name", flat=True)
161
151
  )
162
152
 
163
- oldest_ready = {
164
- row["queue_name"]: row["oldest"]
165
- for row in ReadyExecution.objects.using(alias)
166
- .filter(backend_alias=backend_alias)
167
- .values("queue_name")
168
- .annotate(oldest=Min(Coalesce("latency_started_at", "created_at")))
169
- }
170
- oldest_scheduled = {
171
- row["queue_name"]: row["oldest"]
172
- for row in ScheduledExecution.objects.using(alias)
173
- .filter(backend_alias=backend_alias)
174
- .values("queue_name")
175
- .annotate(oldest=Min("scheduled_at"))
176
- }
177
- oldest_blocked = {
178
- row["queue_name"]: row["oldest"]
179
- for row in BlockedExecution.objects.using(alias)
180
- .filter(backend_alias=backend_alias)
181
- .values("queue_name")
182
- .annotate(oldest=Min("expires_at"))
183
- }
184
-
185
153
  live_workers = list(
186
154
  _live_processes_for_backend(
187
155
  alias=alias, backend_alias=backend_alias, kind="Worker", process_cutoff=process_cutoff
188
156
  )
189
157
  )
190
158
 
191
- queue_names.update(ready_counts)
192
- queue_names.update(claimed_counts)
193
- queue_names.update(scheduled_counts)
194
- queue_names.update(blocked_counts)
195
- queue_names.update(failed_counts)
196
- queue_names.update(finished_counts)
197
159
  queue_names.update(paused_queues)
198
160
  queue_names.update(recurring_queues)
199
161
 
@@ -203,16 +165,8 @@ def queue_rows(*, backend_alias, now, process_cutoff):
203
165
  queue_name=queue_name,
204
166
  now=now,
205
167
  process_cutoff=process_cutoff,
206
- ready_count=ready_counts.get(queue_name, 0),
207
- claimed_count=claimed_counts.get(queue_name, 0),
208
- scheduled_count=scheduled_counts.get(queue_name, 0),
209
- blocked_count=blocked_counts.get(queue_name, 0),
210
- failed_count=failed_counts.get(queue_name, 0),
211
- finished_count=finished_counts.get(queue_name, 0),
168
+ state_summary=state_summaries.get(queue_name) or empty_queue_state_summary(queue_name),
212
169
  paused=queue_name in paused_queues,
213
- oldest_ready_at=oldest_ready.get(queue_name),
214
- oldest_scheduled_at=oldest_scheduled.get(queue_name),
215
- oldest_blocked_at=oldest_blocked.get(queue_name),
216
170
  live_workers=live_workers,
217
171
  )
218
172
  for queue_name in sorted(queue_names)
@@ -225,12 +179,7 @@ def queue_snapshot(
225
179
  queue_name,
226
180
  now,
227
181
  process_cutoff,
228
- ready_count=None,
229
- claimed_count=None,
230
- scheduled_count=None,
231
- blocked_count=None,
232
- failed_count=None,
233
- finished_count=None,
182
+ state_summary=None,
234
183
  paused=_NOT_PROVIDED,
235
184
  oldest_ready_at=_NOT_PROVIDED,
236
185
  oldest_scheduled_at=_NOT_PROVIDED,
@@ -238,33 +187,16 @@ def queue_snapshot(
238
187
  live_workers=None,
239
188
  ):
240
189
  alias = get_database_alias(backend_alias)
241
- if ready_count is None:
242
- state_counts = queue_state_counts(backend_alias=backend_alias, queue_name=queue_name)
243
- ready_count = state_counts["ready"]
244
- claimed_count = state_counts["claimed"]
245
- scheduled_count = state_counts["scheduled"]
246
- blocked_count = state_counts["blocked"]
247
- failed_count = state_counts["failed"]
248
- finished_count = state_counts["finished"]
190
+ if state_summary is None:
191
+ state_summary = queue_state_summary(backend_alias=backend_alias, queue_name=queue_name)
249
192
  if paused is _NOT_PROVIDED:
250
193
  paused = queue_is_paused(backend_alias=backend_alias, queue_name=queue_name)
251
194
  if oldest_ready_at is _NOT_PROVIDED:
252
- oldest_ready_at = oldest_ready_at_for_queue(
253
- backend_alias=backend_alias,
254
- queue_name=queue_name,
255
- )
195
+ oldest_ready_at = state_summary.oldest_ready_at
256
196
  if oldest_scheduled_at is _NOT_PROVIDED:
257
- oldest_scheduled_at = (
258
- ScheduledExecution.objects.using(alias)
259
- .filter(backend_alias=backend_alias, queue_name=queue_name)
260
- .aggregate(oldest=Min("scheduled_at"))["oldest"]
261
- )
197
+ oldest_scheduled_at = state_summary.oldest_scheduled_at
262
198
  if oldest_blocked_at is _NOT_PROVIDED:
263
- oldest_blocked_at = (
264
- BlockedExecution.objects.using(alias)
265
- .filter(backend_alias=backend_alias, queue_name=queue_name)
266
- .aggregate(oldest=Min("expires_at"))["oldest"]
267
- )
199
+ oldest_blocked_at = state_summary.oldest_blocked_at
268
200
  if live_workers is None:
269
201
  live_workers = list(
270
202
  _live_processes_for_backend(
@@ -283,16 +215,7 @@ def queue_snapshot(
283
215
  oldest_ready_at=oldest_ready_at,
284
216
  )
285
217
 
286
- state_count_fields = queue_state_count_fields(
287
- {
288
- "ready": ready_count,
289
- "claimed": claimed_count,
290
- "scheduled": scheduled_count,
291
- "blocked": blocked_count,
292
- "failed": failed_count,
293
- "finished": finished_count,
294
- }
295
- )
218
+ state_count_fields = state_summary.count_fields()
296
219
 
297
220
  return {
298
221
  "name": queue_name,
@@ -341,12 +264,7 @@ def queue_latency_seconds(
341
264
 
342
265
 
343
266
  def oldest_ready_at_for_queue(*, backend_alias, queue_name):
344
- alias = get_database_alias(backend_alias)
345
- return (
346
- ReadyExecution.objects.using(alias)
347
- .filter(backend_alias=backend_alias, queue_name=queue_name)
348
- .aggregate(oldest=Min(Coalesce("latency_started_at", "created_at")))["oldest"]
349
- )
267
+ return queue_state_summary(backend_alias=backend_alias, queue_name=queue_name).oldest_ready_at
350
268
 
351
269
 
352
270
  def process_rows(*, backend_alias, now, process_cutoff, scope):
@@ -14,12 +14,22 @@ from dj_queue.models import (
14
14
  ScheduledExecution,
15
15
  )
16
16
 
17
+ EXECUTION_STATE_MODELS = (
18
+ ReadyExecution,
19
+ ScheduledExecution,
20
+ ClaimedExecution,
21
+ BlockedExecution,
22
+ FailedExecution,
23
+ )
17
24
  STATE_RELATIONS = {
18
- ReadyExecution: "ready_execution",
19
- ScheduledExecution: "scheduled_execution",
20
- ClaimedExecution: "claimed_execution",
21
- BlockedExecution: "blocked_execution",
22
- FailedExecution: "failed_execution",
25
+ model: relation_name
26
+ for model, relation_name in (
27
+ (ReadyExecution, "ready_execution"),
28
+ (ScheduledExecution, "scheduled_execution"),
29
+ (ClaimedExecution, "claimed_execution"),
30
+ (BlockedExecution, "blocked_execution"),
31
+ (FailedExecution, "failed_execution"),
32
+ )
23
33
  }
24
34
 
25
35
 
@@ -31,8 +41,22 @@ def _normalize_payload(args, kwargs):
31
41
 
32
42
 
33
43
  def _ensure_no_other_execution_state(alias, job, *, ignored_models=()):
34
- if _job_ids_with_other_execution_state(alias, [job.pk], ignored_models=ignored_models):
35
- raise EnqueueError(f"job {job.id} already has an execution-state row")
44
+ _ensure_job_ids_have_no_other_execution_state(
45
+ alias,
46
+ [job.pk],
47
+ ignored_models=ignored_models,
48
+ )
49
+
50
+
51
+ def _ensure_job_ids_have_no_other_execution_state(alias, job_ids, *, ignored_models=()):
52
+ conflicting_job_ids = _job_ids_with_other_execution_state(
53
+ alias,
54
+ job_ids,
55
+ ignored_models=ignored_models,
56
+ )
57
+ if conflicting_job_ids:
58
+ conflicting_job_id = next(iter(conflicting_job_ids))
59
+ raise EnqueueError(f"job {conflicting_job_id} already has an execution-state row")
36
60
 
37
61
 
38
62
  def _job_ids_with_other_execution_state(alias, job_ids, *, ignored_models=()):
@@ -54,6 +78,34 @@ def _job_ids_with_other_execution_state(alias, job_ids, *, ignored_models=()):
54
78
  )
55
79
 
56
80
 
81
+ def _ensure_state_rows_belong_to_backend(rows, backend_alias):
82
+ for row in rows:
83
+ if row.job.backend_alias != backend_alias:
84
+ raise EnqueueError(f"job {row.job_id} belongs to backend {row.job.backend_alias!r}")
85
+
86
+
87
+ def _state_models_except(*ignored_models):
88
+ ignored = set(ignored_models)
89
+ return tuple(model for model in EXECUTION_STATE_MODELS if model not in ignored)
90
+
91
+
92
+ def _state_absence_checks_sql(models, *, quote, job_id_expression):
93
+ return " AND ".join(
94
+ _state_absence_sql(model, quote=quote, job_id_expression=job_id_expression) for model in models
95
+ )
96
+
97
+
98
+ def _state_absence_sql(model, *, quote, job_id_expression):
99
+ state_table = quote(model._meta.db_table)
100
+ state_job_id_column = quote(model._meta.get_field("job").column)
101
+ return (
102
+ f"NOT EXISTS ("
103
+ f"SELECT 1 FROM {state_table} "
104
+ f"WHERE {state_table}.{state_job_id_column} = {job_id_expression}"
105
+ f")"
106
+ )
107
+
108
+
57
109
  def _task_option(task, name, default=None):
58
110
  if hasattr(task, name):
59
111
  return getattr(task, name)
@@ -219,7 +271,7 @@ def _ready_execution_fields(
219
271
  created_at=None,
220
272
  ):
221
273
  fields = {
222
- "job": job,
274
+ "job_id": job.id,
223
275
  "backend_alias": _execution_backend_alias(job, backend_alias),
224
276
  "queue_name": _execution_queue_name(job, queue_name),
225
277
  "priority": _execution_priority(job, priority),
@@ -232,7 +284,7 @@ def _ready_execution_fields(
232
284
 
233
285
  def _scheduled_execution_fields(job, *, backend_alias, scheduled_at=None, created_at=None):
234
286
  fields = {
235
- "job": job,
287
+ "job_id": job.id,
236
288
  "backend_alias": _execution_backend_alias(job, backend_alias),
237
289
  "queue_name": job.queue_name,
238
290
  "priority": job.priority,
@@ -253,7 +305,7 @@ def _blocked_execution_fields(
253
305
  priority=None,
254
306
  ):
255
307
  return {
256
- "job": job,
308
+ "job_id": job.id,
257
309
  "backend_alias": _execution_backend_alias(job, backend_alias),
258
310
  "queue_name": _execution_queue_name(job, queue_name),
259
311
  "priority": _execution_priority(job, priority),
@@ -0,0 +1,36 @@
1
+ from django.db import connections
2
+
3
+ from dj_queue.db import database_capabilities
4
+
5
+
6
+ def create_ignore_conflicts(model, /, *, using, **fields):
7
+ obj = model(**fields)
8
+ connection = connections[using]
9
+ quote = connection.ops.quote_name
10
+ insert_fields = [
11
+ field
12
+ for field in model._meta.concrete_fields
13
+ if not field.generated and not _is_auto_field(field)
14
+ ]
15
+ columns = ", ".join(quote(field.column) for field in insert_fields)
16
+ placeholders = ", ".join(["%s"] * len(insert_fields))
17
+ params = [
18
+ field.get_db_prep_save(field.pre_save(obj, add=True), connection=connection)
19
+ for field in insert_fields
20
+ ]
21
+
22
+ table = quote(model._meta.db_table)
23
+ if database_capabilities(using).backend_family in {"mysql", "mariadb"}:
24
+ sql = f"INSERT IGNORE INTO {table} ({columns}) VALUES ({placeholders})"
25
+ else:
26
+ sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders}) ON CONFLICT DO NOTHING"
27
+
28
+ with connection.cursor() as cursor:
29
+ cursor.execute(sql, params)
30
+ rowcount = cursor.rowcount
31
+
32
+ return rowcount > 0
33
+
34
+
35
+ def _is_auto_field(field):
36
+ return field.get_internal_type() in {"AutoField", "BigAutoField", "SmallAutoField"}