dj-queue 0.6.2__tar.gz → 0.6.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 (71) hide show
  1. {dj_queue-0.6.2 → dj_queue-0.6.4}/PKG-INFO +1 -1
  2. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/admin.py +44 -1
  3. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/backend.py +1 -1
  4. dj_queue-0.6.4/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +59 -0
  5. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/models/jobs.py +16 -64
  6. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/models/recurring.py +4 -1
  7. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/cleanup.py +8 -4
  8. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/concurrency.py +9 -10
  9. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/jobs.py +74 -17
  10. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/supervisor.py +49 -4
  11. {dj_queue-0.6.2 → dj_queue-0.6.4}/pyproject.toml +1 -1
  12. {dj_queue-0.6.2 → dj_queue-0.6.4}/LICENSE +0 -0
  13. {dj_queue-0.6.2 → dj_queue-0.6.4}/README.md +0 -0
  14. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/__init__.py +0 -0
  15. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/api.py +0 -0
  16. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/apps.py +0 -0
  17. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/config.py +0 -0
  18. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/contrib/__init__.py +0 -0
  19. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/contrib/asgi.py +0 -0
  20. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/contrib/gunicorn.py +0 -0
  21. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/contrib/prometheus.py +0 -0
  22. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/dashboard.py +0 -0
  23. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/db.py +0 -0
  24. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/exceptions.py +0 -0
  25. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/hooks.py +0 -0
  26. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/log.py +0 -0
  27. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/management/__init__.py +0 -0
  28. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/management/commands/__init__.py +0 -0
  29. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/management/commands/dj_queue.py +0 -0
  30. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/management/commands/dj_queue_health.py +0 -0
  31. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  32. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/0001_initial.py +0 -0
  33. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  34. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  35. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/0004_dashboard.py +0 -0
  36. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
  37. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/migrations/__init__.py +0 -0
  38. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/models/__init__.py +0 -0
  39. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/models/runtime.py +0 -0
  40. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/observability.py +0 -0
  41. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/__init__.py +0 -0
  42. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/_insert.py +0 -0
  43. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/operations/recurring.py +0 -0
  44. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/routers.py +0 -0
  45. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/__init__.py +0 -0
  46. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/base.py +0 -0
  47. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/dispatcher.py +0 -0
  48. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/errors.py +0 -0
  49. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/interruptible.py +0 -0
  50. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/notify.py +0 -0
  51. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/pidfile.py +0 -0
  52. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/pool.py +0 -0
  53. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/procline.py +0 -0
  54. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/scheduler.py +0 -0
  55. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/runtime/worker.py +0 -0
  56. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  57. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  58. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  59. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  60. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  61. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  62. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  63. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  64. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  65. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  66. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  67. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  68. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templatetags/__init__.py +0 -0
  69. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  70. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/urls.py +0 -0
  71. {dj_queue-0.6.2 → dj_queue-0.6.4}/dj_queue/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Database-backed task queue backend for Django’s Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -28,7 +28,7 @@ from dj_queue.models import (
28
28
  RecurringTask,
29
29
  Semaphore,
30
30
  )
31
- from dj_queue.operations.jobs import enqueue_job_again
31
+ from dj_queue.operations.jobs import dispatch_scheduled_job_now, enqueue_job_again
32
32
 
33
33
 
34
34
  class DjQueueFirstAdminSite(admin.AdminSite):
@@ -563,6 +563,16 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
563
563
  def get_change_actions(self, request, obj):
564
564
  if obj is None:
565
565
  return ()
566
+ if obj.status == "scheduled":
567
+ return (
568
+ {"name": "run_now", "label": "Run now", "css_class": "djq-object-action-retry"},
569
+ {
570
+ "name": "enqueue_copy_now",
571
+ "label": "Enqueue copy now",
572
+ "css_class": "djq-object-action-retry",
573
+ },
574
+ )
575
+
566
576
  actions = [{"name": "enqueue", "label": "Enqueue job", "css_class": "djq-object-action-retry"}]
567
577
  if obj.status == "failed":
568
578
  actions.extend(
@@ -582,6 +592,39 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
582
592
  return tuple(actions)
583
593
 
584
594
  def handle_change_action(self, request, obj, action):
595
+ if action == "run_now":
596
+ try:
597
+ _job, dispatched_as = dispatch_scheduled_job_now(obj.pk, backend_alias=obj.backend_alias)
598
+ except (EnqueueError, ImportError, AttributeError) as exc:
599
+ self.message_user(request, f"Could not dispatch job now: {exc}", level=messages.ERROR)
600
+ return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
601
+
602
+ message = "Dispatched scheduled job for immediate execution"
603
+ if dispatched_as == "blocked":
604
+ message = "Dispatched scheduled job immediately and it is now blocked"
605
+ if dispatched_as == "discarded":
606
+ message = "Dispatched scheduled job immediately and it was discarded"
607
+ self.message_user(request, message, level=messages.SUCCESS)
608
+ return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
609
+
610
+ if action == "enqueue_copy_now":
611
+ try:
612
+ new_job = enqueue_job_again(obj.pk, backend_alias=obj.backend_alias, run_after=None)
613
+ except (EnqueueError, ImportError, AttributeError) as exc:
614
+ self.message_user(request, f"Could not enqueue job: {exc}", level=messages.ERROR)
615
+ return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
616
+
617
+ self.message_user(
618
+ request,
619
+ format_html(
620
+ 'Enqueued immediate copy <a href="{}">{}</a>.',
621
+ self._change_url(object_id=new_job.pk, backend_alias=new_job.backend_alias),
622
+ new_job.pk,
623
+ ),
624
+ level=messages.SUCCESS,
625
+ )
626
+ return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
627
+
585
628
  if action == "enqueue":
586
629
  try:
587
630
  new_job = enqueue_job_again(obj.pk, backend_alias=obj.backend_alias)
@@ -54,7 +54,7 @@ class DjQueueBackend(BaseTaskBackend):
54
54
  "blocked_execution",
55
55
  "failed_execution",
56
56
  )
57
- .get(pk=result_id)
57
+ .get(pk=result_id, backend_alias=self.alias)
58
58
  )
59
59
  except Job.DoesNotExist as exc:
60
60
  raise TaskResultDoesNotExist(str(result_id)) from exc
@@ -0,0 +1,59 @@
1
+ # Generated by Django 6.0.4 on 2026-05-06 05:12
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ (
9
+ "dj_queue",
10
+ "0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more",
11
+ ),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddIndex(
16
+ model_name="blockedexecution",
17
+ index=models.Index(
18
+ fields=["concurrency_key", "-priority", "id"], name="dj_queue_bl_concurr_2d8393_idx"
19
+ ),
20
+ ),
21
+ migrations.AddIndex(
22
+ model_name="blockedexecution",
23
+ index=models.Index(
24
+ fields=["expires_at", "-priority", "id"], name="dj_queue_bl_expires_f4b090_idx"
25
+ ),
26
+ ),
27
+ migrations.AddIndex(
28
+ model_name="failedexecution",
29
+ index=models.Index(fields=["created_at", "job"], name="dj_queue_fa_created_343d7d_idx"),
30
+ ),
31
+ migrations.AddIndex(
32
+ model_name="job",
33
+ index=models.Index(
34
+ fields=["backend_alias", "finished_at", "id"], name="dj_queue_jo_backend_ee105e_idx"
35
+ ),
36
+ ),
37
+ migrations.AddIndex(
38
+ model_name="readyexecution",
39
+ index=models.Index(fields=["-priority", "id"], name="dj_queue_re_priorit_ee6ffe_idx"),
40
+ ),
41
+ migrations.AddIndex(
42
+ model_name="readyexecution",
43
+ index=models.Index(
44
+ fields=["queue_name", "-priority", "id"], name="dj_queue_re_queue_n_612755_idx"
45
+ ),
46
+ ),
47
+ migrations.AddIndex(
48
+ model_name="recurringexecution",
49
+ index=models.Index(
50
+ fields=["backend_alias", "run_at", "id"], name="dj_queue_re_backend_a68bf1_idx"
51
+ ),
52
+ ),
53
+ migrations.AddIndex(
54
+ model_name="scheduledexecution",
55
+ index=models.Index(
56
+ fields=["scheduled_at", "-priority", "id"], name="dj_queue_sc_schedul_edba95_idx"
57
+ ),
58
+ ),
59
+ ]
@@ -1,6 +1,6 @@
1
1
  import uuid
2
2
 
3
- from django.core.exceptions import ObjectDoesNotExist, ValidationError
3
+ from django.core.exceptions import ObjectDoesNotExist
4
4
  from django.db import models
5
5
  from django.db.models import Q
6
6
  from django.utils.module_loading import import_string
@@ -65,6 +65,7 @@ class Job(models.Model):
65
65
  models.Index(fields=["queue_name", "finished_at"]),
66
66
  models.Index(fields=["scheduled_at", "finished_at"]),
67
67
  models.Index(fields=["finished_at"]),
68
+ models.Index(fields=["backend_alias", "finished_at", "id"]),
68
69
  ]
69
70
 
70
71
  @property
@@ -125,16 +126,10 @@ class ReadyExecution(models.Model):
125
126
  indexes = [
126
127
  models.Index(fields=["priority", "id"]),
127
128
  models.Index(fields=["queue_name", "priority", "id"]),
129
+ models.Index(fields=["-priority", "id"]),
130
+ models.Index(fields=["queue_name", "-priority", "id"]),
128
131
  ]
129
132
 
130
- def clean(self):
131
- super().clean()
132
- _validate_live_state(self)
133
-
134
- def save(self, *args, **kwargs):
135
- self.full_clean()
136
- return super().save(*args, **kwargs)
137
-
138
133
  @classmethod
139
134
  def discard_all_in_batches(cls, *, batch_size=500, backend_alias="default"):
140
135
  operation = import_string("dj_queue.operations.jobs.discard_ready_jobs")
@@ -159,15 +154,10 @@ class ScheduledExecution(models.Model):
159
154
 
160
155
  class Meta:
161
156
  db_table = "dj_queue_scheduled_executions"
162
- indexes = [models.Index(fields=["scheduled_at", "priority", "id"])]
163
-
164
- def clean(self):
165
- super().clean()
166
- _validate_live_state(self)
167
-
168
- def save(self, *args, **kwargs):
169
- self.full_clean()
170
- return super().save(*args, **kwargs)
157
+ indexes = [
158
+ models.Index(fields=["scheduled_at", "priority", "id"]),
159
+ models.Index(fields=["scheduled_at", "-priority", "id"]),
160
+ ]
171
161
 
172
162
 
173
163
  class ClaimedExecution(models.Model):
@@ -189,14 +179,6 @@ class ClaimedExecution(models.Model):
189
179
  db_table = "dj_queue_claimed_executions"
190
180
  indexes = [models.Index(fields=["process", "job"])]
191
181
 
192
- def clean(self):
193
- super().clean()
194
- _validate_live_state(self)
195
-
196
- def save(self, *args, **kwargs):
197
- self.full_clean()
198
- return super().save(*args, **kwargs)
199
-
200
182
  @classmethod
201
183
  def discard_all_in_batches(cls, **_kwargs):
202
184
  raise UndiscardableError("cannot discard in-progress jobs")
@@ -219,16 +201,10 @@ class BlockedExecution(models.Model):
219
201
  indexes = [
220
202
  models.Index(fields=["concurrency_key", "priority", "id"]),
221
203
  models.Index(fields=["expires_at", "concurrency_key"]),
204
+ models.Index(fields=["concurrency_key", "-priority", "id"]),
205
+ models.Index(fields=["expires_at", "-priority", "id"]),
222
206
  ]
223
207
 
224
- def clean(self):
225
- super().clean()
226
- _validate_live_state(self)
227
-
228
- def save(self, *args, **kwargs):
229
- self.full_clean()
230
- return super().save(*args, **kwargs)
231
-
232
208
  @classmethod
233
209
  def discard_all_in_batches(cls, *, batch_size=500, backend_alias="default"):
234
210
  operation = import_string("dj_queue.operations.jobs.discard_blocked_jobs")
@@ -253,14 +229,7 @@ class FailedExecution(models.Model):
253
229
 
254
230
  class Meta:
255
231
  db_table = "dj_queue_failed_executions"
256
-
257
- def clean(self):
258
- super().clean()
259
- _validate_live_state(self)
260
-
261
- def save(self, *args, **kwargs):
262
- self.full_clean()
263
- return super().save(*args, **kwargs)
232
+ indexes = [models.Index(fields=["created_at", "job"])]
264
233
 
265
234
  def retry(self):
266
235
  return _retry_failed_job(self.job_id, backend_alias=self.job.backend_alias)
@@ -301,28 +270,11 @@ def _discard_jobs_for_state(model, operation, *, batch_size, backend_alias):
301
270
  alias = get_database_alias(backend_alias)
302
271
  deleted = 0
303
272
  while True:
304
- job_ids = list(model.objects.using(alias).values_list("job_id", flat=True)[:batch_size])
273
+ job_ids = list(
274
+ model.objects.using(alias)
275
+ .filter(job__backend_alias=backend_alias)
276
+ .values_list("job_id", flat=True)[:batch_size]
277
+ )
305
278
  if not job_ids:
306
279
  return deleted
307
280
  deleted += operation(job_ids=job_ids, batch_size=batch_size, backend_alias=backend_alias)
308
-
309
-
310
- def _validate_live_state(instance):
311
- if not instance.job_id:
312
- return
313
-
314
- for model in LIVE_STATE_MODELS:
315
- queryset = model._default_manager.filter(job_id=instance.job_id)
316
- if model is instance.__class__ and instance.pk is not None:
317
- queryset = queryset.exclude(pk=instance.pk)
318
- if queryset.exists():
319
- raise ValidationError({"job": "job already has a live execution state"})
320
-
321
-
322
- LIVE_STATE_MODELS = (
323
- ReadyExecution,
324
- ScheduledExecution,
325
- ClaimedExecution,
326
- BlockedExecution,
327
- FailedExecution,
328
- )
@@ -57,4 +57,7 @@ class RecurringExecution(models.Model):
57
57
  name="dj_queue_recur_exec_backend_run_at_unique",
58
58
  )
59
59
  ]
60
- indexes = [models.Index(fields=["backend_alias", "task_key", "run_at"])]
60
+ indexes = [
61
+ models.Index(fields=["backend_alias", "task_key", "run_at"]),
62
+ models.Index(fields=["backend_alias", "run_at", "id"]),
63
+ ]
@@ -25,7 +25,11 @@ def clear_finished_jobs(
25
25
  if now is None:
26
26
  now = timezone.now()
27
27
  cutoff = now - timedelta(seconds=older_than)
28
- queryset = Job.objects.using(alias).filter(finished_at__lt=cutoff).order_by("finished_at", "id")
28
+ queryset = (
29
+ Job.objects.using(alias)
30
+ .filter(backend_alias=backend_alias, finished_at__lt=cutoff)
31
+ .order_by("finished_at", "id")
32
+ )
29
33
  if task_path is not None:
30
34
  queryset = queryset.filter(task_path=task_path)
31
35
 
@@ -33,7 +37,7 @@ def clear_finished_jobs(
33
37
  if not job_ids:
34
38
  return 0
35
39
 
36
- Job.objects.using(alias).filter(pk__in=job_ids).delete()
40
+ Job.objects.using(alias).filter(backend_alias=backend_alias, pk__in=job_ids).delete()
37
41
  return len(job_ids)
38
42
 
39
43
 
@@ -57,7 +61,7 @@ def clear_failed_jobs(
57
61
  cutoff = now - timedelta(seconds=older_than)
58
62
  queryset = (
59
63
  FailedExecution.objects.using(alias)
60
- .filter(created_at__lt=cutoff)
64
+ .filter(job__backend_alias=backend_alias, created_at__lt=cutoff)
61
65
  .order_by("created_at", "job_id")
62
66
  )
63
67
  if task_path is not None:
@@ -67,7 +71,7 @@ def clear_failed_jobs(
67
71
  if not job_ids:
68
72
  return 0
69
73
 
70
- Job.objects.using(alias).filter(pk__in=job_ids).delete()
74
+ Job.objects.using(alias).filter(backend_alias=backend_alias, pk__in=job_ids).delete()
71
75
  return len(job_ids)
72
76
 
73
77
 
@@ -8,7 +8,7 @@ from django.utils.module_loading import import_string
8
8
  from dj_queue.config import load_backend_config
9
9
  from dj_queue.db import get_database_alias, locked_queryset
10
10
  from dj_queue.log import log_event
11
- from dj_queue.models import BlockedExecution, Job, Pause, ReadyExecution, Semaphore
11
+ from dj_queue.models import BlockedExecution, Pause, ReadyExecution, Semaphore
12
12
  from dj_queue.operations._insert import create_ignore_conflicts
13
13
  from dj_queue.runtime import notify as runtime_notify
14
14
 
@@ -77,7 +77,10 @@ def unblock_next_blocked_job(
77
77
 
78
78
  with transaction.atomic(using=alias):
79
79
  queryset = (
80
- BlockedExecution.objects.using(alias).filter(concurrency_key=key).order_by("-priority", "id")
80
+ BlockedExecution.objects.using(alias)
81
+ .select_related("job")
82
+ .filter(concurrency_key=key, job__backend_alias=backend_alias)
83
+ .order_by("-priority", "id")
81
84
  )
82
85
  blocked = locked_queryset(queryset, use_skip_locked=use_skip_locked).first()
83
86
  if blocked is None:
@@ -91,7 +94,7 @@ def unblock_next_blocked_job(
91
94
  ):
92
95
  return None
93
96
 
94
- job = Job.objects.using(alias).get(pk=blocked.job_id)
97
+ job = blocked.job
95
98
  queue_name = blocked.queue_name
96
99
  priority = blocked.priority
97
100
  blocked.delete(using=alias)
@@ -134,20 +137,16 @@ def promote_expired_blocked_jobs(*, batch_size=500, backend_alias="default", use
134
137
  with transaction.atomic(using=alias):
135
138
  queryset = (
136
139
  BlockedExecution.objects.using(alias)
137
- .filter(expires_at__lte=now)
140
+ .select_related("job")
141
+ .filter(job__backend_alias=backend_alias, expires_at__lte=now)
138
142
  .order_by("expires_at", "-priority", "id")
139
143
  )
140
144
  blocked_rows = list(locked_queryset(queryset, use_skip_locked=use_skip_locked)[:batch_size])
141
145
  if not blocked_rows:
142
146
  return []
143
147
 
144
- jobs_by_id = {
145
- job.id: job
146
- for job in Job.objects.using(alias).filter(pk__in=[b.job_id for b in blocked_rows])
147
- }
148
-
149
148
  for blocked in blocked_rows:
150
- job = jobs_by_id[blocked.job_id]
149
+ job = blocked.job
151
150
  limit, duration_seconds = task_settings.get(job.task_path, (None, None))
152
151
  if limit is None:
153
152
  task = import_string(job.task_path)
@@ -219,7 +219,11 @@ def claim_ready_jobs(
219
219
  )
220
220
 
221
221
  with transaction.atomic(using=alias):
222
- queryset = ReadyExecution.objects.using(alias).filter(job__backend_alias=backend_alias)
222
+ queryset = (
223
+ ReadyExecution.objects.using(alias)
224
+ .select_related("job")
225
+ .filter(job__backend_alias=backend_alias)
226
+ )
223
227
  if paused_queue_names:
224
228
  queryset = queryset.exclude(queue_name__in=paused_queue_names)
225
229
  ready_rows = _select_ready_rows(
@@ -231,9 +235,7 @@ def claim_ready_jobs(
231
235
  if not ready_rows:
232
236
  return []
233
237
 
234
- job_ids = [row.job_id for row in ready_rows]
235
- jobs_by_id = {job.id: job for job in Job.objects.using(alias).filter(pk__in=job_ids)}
236
- jobs = [jobs_by_id[job_id] for job_id in job_ids]
238
+ jobs = [row.job for row in ready_rows]
237
239
 
238
240
  ReadyExecution.objects.using(alias).filter(pk__in=[row.pk for row in ready_rows]).delete()
239
241
  _bulk_create(
@@ -250,7 +252,9 @@ def claim_ready_jobs(
250
252
  def execute_claimed_job(job_id, *, backend_alias="default"):
251
253
  alias = get_database_alias(backend_alias)
252
254
  claimed = (
253
- ClaimedExecution.objects.using(alias).select_related("job", "process").get(job_id=job_id)
255
+ ClaimedExecution.objects.using(alias)
256
+ .select_related("job", "process")
257
+ .get(job_id=job_id, job__backend_alias=backend_alias)
254
258
  )
255
259
  job = claimed.job
256
260
 
@@ -279,7 +283,12 @@ def complete_claimed_job(job_id, return_value, *, backend_alias="default"):
279
283
  alias = get_database_alias(backend_alias)
280
284
 
281
285
  with transaction.atomic(using=alias):
282
- claimed = ClaimedExecution.objects.using(alias).select_related("job").get(job_id=job_id)
286
+ claimed = (
287
+ ClaimedExecution.objects.using(alias)
288
+ .select_for_update()
289
+ .select_related("job")
290
+ .get(job_id=job_id, job__backend_alias=backend_alias)
291
+ )
283
292
  job = claimed.job
284
293
  now = timezone.now()
285
294
  config = load_backend_config(job.backend_alias)
@@ -301,7 +310,12 @@ def fail_claimed_job(job_id, error, *, traceback_text="", backend_alias="default
301
310
  alias = get_database_alias(backend_alias)
302
311
 
303
312
  with transaction.atomic(using=alias):
304
- claimed = ClaimedExecution.objects.using(alias).select_related("job").get(job_id=job_id)
313
+ claimed = (
314
+ ClaimedExecution.objects.using(alias)
315
+ .select_for_update()
316
+ .select_related("job")
317
+ .get(job_id=job_id, job__backend_alias=backend_alias)
318
+ )
305
319
  job = claimed.job
306
320
  claimed.delete(using=alias)
307
321
  FailedExecution.objects.using(alias).create(
@@ -330,16 +344,15 @@ def promote_scheduled_jobs(*, batch_size, backend_alias="default", use_skip_lock
330
344
  with transaction.atomic(using=alias):
331
345
  queryset = (
332
346
  ScheduledExecution.objects.using(alias)
333
- .filter(scheduled_at__lte=now)
347
+ .select_related("job")
348
+ .filter(job__backend_alias=backend_alias, scheduled_at__lte=now)
334
349
  .order_by("scheduled_at", "-priority", "id")
335
350
  )
336
351
  scheduled_rows = list(locked_queryset(queryset, use_skip_locked=use_skip_locked)[:batch_size])
337
352
  if not scheduled_rows:
338
353
  return []
339
354
 
340
- job_ids = [row.job_id for row in scheduled_rows]
341
- jobs_by_id = {job.id: job for job in Job.objects.using(alias).filter(pk__in=job_ids)}
342
- jobs = [jobs_by_id[job_id] for job_id in job_ids]
355
+ jobs = [row.job for row in scheduled_rows]
343
356
 
344
357
  ScheduledExecution.objects.using(alias).filter(
345
358
  pk__in=[row.pk for row in scheduled_rows]
@@ -351,11 +364,49 @@ def promote_scheduled_jobs(*, batch_size, backend_alias="default", use_skip_lock
351
364
  return jobs
352
365
 
353
366
 
367
+ def dispatch_scheduled_job_now(job_id, *, backend_alias="default"):
368
+ alias = get_database_alias(backend_alias)
369
+ config = load_backend_config(backend_alias)
370
+
371
+ with transaction.atomic(using=alias):
372
+ scheduled = locked_queryset(
373
+ ScheduledExecution.objects.using(alias)
374
+ .select_related("job")
375
+ .filter(job_id=job_id, job__backend_alias=backend_alias),
376
+ use_skip_locked=config.use_skip_locked,
377
+ ).first()
378
+ if scheduled is None:
379
+ raise EnqueueError("job is not scheduled")
380
+
381
+ job = scheduled.job
382
+ scheduled.delete(using=alias)
383
+ job.scheduled_at = None
384
+ job.save(using=alias, update_fields=["scheduled_at", "updated_at"])
385
+ dispatched_as = _dispatch_existing_job(job)
386
+
387
+ if dispatched_as == "ready":
388
+ runtime_notify.notify_ready_queues((job.queue_name,), backend_alias=backend_alias)
389
+
390
+ log_event(
391
+ "job.dispatched_now",
392
+ job_id=str(job.id),
393
+ queue_name=job.queue_name,
394
+ priority=job.priority,
395
+ dispatched_as=dispatched_as,
396
+ )
397
+ return job, dispatched_as
398
+
399
+
354
400
  def retry_failed_job(job_id, *, backend_alias="default"):
355
401
  alias = get_database_alias(backend_alias)
356
402
 
357
403
  with transaction.atomic(using=alias):
358
- failed = FailedExecution.objects.using(alias).select_related("job").get(job_id=job_id)
404
+ failed = (
405
+ FailedExecution.objects.using(alias)
406
+ .select_for_update()
407
+ .select_related("job")
408
+ .get(job_id=job_id, job__backend_alias=backend_alias)
409
+ )
359
410
  job = failed.job
360
411
  failed.delete(using=alias)
361
412
  job.return_value = None
@@ -370,15 +421,19 @@ def retry_failed_job(job_id, *, backend_alias="default"):
370
421
  return job
371
422
 
372
423
 
373
- def enqueue_job_again(job_id, *, backend_alias="default"):
424
+ _KEEP_RUN_AFTER = object()
425
+
426
+
427
+ def enqueue_job_again(job_id, *, backend_alias="default", run_after=_KEEP_RUN_AFTER):
374
428
  alias = get_database_alias(backend_alias)
375
- source_job = Job.objects.using(alias).get(pk=job_id)
429
+ source_job = Job.objects.using(alias).get(pk=job_id, backend_alias=backend_alias)
376
430
  task = import_string(source_job.task_path)
431
+ source_run_after = source_job.scheduled_at if run_after is _KEEP_RUN_AFTER else run_after
377
432
  if hasattr(task, "using"):
378
433
  task = task.using(
379
434
  priority=source_job.priority,
380
435
  queue_name=source_job.queue_name,
381
- run_after=source_job.scheduled_at,
436
+ run_after=source_run_after,
382
437
  backend=source_job.backend_alias,
383
438
  )
384
439
  args = list(source_job.payload.get("args", []))
@@ -389,6 +444,7 @@ def enqueue_job_again(job_id, *, backend_alias="default"):
389
444
 
390
445
  def discard_failed_jobs(*, job_ids=None, batch_size=500, backend_alias="default"):
391
446
  alias = get_database_alias(backend_alias)
447
+ config = load_backend_config(backend_alias)
392
448
 
393
449
  with transaction.atomic(using=alias):
394
450
  queryset = (
@@ -396,7 +452,9 @@ def discard_failed_jobs(*, job_ids=None, batch_size=500, backend_alias="default"
396
452
  )
397
453
  if job_ids is not None:
398
454
  queryset = queryset.filter(job_id__in=job_ids)
399
- failed_rows = list(queryset[:batch_size])
455
+ failed_rows = list(
456
+ locked_queryset(queryset, use_skip_locked=config.use_skip_locked)[:batch_size]
457
+ )
400
458
  if not failed_rows:
401
459
  return 0
402
460
 
@@ -465,7 +523,6 @@ def discard_scheduled_jobs(*, job_ids=None, batch_size=500, backend_alias="defau
465
523
  Job.objects.using(alias).filter(pk__in=[row.job_id for row in scheduled_rows]).delete()
466
524
 
467
525
  for job in jobs:
468
- _release_concurrency_slot(job)
469
526
  log_event("job.discarded", job_id=str(job.id), reason="scheduled")
470
527
  return len(jobs)
471
528
 
@@ -4,6 +4,7 @@ import socket
4
4
  import threading
5
5
  import time
6
6
 
7
+ from django.db import connections
7
8
  from django.utils import timezone
8
9
  from datetime import timedelta
9
10
 
@@ -301,8 +302,11 @@ class AsyncSupervisor(Supervisor):
301
302
  def _fail_crashed_runner_jobs(self, runner):
302
303
  if runner.process is None:
303
304
  return
305
+ alias = get_database_alias(self.backend_alias)
304
306
  claimed_job_ids = list(
305
- ClaimedExecution.objects.filter(process=runner.process).values_list("job_id", flat=True)
307
+ ClaimedExecution.objects.using(alias)
308
+ .filter(process=runner.process)
309
+ .values_list("job_id", flat=True)
306
310
  )
307
311
  with app_executor():
308
312
  for job_id in claimed_job_ids:
@@ -430,12 +434,21 @@ class ForkSupervisor(Supervisor):
430
434
  return process
431
435
 
432
436
  def stop(self):
437
+ timeout = max(float(self.config.shutdown_timeout), 0)
433
438
  for pid in tuple(self.children):
434
439
  try:
435
440
  self._killer(pid, signal.SIGTERM)
436
441
  except ProcessLookupError:
437
442
  pass
438
- self.children.clear()
443
+
444
+ self._wait_for_children(timeout)
445
+ for pid in tuple(self.children):
446
+ try:
447
+ self._killer(pid, signal.SIGKILL)
448
+ except ProcessLookupError:
449
+ pass
450
+ self._fail_claimed_jobs_for_pid(pid)
451
+ self.children.pop(pid, None)
439
452
  return super().stop()
440
453
 
441
454
  def register_signal_handlers(self):
@@ -485,6 +498,22 @@ class ForkSupervisor(Supervisor):
485
498
  )
486
499
  return replacement_pid
487
500
 
501
+ def _wait_for_children(self, timeout):
502
+ deadline = time.monotonic() + timeout
503
+ while self.children and time.monotonic() < deadline:
504
+ try:
505
+ pid, _status = self._waitpid(-1, os.WNOHANG)
506
+ except ChildProcessError:
507
+ self.children.clear()
508
+ return None
509
+
510
+ if not pid:
511
+ time.sleep(min(0.05, max(deadline - time.monotonic(), 0)))
512
+ continue
513
+
514
+ self.children.pop(pid, None)
515
+ return None
516
+
488
517
  def poll_once(self):
489
518
  super().poll_once()
490
519
  return self.check_children()
@@ -566,9 +595,25 @@ class ForkSupervisor(Supervisor):
566
595
  return specs
567
596
 
568
597
  def _default_launcher(self, spec):
598
+ connections.close_all()
569
599
  pid = os.fork()
570
600
  if pid == 0:
571
- runner = spec["runner_class"](**spec["kwargs"])
572
- runner.run()
601
+ connections.close_all()
602
+ try:
603
+ runner = spec["runner_class"](**spec["kwargs"])
604
+ self._register_child_signal_handlers(runner)
605
+ runner.run()
606
+ finally:
607
+ connections.close_all()
573
608
  self._exit_fn(0)
609
+ connections.close_all()
574
610
  return pid
611
+
612
+ def _register_child_signal_handlers(self, runner):
613
+ def request_stop(*_args):
614
+ runner.request_stop()
615
+ return True
616
+
617
+ signal.signal(signal.SIGTERM, request_stop)
618
+ signal.signal(signal.SIGINT, request_stop)
619
+ signal.signal(signal.SIGQUIT, lambda *_args: self._exit_fn(1))
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "dj-queue"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Database-backed task queue backend for Django’s Tasks framework."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes