dj-queue 0.10.2__tar.gz → 0.10.4__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.2 → dj_queue-0.10.4}/PKG-INFO +6 -7
- {dj_queue-0.10.2 → dj_queue-0.10.4}/README.md +5 -6
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/backend.py +8 -2
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/_helpers.py +8 -3
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/concurrency.py +228 -33
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/jobs.py +326 -55
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/base.py +9 -2
- {dj_queue-0.10.2 → dj_queue-0.10.4}/pyproject.toml +1 -1
- {dj_queue-0.10.2 → dj_queue-0.10.4}/LICENSE +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/admin.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/api.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/config.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/asgi.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/gunicorn.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/prometheus.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/dashboard.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/db.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/log.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/metrics.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/observability.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/cleanup.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/recurring.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/errors.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/notify.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/pool.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/supervisor.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/worker.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/views.py +0 -0
- {dj_queue-0.10.2 → dj_queue-0.10.4}/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.
|
|
3
|
+
Version: 0.10.4
|
|
4
4
|
Summary: Database-backed task queue backend for Django’s Tasks framework.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -61,6 +61,9 @@ It has a narrow, explicit shape:
|
|
|
61
61
|
For detailed comparisons with Celery, RQ, Procrastinate, and other alternatives,
|
|
62
62
|
see [COMPARISONS.md](docs/COMPARISONS.md).
|
|
63
63
|
|
|
64
|
+
For benchmarks on enqueue, promotion, recurring, claiming, worker-drain, and concurrency-contention scenarios,
|
|
65
|
+
see [docs/benchmarks/](docs/benchmarks/) for the latest published reports.
|
|
66
|
+
|
|
64
67
|
## Installation
|
|
65
68
|
|
|
66
69
|
`dj_queue` requires Python 3.12+ and Django 6.0+.
|
|
@@ -640,8 +643,9 @@ Run your normal application migrations on `default`, then migrate `dj_queue`
|
|
|
640
643
|
onto the queue database:
|
|
641
644
|
|
|
642
645
|
```bash
|
|
643
|
-
|
|
646
|
+
# migrate dj_queue on its queue alias first so django doesn't mark it applied on default
|
|
644
647
|
python manage.py migrate dj_queue --database queue
|
|
648
|
+
python manage.py migrate
|
|
645
649
|
```
|
|
646
650
|
|
|
647
651
|
With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
|
|
@@ -1012,11 +1016,6 @@ Both endpoints support bearer token authentication. Set
|
|
|
1012
1016
|
`Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
|
|
1013
1017
|
the network or proxy layer.
|
|
1014
1018
|
|
|
1015
|
-
## Benchmarks
|
|
1016
|
-
|
|
1017
|
-
The repository includes a standalone benchmark harness for enqueue, promotion, recurring, worker-drain, and concurrency-contention scenarios.
|
|
1018
|
-
See [`docs/benchmarks/`](docs/benchmarks/) for the latest published reports.
|
|
1019
|
-
|
|
1020
1019
|
## License
|
|
1021
1020
|
|
|
1022
1021
|
MIT
|
|
@@ -35,6 +35,9 @@ It has a narrow, explicit shape:
|
|
|
35
35
|
For detailed comparisons with Celery, RQ, Procrastinate, and other alternatives,
|
|
36
36
|
see [COMPARISONS.md](docs/COMPARISONS.md).
|
|
37
37
|
|
|
38
|
+
For benchmarks on enqueue, promotion, recurring, claiming, worker-drain, and concurrency-contention scenarios,
|
|
39
|
+
see [docs/benchmarks/](docs/benchmarks/) for the latest published reports.
|
|
40
|
+
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
40
43
|
`dj_queue` requires Python 3.12+ and Django 6.0+.
|
|
@@ -614,8 +617,9 @@ Run your normal application migrations on `default`, then migrate `dj_queue`
|
|
|
614
617
|
onto the queue database:
|
|
615
618
|
|
|
616
619
|
```bash
|
|
617
|
-
|
|
620
|
+
# migrate dj_queue on its queue alias first so django doesn't mark it applied on default
|
|
618
621
|
python manage.py migrate dj_queue --database queue
|
|
622
|
+
python manage.py migrate
|
|
619
623
|
```
|
|
620
624
|
|
|
621
625
|
With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
|
|
@@ -986,11 +990,6 @@ Both endpoints support bearer token authentication. Set
|
|
|
986
990
|
`Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
|
|
987
991
|
the network or proxy layer.
|
|
988
992
|
|
|
989
|
-
## Benchmarks
|
|
990
|
-
|
|
991
|
-
The repository includes a standalone benchmark harness for enqueue, promotion, recurring, worker-drain, and concurrency-contention scenarios.
|
|
992
|
-
See [`docs/benchmarks/`](docs/benchmarks/) for the latest published reports.
|
|
993
|
-
|
|
994
993
|
## License
|
|
995
994
|
|
|
996
995
|
MIT
|
|
@@ -28,7 +28,13 @@ class DjQueueBackend(BaseTaskBackend):
|
|
|
28
28
|
|
|
29
29
|
def enqueue(self, task, args, kwargs):
|
|
30
30
|
self.validate_task(task)
|
|
31
|
-
job, dispatch_outcome = enqueue_job_with_dispatch(
|
|
31
|
+
job, dispatch_outcome = enqueue_job_with_dispatch(
|
|
32
|
+
task,
|
|
33
|
+
args,
|
|
34
|
+
kwargs,
|
|
35
|
+
backend_alias=self.alias,
|
|
36
|
+
validate=False,
|
|
37
|
+
)
|
|
32
38
|
return task_result_from_enqueued_job(
|
|
33
39
|
job,
|
|
34
40
|
task,
|
|
@@ -49,7 +55,7 @@ class DjQueueBackend(BaseTaskBackend):
|
|
|
49
55
|
self.validate_task(task)
|
|
50
56
|
jobs.append((task, args, kwargs))
|
|
51
57
|
|
|
52
|
-
created_jobs = enqueue_jobs_bulk(jobs, backend_alias=self.alias)
|
|
58
|
+
created_jobs = enqueue_jobs_bulk(jobs, backend_alias=self.alias, validate=False)
|
|
53
59
|
return [
|
|
54
60
|
task_result_from_enqueued_job(
|
|
55
61
|
job,
|
|
@@ -170,8 +170,11 @@ def _scheduled_execution_row(job, *, backend_alias, scheduled_at=None, created_a
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def _create_scheduled_execution(
|
|
174
|
-
|
|
173
|
+
def _create_scheduled_execution(
|
|
174
|
+
alias, job, *, backend_alias, scheduled_at=None, check_conflicts=True
|
|
175
|
+
):
|
|
176
|
+
if check_conflicts:
|
|
177
|
+
_ensure_no_other_execution_state(alias, job)
|
|
175
178
|
return ScheduledExecution.objects.using(alias).create(
|
|
176
179
|
**_scheduled_execution_fields(
|
|
177
180
|
job,
|
|
@@ -190,8 +193,10 @@ def _create_blocked_execution(
|
|
|
190
193
|
expires_at,
|
|
191
194
|
queue_name=None,
|
|
192
195
|
priority=None,
|
|
196
|
+
check_conflicts=True,
|
|
193
197
|
):
|
|
194
|
-
|
|
198
|
+
if check_conflicts:
|
|
199
|
+
_ensure_no_other_execution_state(alias, job)
|
|
195
200
|
return BlockedExecution.objects.using(alias).create(
|
|
196
201
|
**_blocked_execution_fields(
|
|
197
202
|
job,
|
|
@@ -10,11 +10,20 @@ from dj_queue.config import load_backend_config
|
|
|
10
10
|
from dj_queue.db import database_capabilities, get_database_alias, locked_queryset
|
|
11
11
|
from dj_queue.exceptions import EnqueueError
|
|
12
12
|
from dj_queue.log import log_event
|
|
13
|
-
from dj_queue.models import
|
|
13
|
+
from dj_queue.models import (
|
|
14
|
+
BlockedExecution,
|
|
15
|
+
ClaimedExecution,
|
|
16
|
+
FailedExecution,
|
|
17
|
+
Job,
|
|
18
|
+
ReadyExecution,
|
|
19
|
+
ScheduledExecution,
|
|
20
|
+
Semaphore,
|
|
21
|
+
)
|
|
14
22
|
from dj_queue.operations._helpers import (
|
|
15
23
|
_consume_selected_rows,
|
|
16
24
|
_create_blocked_execution,
|
|
17
|
-
|
|
25
|
+
_create_ready_execution_locked,
|
|
26
|
+
_lock_active_pauses,
|
|
18
27
|
_task_option,
|
|
19
28
|
)
|
|
20
29
|
from dj_queue.operations._insert import create_ignore_conflicts
|
|
@@ -42,7 +51,7 @@ def semaphore_acquire(
|
|
|
42
51
|
now=now,
|
|
43
52
|
)
|
|
44
53
|
|
|
45
|
-
with
|
|
54
|
+
with _operation_atomic(alias):
|
|
46
55
|
if create_ignore_conflicts(
|
|
47
56
|
Semaphore,
|
|
48
57
|
using=alias,
|
|
@@ -54,7 +63,7 @@ def semaphore_acquire(
|
|
|
54
63
|
return True
|
|
55
64
|
|
|
56
65
|
reconciled_available = _reconciled_available_expression(limit)
|
|
57
|
-
with
|
|
66
|
+
with _operation_atomic(alias):
|
|
58
67
|
updated = (
|
|
59
68
|
Semaphore.objects.using(alias)
|
|
60
69
|
.filter(key=key, value__gt=F("limit") - Value(limit))
|
|
@@ -177,6 +186,29 @@ def _consume_released_semaphore_slot(alias, key, *, limit, duration_seconds, now
|
|
|
177
186
|
return updated > 0
|
|
178
187
|
|
|
179
188
|
|
|
189
|
+
def _handoff_released_claimed_slot(alias, key, *, limit, duration_seconds, now):
|
|
190
|
+
expires_at = now + timedelta(seconds=duration_seconds)
|
|
191
|
+
released_available = _released_available_expression(limit)
|
|
192
|
+
updated = (
|
|
193
|
+
Semaphore.objects.using(alias)
|
|
194
|
+
.filter(key=key, value__gt=F("limit") - Value(limit) - Value(1))
|
|
195
|
+
.update(
|
|
196
|
+
value=released_available - Value(1),
|
|
197
|
+
limit=limit,
|
|
198
|
+
expires_at=expires_at,
|
|
199
|
+
updated_at=now,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return updated > 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _released_available_expression(limit):
|
|
206
|
+
return Least(
|
|
207
|
+
Value(limit),
|
|
208
|
+
Greatest(Value(0), F("value") + Value(limit) - F("limit") + Value(1)),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
180
212
|
def _reconciled_available_expression(limit):
|
|
181
213
|
return Least(Value(limit), Greatest(Value(0), F("value") + Value(limit) - F("limit")))
|
|
182
214
|
|
|
@@ -209,27 +241,31 @@ def unblock_next_blocked_job(
|
|
|
209
241
|
backend_alias="default",
|
|
210
242
|
use_skip_locked=True,
|
|
211
243
|
handoff_released_slot=False,
|
|
244
|
+
release_slot=False,
|
|
212
245
|
):
|
|
213
246
|
alias = get_database_alias(backend_alias)
|
|
214
247
|
now = timezone.now()
|
|
215
248
|
|
|
216
|
-
with
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
with _operation_atomic(alias):
|
|
250
|
+
blocked = _consume_next_blocked_job(
|
|
251
|
+
alias,
|
|
252
|
+
backend_alias=backend_alias,
|
|
253
|
+
key=key,
|
|
254
|
+
use_skip_locked=use_skip_locked,
|
|
222
255
|
)
|
|
223
|
-
blocked = locked_queryset(queryset, use_skip_locked=use_skip_locked).first()
|
|
224
256
|
if blocked is None:
|
|
225
257
|
return None
|
|
226
258
|
|
|
227
|
-
consumed = _consume_selected_rows(alias, BlockedExecution, [blocked])
|
|
228
|
-
if not consumed:
|
|
229
|
-
return None
|
|
230
|
-
|
|
231
259
|
slot_acquired = False
|
|
232
|
-
if
|
|
260
|
+
if release_slot:
|
|
261
|
+
slot_acquired = _handoff_released_claimed_slot(
|
|
262
|
+
alias,
|
|
263
|
+
key,
|
|
264
|
+
limit=limit,
|
|
265
|
+
duration_seconds=duration_seconds,
|
|
266
|
+
now=now,
|
|
267
|
+
)
|
|
268
|
+
elif handoff_released_slot:
|
|
233
269
|
slot_acquired = _consume_released_semaphore_slot(
|
|
234
270
|
alias,
|
|
235
271
|
key,
|
|
@@ -238,7 +274,7 @@ def unblock_next_blocked_job(
|
|
|
238
274
|
now=now,
|
|
239
275
|
)
|
|
240
276
|
|
|
241
|
-
if not slot_acquired:
|
|
277
|
+
if not slot_acquired and not release_slot:
|
|
242
278
|
slot_acquired = semaphore_acquire(
|
|
243
279
|
key,
|
|
244
280
|
limit=limit,
|
|
@@ -246,37 +282,190 @@ def unblock_next_blocked_job(
|
|
|
246
282
|
backend_alias=backend_alias,
|
|
247
283
|
)
|
|
248
284
|
|
|
285
|
+
mock_job = Job(
|
|
286
|
+
id=blocked["job_id"],
|
|
287
|
+
queue_name=blocked["queue_name"],
|
|
288
|
+
priority=blocked["priority"],
|
|
289
|
+
concurrency_key=blocked["concurrency_key"],
|
|
290
|
+
backend_alias=backend_alias,
|
|
291
|
+
)
|
|
292
|
+
|
|
249
293
|
if not slot_acquired:
|
|
250
294
|
_create_blocked_execution(
|
|
251
295
|
alias,
|
|
252
|
-
|
|
296
|
+
mock_job,
|
|
253
297
|
backend_alias=backend_alias,
|
|
254
|
-
queue_name=blocked
|
|
255
|
-
priority=blocked
|
|
256
|
-
concurrency_key=blocked
|
|
257
|
-
expires_at=blocked
|
|
298
|
+
queue_name=blocked["queue_name"],
|
|
299
|
+
priority=blocked["priority"],
|
|
300
|
+
concurrency_key=blocked["concurrency_key"],
|
|
301
|
+
expires_at=blocked["expires_at"],
|
|
302
|
+
check_conflicts=False,
|
|
258
303
|
)
|
|
259
304
|
return None
|
|
260
305
|
|
|
261
|
-
|
|
262
|
-
queue_name = blocked.queue_name
|
|
263
|
-
priority = blocked.priority
|
|
264
|
-
_create_ready_execution(
|
|
306
|
+
_create_ready_execution_after_blocked_consume(
|
|
265
307
|
alias,
|
|
266
|
-
job=
|
|
308
|
+
job=mock_job,
|
|
267
309
|
backend_alias=backend_alias,
|
|
268
|
-
queue_name=queue_name,
|
|
269
|
-
priority=priority,
|
|
310
|
+
queue_name=blocked["queue_name"],
|
|
311
|
+
priority=blocked["priority"],
|
|
270
312
|
ready_at=now,
|
|
271
313
|
)
|
|
272
314
|
|
|
273
315
|
log_event(
|
|
274
316
|
"job.unblocked",
|
|
275
|
-
job_id=str(
|
|
317
|
+
job_id=str(mock_job.id),
|
|
276
318
|
concurrency_key=key,
|
|
277
319
|
)
|
|
278
|
-
notify_ready_queues_on_commit((
|
|
279
|
-
return
|
|
320
|
+
notify_ready_queues_on_commit((blocked["queue_name"],), backend_alias=backend_alias)
|
|
321
|
+
return mock_job
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _consume_next_blocked_job(alias, *, backend_alias, key, use_skip_locked):
|
|
325
|
+
capabilities = database_capabilities(alias)
|
|
326
|
+
if capabilities.backend_family == "postgresql":
|
|
327
|
+
return _postgres_consume_next_blocked_job(
|
|
328
|
+
alias,
|
|
329
|
+
backend_alias=backend_alias,
|
|
330
|
+
key=key,
|
|
331
|
+
use_skip_locked=use_skip_locked and capabilities.supports_skip_locked,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
queryset = (
|
|
335
|
+
BlockedExecution.objects.using(alias)
|
|
336
|
+
.filter(backend_alias=backend_alias, concurrency_key=key)
|
|
337
|
+
.order_by("-priority", "id")
|
|
338
|
+
)
|
|
339
|
+
blocked = locked_queryset(queryset, use_skip_locked=use_skip_locked).first()
|
|
340
|
+
if blocked is None:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
consumed = _consume_selected_rows(alias, BlockedExecution, [blocked])
|
|
344
|
+
if not consumed:
|
|
345
|
+
return None
|
|
346
|
+
return _blocked_execution_values(blocked)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _postgres_consume_next_blocked_job(alias, *, backend_alias, key, use_skip_locked):
|
|
350
|
+
connection = connections[alias]
|
|
351
|
+
quote = connection.ops.quote_name
|
|
352
|
+
table = quote(BlockedExecution._meta.db_table)
|
|
353
|
+
pk_column = quote(BlockedExecution._meta.pk.column)
|
|
354
|
+
job_id_column = quote(BlockedExecution._meta.get_field("job").column)
|
|
355
|
+
backend_alias_column = quote(BlockedExecution._meta.get_field("backend_alias").column)
|
|
356
|
+
queue_name_column = quote(BlockedExecution._meta.get_field("queue_name").column)
|
|
357
|
+
priority_column = quote(BlockedExecution._meta.get_field("priority").column)
|
|
358
|
+
concurrency_key_column = quote(BlockedExecution._meta.get_field("concurrency_key").column)
|
|
359
|
+
expires_at_column = quote(BlockedExecution._meta.get_field("expires_at").column)
|
|
360
|
+
skip_locked_sql = " SKIP LOCKED" if use_skip_locked else ""
|
|
361
|
+
|
|
362
|
+
with connection.cursor() as cursor:
|
|
363
|
+
cursor.execute(
|
|
364
|
+
f"""
|
|
365
|
+
DELETE FROM {table}
|
|
366
|
+
WHERE {table}.{pk_column} = (
|
|
367
|
+
SELECT {pk_column}
|
|
368
|
+
FROM {table}
|
|
369
|
+
WHERE {backend_alias_column} = %s AND {concurrency_key_column} = %s
|
|
370
|
+
ORDER BY {priority_column} DESC, {pk_column} ASC
|
|
371
|
+
LIMIT 1
|
|
372
|
+
FOR UPDATE{skip_locked_sql}
|
|
373
|
+
)
|
|
374
|
+
RETURNING
|
|
375
|
+
{job_id_column},
|
|
376
|
+
{queue_name_column},
|
|
377
|
+
{priority_column},
|
|
378
|
+
{concurrency_key_column},
|
|
379
|
+
{expires_at_column}
|
|
380
|
+
""",
|
|
381
|
+
[backend_alias, key],
|
|
382
|
+
)
|
|
383
|
+
row = cursor.fetchone()
|
|
384
|
+
|
|
385
|
+
if row is None:
|
|
386
|
+
return None
|
|
387
|
+
return {
|
|
388
|
+
"job_id": row[0],
|
|
389
|
+
"queue_name": row[1],
|
|
390
|
+
"priority": row[2],
|
|
391
|
+
"concurrency_key": row[3],
|
|
392
|
+
"expires_at": row[4],
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _blocked_execution_values(blocked):
|
|
397
|
+
return {
|
|
398
|
+
"job_id": blocked.job_id,
|
|
399
|
+
"queue_name": blocked.queue_name,
|
|
400
|
+
"priority": blocked.priority,
|
|
401
|
+
"concurrency_key": blocked.concurrency_key,
|
|
402
|
+
"expires_at": blocked.expires_at,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _create_ready_execution_after_blocked_consume(
|
|
407
|
+
alias,
|
|
408
|
+
*,
|
|
409
|
+
job,
|
|
410
|
+
backend_alias,
|
|
411
|
+
queue_name,
|
|
412
|
+
priority,
|
|
413
|
+
ready_at,
|
|
414
|
+
):
|
|
415
|
+
_lock_active_pauses(alias, backend_alias, {queue_name})
|
|
416
|
+
connection = connections[alias]
|
|
417
|
+
quote = connection.ops.quote_name
|
|
418
|
+
ready_table = quote(ReadyExecution._meta.db_table)
|
|
419
|
+
job_id_column = quote(ReadyExecution._meta.get_field("job").column)
|
|
420
|
+
backend_alias_column = quote(ReadyExecution._meta.get_field("backend_alias").column)
|
|
421
|
+
queue_name_column = quote(ReadyExecution._meta.get_field("queue_name").column)
|
|
422
|
+
priority_column = quote(ReadyExecution._meta.get_field("priority").column)
|
|
423
|
+
created_at_column = quote(ReadyExecution._meta.get_field("created_at").column)
|
|
424
|
+
latency_started_at_column = quote(ReadyExecution._meta.get_field("latency_started_at").column)
|
|
425
|
+
job_id = Job._meta.get_field("id").get_db_prep_value(
|
|
426
|
+
job.pk,
|
|
427
|
+
connection=connection,
|
|
428
|
+
prepared=False,
|
|
429
|
+
)
|
|
430
|
+
state_models = (ReadyExecution, ScheduledExecution, ClaimedExecution, FailedExecution)
|
|
431
|
+
state_checks = " AND ".join(
|
|
432
|
+
_state_absence_sql(model, job_id_column=job_id_column, quote=quote) for model in state_models
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
with connection.cursor() as cursor:
|
|
436
|
+
cursor.execute(
|
|
437
|
+
f"""
|
|
438
|
+
INSERT INTO {ready_table} (
|
|
439
|
+
{job_id_column},
|
|
440
|
+
{backend_alias_column},
|
|
441
|
+
{queue_name_column},
|
|
442
|
+
{priority_column},
|
|
443
|
+
{created_at_column},
|
|
444
|
+
{latency_started_at_column}
|
|
445
|
+
)
|
|
446
|
+
SELECT %s, %s, %s, %s, %s, %s
|
|
447
|
+
WHERE {state_checks}
|
|
448
|
+
""",
|
|
449
|
+
[
|
|
450
|
+
job_id,
|
|
451
|
+
backend_alias,
|
|
452
|
+
queue_name,
|
|
453
|
+
priority,
|
|
454
|
+
ready_at,
|
|
455
|
+
ready_at,
|
|
456
|
+
*([job_id] * len(state_models)),
|
|
457
|
+
],
|
|
458
|
+
)
|
|
459
|
+
created = cursor.rowcount
|
|
460
|
+
|
|
461
|
+
if created != 1:
|
|
462
|
+
raise EnqueueError(f"job {job.id} already has an execution-state row")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _state_absence_sql(model, *, job_id_column, quote):
|
|
466
|
+
state_table = quote(model._meta.db_table)
|
|
467
|
+
state_job_id_column = quote(model._meta.get_field("job").column)
|
|
468
|
+
return f"NOT EXISTS (SELECT 1 FROM {state_table} WHERE {state_table}.{state_job_id_column} = %s)"
|
|
280
469
|
|
|
281
470
|
|
|
282
471
|
def cleanup_expired_semaphores(*, backend_alias="default"):
|
|
@@ -349,13 +538,14 @@ def promote_expired_blocked_jobs(*, batch_size=500, backend_alias="default", use
|
|
|
349
538
|
priority = blocked.priority
|
|
350
539
|
if not uses_serialized_writes:
|
|
351
540
|
blocked.delete(using=alias)
|
|
352
|
-
|
|
541
|
+
_create_ready_execution_locked(
|
|
353
542
|
alias,
|
|
354
543
|
job=job,
|
|
355
544
|
backend_alias=backend_alias,
|
|
356
545
|
queue_name=queue_name,
|
|
357
546
|
priority=priority,
|
|
358
547
|
ready_at=now,
|
|
548
|
+
check_conflicts=False,
|
|
359
549
|
)
|
|
360
550
|
promoted_jobs.append(job)
|
|
361
551
|
else:
|
|
@@ -369,6 +559,7 @@ def promote_expired_blocked_jobs(*, batch_size=500, backend_alias="default", use
|
|
|
369
559
|
priority=blocked.priority,
|
|
370
560
|
concurrency_key=blocked.concurrency_key,
|
|
371
561
|
expires_at=expires_at,
|
|
562
|
+
check_conflicts=False,
|
|
372
563
|
)
|
|
373
564
|
else:
|
|
374
565
|
blocked.expires_at = expires_at
|
|
@@ -397,3 +588,7 @@ def _positive_int_option(value, name):
|
|
|
397
588
|
if number <= 0:
|
|
398
589
|
raise EnqueueError(f"{name} must be a positive integer")
|
|
399
590
|
return number
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _operation_atomic(alias):
|
|
594
|
+
return transaction.atomic(using=alias, savepoint=not connections[alias].in_atomic_block)
|