dj-queue 0.10.1__tar.gz → 0.10.3__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.1 → dj_queue-0.10.3}/PKG-INFO +9 -1
- {dj_queue-0.10.1 → dj_queue-0.10.3}/README.md +8 -0
- dj_queue-0.10.3/dj_queue/management/commands/dj_queue.py +106 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/concurrency.py +117 -7
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/jobs.py +117 -13
- {dj_queue-0.10.1 → dj_queue-0.10.3}/pyproject.toml +1 -1
- dj_queue-0.10.1/dj_queue/management/commands/dj_queue.py +0 -44
- {dj_queue-0.10.1 → dj_queue-0.10.3}/LICENSE +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/admin.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/api.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/backend.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/config.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/contrib/asgi.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/contrib/gunicorn.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/contrib/prometheus.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/dashboard.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/db.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/log.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/metrics.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/observability.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/_helpers.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/cleanup.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/operations/recurring.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/base.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/errors.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/notify.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/pool.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/supervisor.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/runtime/worker.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/dashboard.html +1 -1
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/views.py +0 -0
- {dj_queue-0.10.1 → dj_queue-0.10.3}/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.3
|
|
4
4
|
Summary: Database-backed task queue backend for Django’s Tasks framework.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -115,6 +115,11 @@ Run migrations:
|
|
|
115
115
|
python manage.py migrate
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
`python manage.py dj_queue` checks for pending `dj_queue` migrations before it
|
|
119
|
+
starts workers. If another process is applying migrations during startup, the
|
|
120
|
+
runtime waits up to 60 seconds by default instead of starting against a stale
|
|
121
|
+
queue schema.
|
|
122
|
+
|
|
118
123
|
## Quick Start
|
|
119
124
|
|
|
120
125
|
Define a task with Django's `@task` decorator:
|
|
@@ -274,11 +279,14 @@ python manage.py dj_queue --backend <alias>
|
|
|
274
279
|
python manage.py dj_queue --only-work
|
|
275
280
|
python manage.py dj_queue --only-dispatch
|
|
276
281
|
python manage.py dj_queue --skip-recurring
|
|
282
|
+
python manage.py dj_queue --migration-wait-timeout 120
|
|
277
283
|
```
|
|
278
284
|
|
|
279
285
|
Notes:
|
|
280
286
|
|
|
281
287
|
- `fork` is the default standalone mode
|
|
288
|
+
- startup waits up to 60 seconds for pending `dj_queue` migrations to finish;
|
|
289
|
+
use `--migration-wait-timeout 0` to fail fast instead
|
|
282
290
|
- `async` is also supported as a standalone mode and runs supervised actors in threads inside one process
|
|
283
291
|
- `--backend` targets a non-default backend alias
|
|
284
292
|
- `--only-work` starts workers without dispatchers or scheduler
|
|
@@ -89,6 +89,11 @@ Run migrations:
|
|
|
89
89
|
python manage.py migrate
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
`python manage.py dj_queue` checks for pending `dj_queue` migrations before it
|
|
93
|
+
starts workers. If another process is applying migrations during startup, the
|
|
94
|
+
runtime waits up to 60 seconds by default instead of starting against a stale
|
|
95
|
+
queue schema.
|
|
96
|
+
|
|
92
97
|
## Quick Start
|
|
93
98
|
|
|
94
99
|
Define a task with Django's `@task` decorator:
|
|
@@ -248,11 +253,14 @@ python manage.py dj_queue --backend <alias>
|
|
|
248
253
|
python manage.py dj_queue --only-work
|
|
249
254
|
python manage.py dj_queue --only-dispatch
|
|
250
255
|
python manage.py dj_queue --skip-recurring
|
|
256
|
+
python manage.py dj_queue --migration-wait-timeout 120
|
|
251
257
|
```
|
|
252
258
|
|
|
253
259
|
Notes:
|
|
254
260
|
|
|
255
261
|
- `fork` is the default standalone mode
|
|
262
|
+
- startup waits up to 60 seconds for pending `dj_queue` migrations to finish;
|
|
263
|
+
use `--migration-wait-timeout 0` to fail fast instead
|
|
256
264
|
- `async` is also supported as a standalone mode and runs supervised actors in threads inside one process
|
|
257
265
|
- `--backend` targets a non-default backend alias
|
|
258
266
|
- `--only-work` starts workers without dispatchers or scheduler
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
5
|
+
from django.core.management.base import BaseCommand
|
|
6
|
+
from django.core.management.base import CommandError
|
|
7
|
+
from django.db import connections
|
|
8
|
+
from django.db.migrations.executor import MigrationExecutor
|
|
9
|
+
|
|
10
|
+
from dj_queue.config import load_backend_config
|
|
11
|
+
from dj_queue.runtime.supervisor import AsyncSupervisor, ForkSupervisor
|
|
12
|
+
|
|
13
|
+
MIGRATION_WAIT_TIMEOUT = 60
|
|
14
|
+
MIGRATION_WAIT_INTERVAL = 0.5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_supervisor(*, backend_alias, cli_overrides):
|
|
18
|
+
mode = load_backend_config(backend_alias, cli_overrides=cli_overrides).mode
|
|
19
|
+
supervisor_class = AsyncSupervisor if mode == "async" else ForkSupervisor
|
|
20
|
+
return supervisor_class.from_backend_config(
|
|
21
|
+
backend_alias=backend_alias,
|
|
22
|
+
cli_overrides=cli_overrides,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wait_for_dj_queue_migrations(
|
|
27
|
+
database_alias,
|
|
28
|
+
*,
|
|
29
|
+
timeout=MIGRATION_WAIT_TIMEOUT,
|
|
30
|
+
interval=MIGRATION_WAIT_INTERVAL,
|
|
31
|
+
stdout=None,
|
|
32
|
+
):
|
|
33
|
+
if not math.isfinite(timeout) or timeout < 0:
|
|
34
|
+
raise CommandError("dj_queue migration wait timeout must be non-negative")
|
|
35
|
+
|
|
36
|
+
deadline = time.monotonic() + timeout
|
|
37
|
+
announced = False
|
|
38
|
+
while True:
|
|
39
|
+
pending = pending_dj_queue_migrations(database_alias)
|
|
40
|
+
if not pending:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
migration_names = ", ".join(
|
|
44
|
+
f"{migration.app_label}.{migration.name}" for migration, _backwards in pending
|
|
45
|
+
)
|
|
46
|
+
if stdout is not None and not announced:
|
|
47
|
+
stdout.write(
|
|
48
|
+
f"waiting for dj_queue migrations on database {database_alias!r}: {migration_names}"
|
|
49
|
+
)
|
|
50
|
+
announced = True
|
|
51
|
+
|
|
52
|
+
if time.monotonic() >= deadline:
|
|
53
|
+
raise CommandError(
|
|
54
|
+
f"dj_queue migrations are pending on database {database_alias!r}: {migration_names}; "
|
|
55
|
+
f"run manage.py migrate dj_queue --database {database_alias} before starting dj_queue"
|
|
56
|
+
)
|
|
57
|
+
time.sleep(min(interval, max(deadline - time.monotonic(), 0)))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pending_dj_queue_migrations(database_alias):
|
|
61
|
+
executor = MigrationExecutor(connections[database_alias])
|
|
62
|
+
targets = [node for node in executor.loader.graph.leaf_nodes() if node[0] == "dj_queue"]
|
|
63
|
+
return [
|
|
64
|
+
(migration, backwards)
|
|
65
|
+
for migration, backwards in executor.migration_plan(targets)
|
|
66
|
+
if migration.app_label == "dj_queue"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Command(BaseCommand):
|
|
71
|
+
help = "Start the dj_queue supervisor"
|
|
72
|
+
|
|
73
|
+
def add_arguments(self, parser):
|
|
74
|
+
parser.add_argument("--backend", default="default")
|
|
75
|
+
parser.add_argument("--config")
|
|
76
|
+
parser.add_argument("--mode", choices=("fork", "async"))
|
|
77
|
+
parser.add_argument("--only-work", action="store_true")
|
|
78
|
+
parser.add_argument("--only-dispatch", action="store_true")
|
|
79
|
+
parser.add_argument("--skip-recurring", action="store_true")
|
|
80
|
+
parser.add_argument("--migration-wait-timeout", type=float, default=MIGRATION_WAIT_TIMEOUT)
|
|
81
|
+
|
|
82
|
+
def handle(self, *args, **options):
|
|
83
|
+
cli_overrides = {
|
|
84
|
+
"config": options["config"],
|
|
85
|
+
"mode": options["mode"],
|
|
86
|
+
"only_work": options["only_work"],
|
|
87
|
+
"only_dispatch": options["only_dispatch"],
|
|
88
|
+
"skip_recurring": options["skip_recurring"],
|
|
89
|
+
}
|
|
90
|
+
try:
|
|
91
|
+
config = load_backend_config(
|
|
92
|
+
options["backend"],
|
|
93
|
+
cli_overrides=cli_overrides,
|
|
94
|
+
)
|
|
95
|
+
wait_for_dj_queue_migrations(
|
|
96
|
+
config.database_alias,
|
|
97
|
+
timeout=options["migration_wait_timeout"],
|
|
98
|
+
stdout=self.stdout,
|
|
99
|
+
)
|
|
100
|
+
supervisor = build_supervisor(
|
|
101
|
+
backend_alias=options["backend"],
|
|
102
|
+
cli_overrides=cli_overrides,
|
|
103
|
+
)
|
|
104
|
+
except ImproperlyConfigured as exc:
|
|
105
|
+
raise CommandError(str(exc)) from exc
|
|
106
|
+
supervisor.run()
|
|
@@ -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,
|
|
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,11 +241,12 @@ 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
|
|
249
|
+
with _operation_atomic(alias):
|
|
217
250
|
queryset = (
|
|
218
251
|
BlockedExecution.objects.using(alias)
|
|
219
252
|
.select_related("job")
|
|
@@ -229,7 +262,15 @@ def unblock_next_blocked_job(
|
|
|
229
262
|
return None
|
|
230
263
|
|
|
231
264
|
slot_acquired = False
|
|
232
|
-
if
|
|
265
|
+
if release_slot:
|
|
266
|
+
slot_acquired = _handoff_released_claimed_slot(
|
|
267
|
+
alias,
|
|
268
|
+
key,
|
|
269
|
+
limit=limit,
|
|
270
|
+
duration_seconds=duration_seconds,
|
|
271
|
+
now=now,
|
|
272
|
+
)
|
|
273
|
+
elif handoff_released_slot:
|
|
233
274
|
slot_acquired = _consume_released_semaphore_slot(
|
|
234
275
|
alias,
|
|
235
276
|
key,
|
|
@@ -238,7 +279,7 @@ def unblock_next_blocked_job(
|
|
|
238
279
|
now=now,
|
|
239
280
|
)
|
|
240
281
|
|
|
241
|
-
if not slot_acquired:
|
|
282
|
+
if not slot_acquired and not release_slot:
|
|
242
283
|
slot_acquired = semaphore_acquire(
|
|
243
284
|
key,
|
|
244
285
|
limit=limit,
|
|
@@ -261,7 +302,7 @@ def unblock_next_blocked_job(
|
|
|
261
302
|
job = blocked.job
|
|
262
303
|
queue_name = blocked.queue_name
|
|
263
304
|
priority = blocked.priority
|
|
264
|
-
|
|
305
|
+
_create_ready_execution_after_blocked_consume(
|
|
265
306
|
alias,
|
|
266
307
|
job=job,
|
|
267
308
|
backend_alias=backend_alias,
|
|
@@ -279,6 +320,71 @@ def unblock_next_blocked_job(
|
|
|
279
320
|
return job
|
|
280
321
|
|
|
281
322
|
|
|
323
|
+
def _create_ready_execution_after_blocked_consume(
|
|
324
|
+
alias,
|
|
325
|
+
*,
|
|
326
|
+
job,
|
|
327
|
+
backend_alias,
|
|
328
|
+
queue_name,
|
|
329
|
+
priority,
|
|
330
|
+
ready_at,
|
|
331
|
+
):
|
|
332
|
+
_lock_active_pauses(alias, backend_alias, {queue_name})
|
|
333
|
+
connection = connections[alias]
|
|
334
|
+
quote = connection.ops.quote_name
|
|
335
|
+
ready_table = quote(ReadyExecution._meta.db_table)
|
|
336
|
+
job_id_column = quote(ReadyExecution._meta.get_field("job").column)
|
|
337
|
+
backend_alias_column = quote(ReadyExecution._meta.get_field("backend_alias").column)
|
|
338
|
+
queue_name_column = quote(ReadyExecution._meta.get_field("queue_name").column)
|
|
339
|
+
priority_column = quote(ReadyExecution._meta.get_field("priority").column)
|
|
340
|
+
created_at_column = quote(ReadyExecution._meta.get_field("created_at").column)
|
|
341
|
+
latency_started_at_column = quote(ReadyExecution._meta.get_field("latency_started_at").column)
|
|
342
|
+
job_id = Job._meta.get_field("id").get_db_prep_value(
|
|
343
|
+
job.pk,
|
|
344
|
+
connection=connection,
|
|
345
|
+
prepared=False,
|
|
346
|
+
)
|
|
347
|
+
state_models = (ReadyExecution, ScheduledExecution, ClaimedExecution, FailedExecution)
|
|
348
|
+
state_checks = " AND ".join(
|
|
349
|
+
_state_absence_sql(model, job_id_column=job_id_column, quote=quote) for model in state_models
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
with connection.cursor() as cursor:
|
|
353
|
+
cursor.execute(
|
|
354
|
+
f"""
|
|
355
|
+
INSERT INTO {ready_table} (
|
|
356
|
+
{job_id_column},
|
|
357
|
+
{backend_alias_column},
|
|
358
|
+
{queue_name_column},
|
|
359
|
+
{priority_column},
|
|
360
|
+
{created_at_column},
|
|
361
|
+
{latency_started_at_column}
|
|
362
|
+
)
|
|
363
|
+
SELECT %s, %s, %s, %s, %s, %s
|
|
364
|
+
WHERE {state_checks}
|
|
365
|
+
""",
|
|
366
|
+
[
|
|
367
|
+
job_id,
|
|
368
|
+
backend_alias,
|
|
369
|
+
queue_name,
|
|
370
|
+
priority,
|
|
371
|
+
ready_at,
|
|
372
|
+
ready_at,
|
|
373
|
+
*([job_id] * len(state_models)),
|
|
374
|
+
],
|
|
375
|
+
)
|
|
376
|
+
created = cursor.rowcount
|
|
377
|
+
|
|
378
|
+
if created != 1:
|
|
379
|
+
raise EnqueueError(f"job {job.id} already has an execution-state row")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _state_absence_sql(model, *, job_id_column, quote):
|
|
383
|
+
state_table = quote(model._meta.db_table)
|
|
384
|
+
state_job_id_column = quote(model._meta.get_field("job").column)
|
|
385
|
+
return f"NOT EXISTS (SELECT 1 FROM {state_table} WHERE {state_table}.{state_job_id_column} = %s)"
|
|
386
|
+
|
|
387
|
+
|
|
282
388
|
def cleanup_expired_semaphores(*, backend_alias="default"):
|
|
283
389
|
alias = get_database_alias(backend_alias)
|
|
284
390
|
claimed_concurrency_keys = (
|
|
@@ -397,3 +503,7 @@ def _positive_int_option(value, name):
|
|
|
397
503
|
if number <= 0:
|
|
398
504
|
raise EnqueueError(f"{name} must be a positive integer")
|
|
399
505
|
return number
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _operation_atomic(alias):
|
|
509
|
+
return transaction.atomic(using=alias, savepoint=not connections[alias].in_atomic_block)
|
|
@@ -32,7 +32,6 @@ from dj_queue.operations._helpers import (
|
|
|
32
32
|
_ensure_no_other_execution_state,
|
|
33
33
|
_consume_selected_rows,
|
|
34
34
|
_create_blocked_execution,
|
|
35
|
-
_create_ready_execution,
|
|
36
35
|
_create_ready_execution_locked,
|
|
37
36
|
_create_scheduled_execution,
|
|
38
37
|
_exclude_active_pauses,
|
|
@@ -110,7 +109,12 @@ def enqueue_job_with_dispatch(task, args, kwargs, *, backend_alias="default"):
|
|
|
110
109
|
scheduled_at=task.run_after,
|
|
111
110
|
concurrency_key=concurrency_key,
|
|
112
111
|
)
|
|
113
|
-
dispatch_outcome = _dispatch_job(
|
|
112
|
+
dispatch_outcome = _dispatch_job(
|
|
113
|
+
job,
|
|
114
|
+
task=task,
|
|
115
|
+
backend_alias=backend_alias,
|
|
116
|
+
check_conflicts=False,
|
|
117
|
+
)
|
|
114
118
|
|
|
115
119
|
if dispatch_outcome.should_notify:
|
|
116
120
|
notify_ready_queues_on_commit((job.queue_name,), backend_alias=backend_alias)
|
|
@@ -429,15 +433,13 @@ def _complete_claimed_job(job, return_value, *, backend_alias="default", task=No
|
|
|
429
433
|
|
|
430
434
|
with transaction.atomic(using=alias):
|
|
431
435
|
_delete_claimed_execution(alias, job.id)
|
|
432
|
-
_ensure_no_other_execution_state(alias, job, ignored_models=(ClaimedExecution,))
|
|
433
436
|
now = timezone.now()
|
|
434
437
|
config = load_backend_config(job.backend_alias)
|
|
435
438
|
|
|
436
439
|
if config.preserve_finished_jobs:
|
|
437
|
-
job
|
|
438
|
-
job.return_value = return_value
|
|
439
|
-
job.save(using=alias, update_fields=["finished_at", "return_value", "updated_at"])
|
|
440
|
+
_finish_job_if_no_execution_state(alias, job, return_value, finished_at=now)
|
|
440
441
|
else:
|
|
442
|
+
_ensure_no_other_execution_state(alias, job, ignored_models=(ClaimedExecution,))
|
|
441
443
|
job.delete(using=alias)
|
|
442
444
|
|
|
443
445
|
_release_concurrency_slot(job, task=task)
|
|
@@ -910,7 +912,7 @@ def _dispatch_existing_job(job):
|
|
|
910
912
|
return _dispatch_job(job, task=task, backend_alias=job.backend_alias)
|
|
911
913
|
|
|
912
914
|
|
|
913
|
-
def _dispatch_job(job, *, task, backend_alias, now=None):
|
|
915
|
+
def _dispatch_job(job, *, task, backend_alias, now=None, check_conflicts=True):
|
|
914
916
|
alias = get_database_alias(backend_alias)
|
|
915
917
|
if now is None:
|
|
916
918
|
now = timezone.now()
|
|
@@ -925,11 +927,13 @@ def _dispatch_job(job, *, task, backend_alias, now=None):
|
|
|
925
927
|
return DispatchOutcome.SCHEDULED
|
|
926
928
|
|
|
927
929
|
if not job.concurrency_key:
|
|
928
|
-
|
|
930
|
+
_create_ready_execution_locked(
|
|
929
931
|
alias,
|
|
930
932
|
job=job,
|
|
931
933
|
backend_alias=backend_alias,
|
|
934
|
+
queue_name=job.queue_name,
|
|
932
935
|
ready_at=now,
|
|
936
|
+
check_conflicts=check_conflicts,
|
|
933
937
|
)
|
|
934
938
|
return DispatchOutcome.READY
|
|
935
939
|
|
|
@@ -979,6 +983,19 @@ def _release_concurrency_slot(job, *, task=None):
|
|
|
979
983
|
limit = _semaphore_limit(job) or 1
|
|
980
984
|
duration_seconds = config.default_concurrency_duration
|
|
981
985
|
|
|
986
|
+
if (
|
|
987
|
+
unblock_next_blocked_job(
|
|
988
|
+
job.concurrency_key,
|
|
989
|
+
limit=limit,
|
|
990
|
+
duration_seconds=duration_seconds,
|
|
991
|
+
backend_alias=job.backend_alias,
|
|
992
|
+
use_skip_locked=config.use_skip_locked,
|
|
993
|
+
release_slot=True,
|
|
994
|
+
)
|
|
995
|
+
is not None
|
|
996
|
+
):
|
|
997
|
+
return
|
|
998
|
+
|
|
982
999
|
semaphore_release(
|
|
983
1000
|
job.concurrency_key,
|
|
984
1001
|
limit=limit,
|
|
@@ -1064,11 +1081,19 @@ def _select_ready_rows(queryset, *, limit, queues, use_skip_locked):
|
|
|
1064
1081
|
ordered_selectors = selectors if star_index is None else selectors[:star_index]
|
|
1065
1082
|
|
|
1066
1083
|
if ordered_selectors:
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1084
|
+
if _selectors_are_exact(ordered_selectors):
|
|
1085
|
+
rows = _select_exact_selector_rows(
|
|
1086
|
+
queryset.exclude(pk__in=selected_ids),
|
|
1087
|
+
ordered_selectors,
|
|
1088
|
+
limit=limit,
|
|
1089
|
+
use_skip_locked=use_skip_locked,
|
|
1090
|
+
)
|
|
1091
|
+
else:
|
|
1092
|
+
ordered = _ordered_selector_rows_queryset(
|
|
1093
|
+
queryset.exclude(pk__in=selected_ids),
|
|
1094
|
+
ordered_selectors,
|
|
1095
|
+
)
|
|
1096
|
+
rows = list(locked_queryset(ordered, use_skip_locked=use_skip_locked)[:limit])
|
|
1072
1097
|
selected_rows.extend(rows)
|
|
1073
1098
|
selected_ids.update(row.pk for row in rows)
|
|
1074
1099
|
|
|
@@ -1082,6 +1107,22 @@ def _select_ready_rows(queryset, *, limit, queues, use_skip_locked):
|
|
|
1082
1107
|
return selected_rows
|
|
1083
1108
|
|
|
1084
1109
|
|
|
1110
|
+
def _select_exact_selector_rows(queryset, selectors, *, limit, use_skip_locked):
|
|
1111
|
+
selected_rows = []
|
|
1112
|
+
for selector in dict.fromkeys(selectors):
|
|
1113
|
+
remaining = limit - len(selected_rows)
|
|
1114
|
+
if remaining <= 0:
|
|
1115
|
+
break
|
|
1116
|
+
ordered = queryset.filter(queue_name=selector).order_by("-priority", "id")
|
|
1117
|
+
rows = list(locked_queryset(ordered, use_skip_locked=use_skip_locked)[:remaining])
|
|
1118
|
+
selected_rows.extend(rows)
|
|
1119
|
+
return selected_rows
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _selectors_are_exact(selectors):
|
|
1123
|
+
return all(selector != "*" and not selector.endswith("*") for selector in selectors)
|
|
1124
|
+
|
|
1125
|
+
|
|
1085
1126
|
def _ordered_selector_rows_queryset(queryset, selectors):
|
|
1086
1127
|
filtered = _filter_queue_selectors(queryset, selectors)
|
|
1087
1128
|
selector_rank = Case(
|
|
@@ -1141,6 +1182,69 @@ def _delete_claimed_execution(alias, job_id):
|
|
|
1141
1182
|
raise ClaimedExecution.DoesNotExist
|
|
1142
1183
|
|
|
1143
1184
|
|
|
1185
|
+
def _finish_job_if_no_execution_state(alias, job, return_value, *, finished_at):
|
|
1186
|
+
connection = connections[alias]
|
|
1187
|
+
quote = connection.ops.quote_name
|
|
1188
|
+
jobs_table = quote(Job._meta.db_table)
|
|
1189
|
+
job_id_column = quote(Job._meta.get_field("id").column)
|
|
1190
|
+
backend_alias_column = quote(Job._meta.get_field("backend_alias").column)
|
|
1191
|
+
finished_at_column = quote(Job._meta.get_field("finished_at").column)
|
|
1192
|
+
return_value_column = quote(Job._meta.get_field("return_value").column)
|
|
1193
|
+
updated_at_column = quote(Job._meta.get_field("updated_at").column)
|
|
1194
|
+
state_checks = " AND ".join(
|
|
1195
|
+
_state_absence_sql(model, jobs_table=jobs_table, job_id_column=job_id_column, quote=quote)
|
|
1196
|
+
for model in (
|
|
1197
|
+
ReadyExecution,
|
|
1198
|
+
ScheduledExecution,
|
|
1199
|
+
BlockedExecution,
|
|
1200
|
+
FailedExecution,
|
|
1201
|
+
)
|
|
1202
|
+
)
|
|
1203
|
+
job_id = Job._meta.get_field("id").get_db_prep_value(
|
|
1204
|
+
job.pk,
|
|
1205
|
+
connection=connection,
|
|
1206
|
+
prepared=False,
|
|
1207
|
+
)
|
|
1208
|
+
prepared_return_value = Job._meta.get_field("return_value").get_db_prep_save(
|
|
1209
|
+
return_value,
|
|
1210
|
+
connection=connection,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
with connection.cursor() as cursor:
|
|
1214
|
+
cursor.execute(
|
|
1215
|
+
f"""
|
|
1216
|
+
UPDATE {jobs_table}
|
|
1217
|
+
SET
|
|
1218
|
+
{finished_at_column} = %s,
|
|
1219
|
+
{return_value_column} = %s,
|
|
1220
|
+
{updated_at_column} = %s
|
|
1221
|
+
WHERE
|
|
1222
|
+
{jobs_table}.{job_id_column} = %s
|
|
1223
|
+
AND {jobs_table}.{backend_alias_column} = %s
|
|
1224
|
+
AND {state_checks}
|
|
1225
|
+
""",
|
|
1226
|
+
[finished_at, prepared_return_value, finished_at, job_id, job.backend_alias],
|
|
1227
|
+
)
|
|
1228
|
+
updated = cursor.rowcount
|
|
1229
|
+
|
|
1230
|
+
if updated != 1:
|
|
1231
|
+
raise EnqueueError(f"job {job.id} already has an execution-state row")
|
|
1232
|
+
job.finished_at = finished_at
|
|
1233
|
+
job.return_value = return_value
|
|
1234
|
+
job.updated_at = finished_at
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _state_absence_sql(model, *, jobs_table, job_id_column, quote):
|
|
1238
|
+
state_table = quote(model._meta.db_table)
|
|
1239
|
+
state_job_id_column = quote(model._meta.get_field("job").column)
|
|
1240
|
+
return (
|
|
1241
|
+
f"NOT EXISTS ("
|
|
1242
|
+
f"SELECT 1 FROM {state_table} "
|
|
1243
|
+
f"WHERE {state_table}.{state_job_id_column} = {jobs_table}.{job_id_column}"
|
|
1244
|
+
f")"
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
|
|
1144
1248
|
def _bulk_create(alias, model, objects):
|
|
1145
1249
|
if not objects:
|
|
1146
1250
|
return None
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from django.core.exceptions import ImproperlyConfigured
|
|
2
|
-
from django.core.management.base import BaseCommand
|
|
3
|
-
from django.core.management.base import CommandError
|
|
4
|
-
|
|
5
|
-
from dj_queue.config import load_backend_config
|
|
6
|
-
from dj_queue.runtime.supervisor import AsyncSupervisor, ForkSupervisor
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def build_supervisor(*, backend_alias, cli_overrides):
|
|
10
|
-
mode = load_backend_config(backend_alias, cli_overrides=cli_overrides).mode
|
|
11
|
-
supervisor_class = AsyncSupervisor if mode == "async" else ForkSupervisor
|
|
12
|
-
return supervisor_class.from_backend_config(
|
|
13
|
-
backend_alias=backend_alias,
|
|
14
|
-
cli_overrides=cli_overrides,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Command(BaseCommand):
|
|
19
|
-
help = "Start the dj_queue supervisor"
|
|
20
|
-
|
|
21
|
-
def add_arguments(self, parser):
|
|
22
|
-
parser.add_argument("--backend", default="default")
|
|
23
|
-
parser.add_argument("--config")
|
|
24
|
-
parser.add_argument("--mode", choices=("fork", "async"))
|
|
25
|
-
parser.add_argument("--only-work", action="store_true")
|
|
26
|
-
parser.add_argument("--only-dispatch", action="store_true")
|
|
27
|
-
parser.add_argument("--skip-recurring", action="store_true")
|
|
28
|
-
|
|
29
|
-
def handle(self, *args, **options):
|
|
30
|
-
cli_overrides = {
|
|
31
|
-
"config": options["config"],
|
|
32
|
-
"mode": options["mode"],
|
|
33
|
-
"only_work": options["only_work"],
|
|
34
|
-
"only_dispatch": options["only_dispatch"],
|
|
35
|
-
"skip_recurring": options["skip_recurring"],
|
|
36
|
-
}
|
|
37
|
-
try:
|
|
38
|
-
supervisor = build_supervisor(
|
|
39
|
-
backend_alias=options["backend"],
|
|
40
|
-
cli_overrides=cli_overrides,
|
|
41
|
-
)
|
|
42
|
-
except ImproperlyConfigured as exc:
|
|
43
|
-
raise CommandError(str(exc)) from exc
|
|
44
|
-
supervisor.run()
|
|
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
|
|
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
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/migrations/0003_recurringtask_recurringexecution.py
RENAMED
|
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
|
|
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
|
|
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
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html
RENAMED
|
File without changes
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html
RENAMED
|
File without changes
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html
RENAMED
|
File without changes
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -586,8 +586,8 @@
|
|
|
586
586
|
<tr class="{% cycle 'row1' 'row2' %}">
|
|
587
587
|
<th class="djq-col-name"><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=ready">{{ queue.name }}</a></th>
|
|
588
588
|
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=ready">{{ queue.ready_count }}</a></td>
|
|
589
|
-
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=claimed">{{ queue.claimed_count }}</a></td>
|
|
590
589
|
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=scheduled">{{ queue.scheduled_count }}</a></td>
|
|
590
|
+
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=claimed">{{ queue.claimed_count }}</a></td>
|
|
591
591
|
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=blocked">{{ queue.blocked_count }}</a></td>
|
|
592
592
|
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=failed">{{ queue.failed_count }}</a></td>
|
|
593
593
|
<td><a href="{% url 'admin:dj_queue_dashboard_queue' queue.name %}?backend={{ backend_alias }}&state=finished">{{ queue.finished_count }}</a></td>
|
{dj_queue-0.10.1 → dj_queue-0.10.3}/dj_queue/templates/admin/dj_queue/includes/fieldset.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|