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.
- {dj_queue-0.10.5 → dj_queue-0.10.6}/PKG-INFO +1 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/config.py +3 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/asgi.py +6 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/gunicorn.py +27 -5
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/dashboard.py +6 -6
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/metrics.py +7 -7
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/observability.py +57 -139
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/_helpers.py +62 -10
- dj_queue-0.10.6/dj_queue/operations/_insert.py +36 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/cleanup.py +11 -5
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/concurrency.py +49 -24
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/jobs.py +73 -60
- dj_queue-0.10.6/dj_queue/queue_state.py +270 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/base.py +7 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/errors.py +0 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/notify.py +2 -1
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/supervisor.py +5 -3
- {dj_queue-0.10.5 → dj_queue-0.10.6}/pyproject.toml +1 -1
- dj_queue-0.10.5/dj_queue/operations/_insert.py +0 -24
- dj_queue-0.10.5/dj_queue/queue_state.py +0 -138
- {dj_queue-0.10.5 → dj_queue-0.10.6}/LICENSE +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/README.md +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/admin.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/api.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/backend.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/contrib/prometheus.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/db.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/log.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0004_dashboard.py +0 -0
- {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
- {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
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {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
- {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
- {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
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/operations/recurring.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/pool.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/runtime/worker.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/views.py +0 -0
- {dj_queue-0.10.5 → dj_queue-0.10.6}/dj_queue/wakeup.py +0 -0
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
272
|
-
process_rows = snapshot
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39
|
-
queue_database_alias = snapshot
|
|
40
|
-
runner_metrics = snapshot
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
242
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"}
|