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.
Files changed (85) hide show
  1. {dj_queue-0.10.2 → dj_queue-0.10.4}/PKG-INFO +6 -7
  2. {dj_queue-0.10.2 → dj_queue-0.10.4}/README.md +5 -6
  3. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/backend.py +8 -2
  4. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/_helpers.py +8 -3
  5. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/concurrency.py +228 -33
  6. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/jobs.py +326 -55
  7. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/base.py +9 -2
  8. {dj_queue-0.10.2 → dj_queue-0.10.4}/pyproject.toml +1 -1
  9. {dj_queue-0.10.2 → dj_queue-0.10.4}/LICENSE +0 -0
  10. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/__init__.py +0 -0
  11. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/admin.py +0 -0
  12. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/api.py +0 -0
  13. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/apps.py +0 -0
  14. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/config.py +0 -0
  15. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/__init__.py +0 -0
  16. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/asgi.py +0 -0
  17. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/gunicorn.py +0 -0
  18. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/contrib/prometheus.py +0 -0
  19. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/cron.py +0 -0
  20. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/dashboard.py +0 -0
  21. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/db.py +0 -0
  22. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/exceptions.py +0 -0
  23. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/hooks.py +0 -0
  24. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/log.py +0 -0
  25. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/__init__.py +0 -0
  26. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/__init__.py +0 -0
  27. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue.py +0 -0
  28. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue_health.py +0 -0
  29. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  30. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/metrics.py +0 -0
  31. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0001_initial.py +0 -0
  32. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  33. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  34. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0004_dashboard.py +0 -0
  35. {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
  36. {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
  37. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  38. {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
  39. {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
  40. {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
  41. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/migrations/__init__.py +0 -0
  42. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/__init__.py +0 -0
  43. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/jobs.py +0 -0
  44. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/recurring.py +0 -0
  45. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/models/runtime.py +0 -0
  46. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/observability.py +0 -0
  47. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/__init__.py +0 -0
  48. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/_insert.py +0 -0
  49. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/cleanup.py +0 -0
  50. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/queues.py +0 -0
  51. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/operations/recurring.py +0 -0
  52. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/queue_selectors.py +0 -0
  53. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/queue_state.py +0 -0
  54. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/routers.py +0 -0
  55. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/__init__.py +0 -0
  56. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/connection_budget.py +0 -0
  57. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/dispatcher.py +0 -0
  58. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/errors.py +0 -0
  59. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/interruptible.py +0 -0
  60. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/notify.py +0 -0
  61. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/pidfile.py +0 -0
  62. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/pool.py +0 -0
  63. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/procline.py +0 -0
  64. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/scheduler.py +0 -0
  65. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/supervisor.py +0 -0
  66. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/topology.py +0 -0
  67. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/runtime/worker.py +0 -0
  68. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/task_results.py +0 -0
  69. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  70. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  71. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  72. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  73. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  74. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  75. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  76. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  77. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  78. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  79. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  80. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  81. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templatetags/__init__.py +0 -0
  82. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  83. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/urls.py +0 -0
  84. {dj_queue-0.10.2 → dj_queue-0.10.4}/dj_queue/views.py +0 -0
  85. {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.2
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
- python manage.py migrate
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
- python manage.py migrate
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(task, args, kwargs, backend_alias=self.alias)
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(alias, job, *, backend_alias, scheduled_at=None):
174
- _ensure_no_other_execution_state(alias, job)
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
- _ensure_no_other_execution_state(alias, job)
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 BlockedExecution, ClaimedExecution, ReadyExecution, Semaphore
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
- _create_ready_execution,
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 transaction.atomic(using=alias):
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 transaction.atomic(using=alias):
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 transaction.atomic(using=alias):
217
- queryset = (
218
- BlockedExecution.objects.using(alias)
219
- .select_related("job")
220
- .filter(backend_alias=backend_alias, concurrency_key=key)
221
- .order_by("-priority", "id")
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 handoff_released_slot:
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
- blocked.job,
296
+ mock_job,
253
297
  backend_alias=backend_alias,
254
- queue_name=blocked.queue_name,
255
- priority=blocked.priority,
256
- concurrency_key=blocked.concurrency_key,
257
- expires_at=blocked.expires_at,
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
- job = blocked.job
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=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(job.id),
317
+ job_id=str(mock_job.id),
276
318
  concurrency_key=key,
277
319
  )
278
- notify_ready_queues_on_commit((job.queue_name,), backend_alias=backend_alias)
279
- return job
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
- _create_ready_execution(
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)