dj-queue 0.14.1__tar.gz → 0.14.2__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.14.1 → dj_queue-0.14.2}/PKG-INFO +18 -10
- {dj_queue-0.14.1 → dj_queue-0.14.2}/README.md +17 -9
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/admin.py +93 -95
- dj_queue-0.14.2/dj_queue/admin_permissions.py +27 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/api.py +7 -7
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/backend.py +2 -2
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/config.py +42 -34
- dj_queue-0.14.2/dj_queue/dashboard.py +466 -0
- dj_queue-0.14.2/dj_queue/dashboard_actions.py +71 -0
- dj_queue-0.14.2/dj_queue/dashboard_tables.py +640 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/db.py +8 -4
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/exceptions.py +4 -0
- dj_queue-0.14.2/dj_queue/health.py +258 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/hooks.py +2 -1
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/commands/dj_queue.py +3 -1
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/commands/dj_queue_health.py +4 -3
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/commands/dj_queue_postgres_autovacuum.py +2 -2
- dj_queue-0.14.2/dj_queue/metrics.py +201 -0
- dj_queue-0.14.2/dj_queue/observability.py +224 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/_helpers.py +18 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/claiming.py +6 -4
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/cleanup.py +10 -7
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/concurrency.py +194 -185
- dj_queue-0.14.2/dj_queue/operations/dispatch.py +200 -0
- dj_queue-0.14.2/dj_queue/operations/enqueue.py +253 -0
- dj_queue-0.14.2/dj_queue/operations/execution.py +263 -0
- dj_queue-0.14.2/dj_queue/operations/jobs.py +640 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/queues.py +5 -5
- dj_queue-0.14.2/dj_queue/operations/recovery.py +247 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/recurring.py +55 -28
- dj_queue-0.14.2/dj_queue/postgres_diagnostics.py +241 -0
- dj_queue-0.14.2/dj_queue/reads/controls.py +120 -0
- dj_queue-0.14.2/dj_queue/reads/jobs.py +49 -0
- dj_queue-0.14.2/dj_queue/reads/processes.py +161 -0
- dj_queue-0.14.2/dj_queue/reads/queues.py +181 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/base.py +34 -12
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/dispatcher.py +6 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/errors.py +3 -3
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/notify.py +32 -20
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/scheduler.py +9 -1
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/supervisor.py +12 -1
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/worker.py +23 -8
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_queue_controls.html +3 -1
- dj_queue-0.14.2/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/wakeup.py +9 -4
- {dj_queue-0.14.1 → dj_queue-0.14.2}/pyproject.toml +2 -2
- dj_queue-0.14.1/dj_queue/dashboard.py +0 -1343
- dj_queue-0.14.1/dj_queue/dashboard_actions.py +0 -140
- dj_queue-0.14.1/dj_queue/metrics.py +0 -232
- dj_queue-0.14.1/dj_queue/observability.py +0 -991
- dj_queue-0.14.1/dj_queue/operations/jobs.py +0 -1653
- {dj_queue-0.14.1 → dj_queue-0.14.2}/LICENSE +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/apps.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/contrib/asgi.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/contrib/gunicorn.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/contrib/prometheus.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/cron.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/log.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0011_remove_blockedexecution_djq_bl_b_conc_idx_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0012_job_djq_jobs_b_queue_id_idx_job_djq_jobs_b_conc_idx_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0013_failedexecution_retry_at_and_more.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0014_semaphore_active_count.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0015_job_concurrency_duration.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0016_job_concurrency_limit.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0017_job_concurrency_on_conflict.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/0018_recurringexecution_intended_job_id.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/py.typed +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.14.1/dj_queue/templatetags → dj_queue-0.14.2/dj_queue/reads}/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/routers.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/pool.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/__init__.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/common.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/mysql.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/postgres.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/sqlite.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/sql/state.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/static/admin/dj_queue/breadcrumbs.css +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/task_results.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/urls.py +0 -0
- {dj_queue-0.14.1 → dj_queue-0.14.2}/dj_queue/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dj-queue
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.2
|
|
4
4
|
Summary: Database-backed task queue backend for Django’s Tasks framework.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -425,7 +425,7 @@ application deployment, so keep your task import paths and accepted argument
|
|
|
425
425
|
shapes compatible. Use a new task name when a payload change cannot be backward
|
|
426
426
|
compatible.
|
|
427
427
|
|
|
428
|
-
Maintainers can
|
|
428
|
+
Maintainers can check a candidate rollout against two immutable revisions:
|
|
429
429
|
|
|
430
430
|
```bash
|
|
431
431
|
bin/prerelease.py \
|
|
@@ -435,14 +435,22 @@ bin/prerelease.py \
|
|
|
435
435
|
--django '>=6.0,<6.1'
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
-
The command
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
438
|
+
The command builds isolated wheels, applies N migrations while an N−1 producer
|
|
439
|
+
and supervisor run, then checks both cross-version producer/worker directions
|
|
440
|
+
and N-only operation. Each phase must complete immediate, scheduled, limited,
|
|
441
|
+
bulk, recurring, and fail-once/retry work before its deadline.
|
|
442
|
+
|
|
443
|
+
The check requires one shared rollout protocol and the same Django version.
|
|
444
|
+
It verifies planned side effects, queue drain, deep health, and clean shutdown.
|
|
445
|
+
It creates a fresh database and drops it before reporting success; use
|
|
446
|
+
`--keep-database` to retain it for inspection. Wheels, hashes, phase results,
|
|
447
|
+
migration timing, and logs stay under `benchmark-results/`.
|
|
448
|
+
|
|
449
|
+
The manual/reusable **Mixed-version upgrade check** workflow uses hosted runners
|
|
450
|
+
for PostgreSQL 14, MySQL 8.0, and MariaDB 10.6 with Django 6.0. Run this check for
|
|
451
|
+
upgrade-sensitive changes; backend-specific DDL changes may need extra database
|
|
452
|
+
versions. This is a correctness check, not a capacity or latency benchmark.
|
|
453
|
+
Use the [benchmark tools](docs/benchmarks.md) for performance comparisons.
|
|
446
454
|
|
|
447
455
|
## Recurring Tasks
|
|
448
456
|
|
|
@@ -398,7 +398,7 @@ application deployment, so keep your task import paths and accepted argument
|
|
|
398
398
|
shapes compatible. Use a new task name when a payload change cannot be backward
|
|
399
399
|
compatible.
|
|
400
400
|
|
|
401
|
-
Maintainers can
|
|
401
|
+
Maintainers can check a candidate rollout against two immutable revisions:
|
|
402
402
|
|
|
403
403
|
```bash
|
|
404
404
|
bin/prerelease.py \
|
|
@@ -408,14 +408,22 @@ bin/prerelease.py \
|
|
|
408
408
|
--django '>=6.0,<6.1'
|
|
409
409
|
```
|
|
410
410
|
|
|
411
|
-
The command
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
The command builds isolated wheels, applies N migrations while an N−1 producer
|
|
412
|
+
and supervisor run, then checks both cross-version producer/worker directions
|
|
413
|
+
and N-only operation. Each phase must complete immediate, scheduled, limited,
|
|
414
|
+
bulk, recurring, and fail-once/retry work before its deadline.
|
|
415
|
+
|
|
416
|
+
The check requires one shared rollout protocol and the same Django version.
|
|
417
|
+
It verifies planned side effects, queue drain, deep health, and clean shutdown.
|
|
418
|
+
It creates a fresh database and drops it before reporting success; use
|
|
419
|
+
`--keep-database` to retain it for inspection. Wheels, hashes, phase results,
|
|
420
|
+
migration timing, and logs stay under `benchmark-results/`.
|
|
421
|
+
|
|
422
|
+
The manual/reusable **Mixed-version upgrade check** workflow uses hosted runners
|
|
423
|
+
for PostgreSQL 14, MySQL 8.0, and MariaDB 10.6 with Django 6.0. Run this check for
|
|
424
|
+
upgrade-sensitive changes; backend-specific DDL changes may need extra database
|
|
425
|
+
versions. This is a correctness check, not a capacity or latency benchmark.
|
|
426
|
+
Use the [benchmark tools](docs/benchmarks.md) for performance comparisons.
|
|
419
427
|
|
|
420
428
|
## Recurring Tasks
|
|
421
429
|
|
|
@@ -10,7 +10,12 @@ from django.utils import timezone
|
|
|
10
10
|
from django.utils.html import format_html
|
|
11
11
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
12
12
|
|
|
13
|
-
from dj_queue import dashboard, dashboard_actions
|
|
13
|
+
from dj_queue import dashboard, dashboard_actions
|
|
14
|
+
from dj_queue.admin_permissions import (
|
|
15
|
+
has_action_permission,
|
|
16
|
+
permitted_actions,
|
|
17
|
+
require_action_permission,
|
|
18
|
+
)
|
|
14
19
|
from dj_queue.api import QueueInfo, unschedule_recurring_task
|
|
15
20
|
from dj_queue.db import get_database_alias
|
|
16
21
|
from dj_queue.exceptions import EnqueueError
|
|
@@ -23,9 +28,10 @@ from dj_queue.models import (
|
|
|
23
28
|
RecurringTask,
|
|
24
29
|
Semaphore,
|
|
25
30
|
)
|
|
31
|
+
from dj_queue.operations.dispatch import DispatchOutcome
|
|
26
32
|
from dj_queue.operations.jobs import (
|
|
27
|
-
DispatchOutcome,
|
|
28
33
|
discard_failed_job,
|
|
34
|
+
discard_failed_jobs,
|
|
29
35
|
dispatch_scheduled_job_now,
|
|
30
36
|
enqueue_job_again,
|
|
31
37
|
retry_failed_job,
|
|
@@ -37,6 +43,7 @@ from dj_queue.queue_state import (
|
|
|
37
43
|
is_queue_state,
|
|
38
44
|
status_rank_expression,
|
|
39
45
|
)
|
|
46
|
+
from dj_queue.reads import controls, processes
|
|
40
47
|
|
|
41
48
|
ADMIN_ACTION_ERRORS = (
|
|
42
49
|
EnqueueError,
|
|
@@ -160,7 +167,9 @@ class DashboardAdmin(admin.ModelAdmin):
|
|
|
160
167
|
context = {
|
|
161
168
|
**self.admin_site.each_context(request),
|
|
162
169
|
**queue_context,
|
|
163
|
-
"job_actions":
|
|
170
|
+
"job_actions": permitted_actions(
|
|
171
|
+
request.user, dashboard_actions.job_actions_for_state(state)
|
|
172
|
+
),
|
|
164
173
|
"title": "dj_queue",
|
|
165
174
|
"subtitle": queue_name,
|
|
166
175
|
}
|
|
@@ -198,6 +207,9 @@ class DashboardAdmin(admin.ModelAdmin):
|
|
|
198
207
|
def _post_action_response(self, request, operation, fallback_url):
|
|
199
208
|
if request.method != "POST":
|
|
200
209
|
return HttpResponseNotAllowed(["POST"])
|
|
210
|
+
action = request.POST.get("action")
|
|
211
|
+
if action:
|
|
212
|
+
require_action_permission(request.user, action)
|
|
201
213
|
try:
|
|
202
214
|
message = operation()
|
|
203
215
|
except ADMIN_POST_ACTION_ERRORS as exc:
|
|
@@ -243,13 +255,14 @@ class HiddenSidebarAdminMixin:
|
|
|
243
255
|
if request.method == "POST":
|
|
244
256
|
action = request.POST.get("_djq_object_action")
|
|
245
257
|
if action and obj is not None:
|
|
258
|
+
require_action_permission(request.user, action)
|
|
246
259
|
return self.handle_change_action(request, obj, action)
|
|
247
260
|
|
|
248
261
|
extra_context = {
|
|
249
262
|
**(extra_context or {}),
|
|
250
263
|
"dashboard_url": self._dashboard_url(request),
|
|
251
264
|
"changelist_url": self._changelist_url(backend_alias=self._backend_alias(request)),
|
|
252
|
-
"change_actions": self.get_change_actions(request, obj),
|
|
265
|
+
"change_actions": permitted_actions(request.user, self.get_change_actions(request, obj)),
|
|
253
266
|
}
|
|
254
267
|
return super().changeform_view(
|
|
255
268
|
request,
|
|
@@ -334,18 +347,18 @@ class HiddenSidebarAdminMixin:
|
|
|
334
347
|
operation,
|
|
335
348
|
success_message,
|
|
336
349
|
error_message,
|
|
337
|
-
|
|
338
|
-
|
|
350
|
+
success_url,
|
|
351
|
+
error_url,
|
|
339
352
|
):
|
|
340
353
|
try:
|
|
341
354
|
result = operation()
|
|
342
355
|
except ADMIN_ACTION_ERRORS as exc:
|
|
343
356
|
self.message_user(request, f"{error_message}: {exc}", level=messages.ERROR)
|
|
344
|
-
return
|
|
357
|
+
return HttpResponseRedirect(error_url)
|
|
345
358
|
|
|
346
359
|
message = success_message(result) if callable(success_message) else success_message
|
|
347
360
|
self.message_user(request, message, level=messages.SUCCESS)
|
|
348
|
-
return
|
|
361
|
+
return HttpResponseRedirect(success_url)
|
|
349
362
|
|
|
350
363
|
def _change_redirect(self, *, object_id, backend_alias):
|
|
351
364
|
return HttpResponseRedirect(self._change_url(object_id=object_id, backend_alias=backend_alias))
|
|
@@ -453,10 +466,10 @@ class ProcessStatusListFilter(admin.SimpleListFilter):
|
|
|
453
466
|
value = self.value()
|
|
454
467
|
if not value:
|
|
455
468
|
return queryset
|
|
456
|
-
cutoff =
|
|
469
|
+
cutoff = processes.process_cutoff_for_backend(
|
|
457
470
|
dashboard.resolve_backend_alias(request.GET.get("backend"))
|
|
458
471
|
)
|
|
459
|
-
return
|
|
472
|
+
return processes.filter_process_status(queryset, value, process_cutoff=cutoff)
|
|
460
473
|
|
|
461
474
|
|
|
462
475
|
@admin.register(Job)
|
|
@@ -625,92 +638,68 @@ class JobAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
625
638
|
return tuple(actions)
|
|
626
639
|
|
|
627
640
|
def handle_change_action(self, request, obj, action):
|
|
641
|
+
backend_alias = obj.backend_alias
|
|
642
|
+
object_url = self._change_url(object_id=obj.pk, backend_alias=backend_alias)
|
|
628
643
|
if action == "run_now":
|
|
629
|
-
|
|
630
|
-
_job, dispatch_outcome = dispatch_scheduled_job_now(
|
|
631
|
-
obj.pk, backend_alias=obj.backend_alias
|
|
632
|
-
)
|
|
633
|
-
except ADMIN_ACTION_ERRORS as exc:
|
|
634
|
-
self.message_user(request, f"Could not dispatch job now: {exc}", level=messages.ERROR)
|
|
635
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
636
|
-
|
|
637
|
-
message = "Dispatched scheduled job for immediate execution"
|
|
638
|
-
if dispatch_outcome is DispatchOutcome.BLOCKED:
|
|
639
|
-
message = "Dispatched scheduled job immediately and it is now blocked"
|
|
640
|
-
if dispatch_outcome is DispatchOutcome.DISCARDED:
|
|
641
|
-
message = "Dispatched scheduled job immediately and it was discarded"
|
|
642
|
-
self.message_user(request, message, level=messages.SUCCESS)
|
|
643
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
644
|
-
|
|
645
|
-
if action == "enqueue_copy_now":
|
|
646
|
-
try:
|
|
647
|
-
new_job = enqueue_job_again(obj.pk, backend_alias=obj.backend_alias, run_after=None)
|
|
648
|
-
except ADMIN_ACTION_ERRORS as exc:
|
|
649
|
-
self.message_user(request, f"Could not enqueue job: {exc}", level=messages.ERROR)
|
|
650
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
651
|
-
|
|
652
|
-
self.message_user(
|
|
644
|
+
return self._run_change_operation(
|
|
653
645
|
request,
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
),
|
|
659
|
-
|
|
646
|
+
operation=lambda: dispatch_scheduled_job_now(obj.pk, backend_alias=backend_alias),
|
|
647
|
+
success_message=lambda result: {
|
|
648
|
+
DispatchOutcome.BLOCKED: "Dispatched scheduled job immediately and it is now blocked",
|
|
649
|
+
DispatchOutcome.DISCARDED: "Dispatched scheduled job immediately and it was discarded",
|
|
650
|
+
}.get(result[1], "Dispatched scheduled job for immediate execution"),
|
|
651
|
+
error_message="Could not dispatch job now",
|
|
652
|
+
success_url=object_url,
|
|
653
|
+
error_url=object_url,
|
|
660
654
|
)
|
|
661
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
662
655
|
|
|
663
|
-
if action
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
669
|
-
|
|
670
|
-
self.message_user(
|
|
656
|
+
if action in {"enqueue", "enqueue_copy_now"}:
|
|
657
|
+
immediate = action == "enqueue_copy_now"
|
|
658
|
+
options = {"run_after": None} if immediate else {}
|
|
659
|
+
label = "Enqueued immediate copy" if immediate else "Enqueued job"
|
|
660
|
+
return self._run_change_operation(
|
|
671
661
|
request,
|
|
672
|
-
|
|
673
|
-
|
|
662
|
+
operation=lambda: enqueue_job_again(obj.pk, backend_alias=backend_alias, **options),
|
|
663
|
+
success_message=lambda new_job: format_html(
|
|
664
|
+
'{} <a href="{}">{}</a>.',
|
|
665
|
+
label,
|
|
674
666
|
self._change_url(object_id=new_job.pk, backend_alias=new_job.backend_alias),
|
|
675
667
|
new_job.pk,
|
|
676
668
|
),
|
|
677
|
-
|
|
669
|
+
error_message="Could not enqueue job",
|
|
670
|
+
success_url=object_url,
|
|
671
|
+
error_url=object_url,
|
|
678
672
|
)
|
|
679
|
-
return self._current_object_redirect(obj, backend_alias=obj.backend_alias)
|
|
680
673
|
|
|
681
674
|
if obj.status != "failed":
|
|
682
675
|
self.message_user(request, "This job is not failed", level=messages.ERROR)
|
|
683
|
-
return
|
|
676
|
+
return HttpResponseRedirect(object_url)
|
|
684
677
|
|
|
685
678
|
if action == "retry":
|
|
686
679
|
return self._run_change_operation(
|
|
687
680
|
request,
|
|
688
681
|
operation=lambda: retry_failed_job(
|
|
689
|
-
obj.failed_execution.job_id, backend_alias=
|
|
682
|
+
obj.failed_execution.job_id, backend_alias=backend_alias
|
|
690
683
|
),
|
|
691
684
|
success_message="Retried failed job",
|
|
692
685
|
error_message="Could not retry failed job",
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
),
|
|
696
|
-
error_redirect=lambda: self._current_object_redirect(obj, backend_alias=obj.backend_alias),
|
|
686
|
+
success_url=object_url,
|
|
687
|
+
error_url=object_url,
|
|
697
688
|
)
|
|
698
689
|
|
|
699
690
|
if action == "discard":
|
|
700
691
|
return self._run_change_operation(
|
|
701
692
|
request,
|
|
702
693
|
operation=lambda: discard_failed_job(
|
|
703
|
-
obj.failed_execution.job_id, backend_alias=
|
|
694
|
+
obj.failed_execution.job_id, backend_alias=backend_alias
|
|
704
695
|
),
|
|
705
696
|
success_message="Discarded failed job",
|
|
706
697
|
error_message="Could not discard failed job",
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
),
|
|
710
|
-
error_redirect=lambda: self._current_object_redirect(obj, backend_alias=obj.backend_alias),
|
|
698
|
+
success_url=self._changelist_url(backend_alias=backend_alias),
|
|
699
|
+
error_url=object_url,
|
|
711
700
|
)
|
|
712
701
|
|
|
713
|
-
return
|
|
702
|
+
return HttpResponseRedirect(object_url)
|
|
714
703
|
|
|
715
704
|
|
|
716
705
|
@admin.register(FailedExecution)
|
|
@@ -727,12 +716,20 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
727
716
|
readonly_fields = ("job", "exception_class", "message", "traceback", "retry_at", "created_at")
|
|
728
717
|
search_fields = ("job__id", "job__task_path", "message", "exception_class")
|
|
729
718
|
|
|
730
|
-
|
|
719
|
+
def has_retry_jobs_permission(self, request):
|
|
720
|
+
return has_action_permission(request.user, "retry")
|
|
721
|
+
|
|
722
|
+
def has_discard_jobs_permission(self, request):
|
|
723
|
+
return has_action_permission(request.user, "discard")
|
|
724
|
+
|
|
725
|
+
@admin.action(description="Retry selected failed jobs", permissions=["retry_jobs"])
|
|
731
726
|
def retry_jobs(self, request, queryset):
|
|
727
|
+
require_action_permission(request.user, "retry")
|
|
732
728
|
try:
|
|
729
|
+
job_ids = list(queryset.values_list("job_id", flat=True))
|
|
733
730
|
retried = retry_failed_jobs(
|
|
734
|
-
job_ids=
|
|
735
|
-
batch_size=
|
|
731
|
+
job_ids=job_ids,
|
|
732
|
+
batch_size=max(len(job_ids), 1),
|
|
736
733
|
backend_alias=self._backend_alias(request),
|
|
737
734
|
)
|
|
738
735
|
except ADMIN_ACTION_ERRORS as exc:
|
|
@@ -740,14 +737,16 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
740
737
|
return
|
|
741
738
|
self.message_user(request, f"Retried {retried} failed jobs", level=messages.SUCCESS)
|
|
742
739
|
|
|
743
|
-
@admin.action(description="Discard selected failed jobs")
|
|
740
|
+
@admin.action(description="Discard selected failed jobs", permissions=["discard_jobs"])
|
|
744
741
|
def discard_jobs(self, request, queryset):
|
|
742
|
+
require_action_permission(request.user, "discard")
|
|
745
743
|
try:
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
)
|
|
744
|
+
job_ids = list(queryset.values_list("job_id", flat=True))
|
|
745
|
+
discarded = discard_failed_jobs(
|
|
746
|
+
job_ids=job_ids,
|
|
747
|
+
batch_size=max(len(job_ids), 1),
|
|
748
|
+
backend_alias=self._backend_alias(request),
|
|
749
|
+
)
|
|
751
750
|
except ADMIN_ACTION_ERRORS as exc:
|
|
752
751
|
self.message_user(request, f"Could not discard failed jobs: {exc}", level=messages.ERROR)
|
|
753
752
|
return
|
|
@@ -783,10 +782,8 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
783
782
|
operation=lambda: retry_failed_job(job_id, backend_alias=backend_alias),
|
|
784
783
|
success_message="Retried failed job",
|
|
785
784
|
error_message="Could not retry failed job",
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
),
|
|
789
|
-
error_redirect=lambda: self._current_object_redirect(obj, backend_alias=backend_alias),
|
|
785
|
+
success_url=f"{reverse('admin:dj_queue_job_change', args=[job_id])}?{urlencode({'backend': backend_alias})}",
|
|
786
|
+
error_url=self._change_url(object_id=obj.pk, backend_alias=backend_alias),
|
|
790
787
|
)
|
|
791
788
|
|
|
792
789
|
if action == "discard":
|
|
@@ -795,10 +792,8 @@ class FailedExecutionAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
795
792
|
operation=lambda: discard_failed_job(obj.job_id, backend_alias=backend_alias),
|
|
796
793
|
success_message="Discarded failed job",
|
|
797
794
|
error_message="Could not discard failed job",
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
),
|
|
801
|
-
error_redirect=lambda: self._current_object_redirect(obj, backend_alias=backend_alias),
|
|
795
|
+
success_url=self._changelist_url(backend_alias=backend_alias),
|
|
796
|
+
error_url=self._change_url(object_id=obj.pk, backend_alias=backend_alias),
|
|
802
797
|
)
|
|
803
798
|
|
|
804
799
|
return self._current_object_redirect(obj, backend_alias=backend_alias)
|
|
@@ -832,8 +827,8 @@ class ProcessAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
832
827
|
|
|
833
828
|
def get_queryset(self, request):
|
|
834
829
|
queryset = super().get_queryset(request)
|
|
835
|
-
cutoff =
|
|
836
|
-
return queryset.annotate(live_rank=
|
|
830
|
+
cutoff = processes.process_cutoff_for_backend(self._backend_alias(request))
|
|
831
|
+
return queryset.annotate(live_rank=processes.process_live_rank_expression(cutoff))
|
|
837
832
|
|
|
838
833
|
@admin.display(description="status", ordering="live_rank")
|
|
839
834
|
def display_status(self, obj):
|
|
@@ -868,8 +863,14 @@ class RecurringTaskAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
868
863
|
)
|
|
869
864
|
search_fields = ("key", "task_path", "queue_name")
|
|
870
865
|
|
|
871
|
-
|
|
866
|
+
def has_unschedule_tasks_permission(self, request):
|
|
867
|
+
return has_action_permission(request.user, "unschedule")
|
|
868
|
+
|
|
869
|
+
@admin.action(
|
|
870
|
+
description="Unschedule selected recurring tasks", permissions=["unschedule_tasks"]
|
|
871
|
+
)
|
|
872
872
|
def unschedule_tasks(self, request, queryset):
|
|
873
|
+
require_action_permission(request.user, "unschedule")
|
|
873
874
|
backend_alias = self._backend_alias(request)
|
|
874
875
|
unscheduled = 0
|
|
875
876
|
static_failures = 0
|
|
@@ -944,21 +945,18 @@ class PauseAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
944
945
|
def handle_change_action(self, request, obj, action):
|
|
945
946
|
backend_alias = self._backend_alias(request)
|
|
946
947
|
if action == "resume":
|
|
947
|
-
|
|
948
|
-
QueueInfo(obj.queue_name, backend_alias=backend_alias).resume()
|
|
949
|
-
except ADMIN_ACTION_ERRORS as exc:
|
|
950
|
-
self.message_user(request, f"Could not resume queue: {exc}", level=messages.ERROR)
|
|
951
|
-
return self._current_object_redirect(obj, backend_alias=backend_alias)
|
|
952
|
-
self.message_user(
|
|
948
|
+
return self._run_change_operation(
|
|
953
949
|
request,
|
|
954
|
-
|
|
950
|
+
operation=lambda: QueueInfo(obj.queue_name, backend_alias=backend_alias).resume(),
|
|
951
|
+
success_message=lambda _result: format_html(
|
|
955
952
|
'Resumed queue <a href="{}">{}</a>',
|
|
956
953
|
f"{reverse('admin:dj_queue_dashboard_queue', args=[obj.queue_name])}?{urlencode({'backend': backend_alias})}",
|
|
957
954
|
obj.queue_name,
|
|
958
955
|
),
|
|
959
|
-
|
|
956
|
+
error_message="Could not resume queue",
|
|
957
|
+
success_url=self._changelist_url(backend_alias=backend_alias),
|
|
958
|
+
error_url=self._change_url(object_id=obj.pk, backend_alias=backend_alias),
|
|
960
959
|
)
|
|
961
|
-
return HttpResponseRedirect(self._changelist_url(backend_alias=backend_alias))
|
|
962
960
|
|
|
963
961
|
return self._current_object_redirect(obj, backend_alias=backend_alias)
|
|
964
962
|
|
|
@@ -989,7 +987,7 @@ class SemaphoreAdmin(HiddenSidebarAdminMixin, admin.ModelAdmin):
|
|
|
989
987
|
queryset = super().get_queryset(request)
|
|
990
988
|
alias = self._backend_database_alias(request)
|
|
991
989
|
return queryset.annotate(
|
|
992
|
-
blocked_waiter_count=
|
|
990
|
+
blocked_waiter_count=controls.semaphore_blocked_waiter_count_expression(alias)
|
|
993
991
|
)
|
|
994
992
|
|
|
995
993
|
@admin.display(description="active")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.core.exceptions import PermissionDenied
|
|
2
|
+
|
|
3
|
+
ACTION_PERMISSIONS = {
|
|
4
|
+
"enqueue": "dj_queue.add_job",
|
|
5
|
+
"enqueue_copy_now": "dj_queue.add_job",
|
|
6
|
+
"run_now": "dj_queue.change_job",
|
|
7
|
+
"retry": "dj_queue.change_job",
|
|
8
|
+
"discard": "dj_queue.delete_job",
|
|
9
|
+
"clear": "dj_queue.delete_job",
|
|
10
|
+
"pause": "dj_queue.add_pause",
|
|
11
|
+
"resume": "dj_queue.delete_pause",
|
|
12
|
+
"unschedule": "dj_queue.delete_recurringtask",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_action_permission(user, action):
|
|
17
|
+
permission = ACTION_PERMISSIONS.get(action)
|
|
18
|
+
return bool(permission and user.is_active and user.is_staff and user.has_perm(permission))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def require_action_permission(user, action):
|
|
22
|
+
if not has_action_permission(user, action):
|
|
23
|
+
raise PermissionDenied
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def permitted_actions(user, actions):
|
|
27
|
+
return tuple(action for action in actions if has_action_permission(user, action["name"]))
|
|
@@ -5,8 +5,8 @@ from typing import Any, Literal, Self, TypeVar
|
|
|
5
5
|
from django.db import transaction
|
|
6
6
|
from django.tasks import Task
|
|
7
7
|
|
|
8
|
-
from dj_queue import observability
|
|
9
8
|
from dj_queue.operations.claiming import ClaimedJob, claim_ready_jobs
|
|
9
|
+
from dj_queue.operations.execution import execute_claimed_job
|
|
10
10
|
from dj_queue.operations.jobs import (
|
|
11
11
|
discard_blocked_jobs,
|
|
12
12
|
discard_failed_job,
|
|
@@ -14,13 +14,13 @@ from dj_queue.operations.jobs import (
|
|
|
14
14
|
discard_ready_jobs,
|
|
15
15
|
discard_ready_jobs_for_queue,
|
|
16
16
|
discard_scheduled_jobs,
|
|
17
|
-
execute_claimed_job,
|
|
18
17
|
retry_failed_job,
|
|
19
18
|
retry_failed_jobs,
|
|
20
19
|
schedule_failed_job_retry,
|
|
21
20
|
)
|
|
22
21
|
from dj_queue.operations.queues import pause_queue, resume_queue
|
|
23
22
|
from dj_queue.operations.recurring import schedule_recurring_task, unschedule_recurring_task
|
|
23
|
+
from dj_queue.reads import queues
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
26
|
"ClaimedJob",
|
|
@@ -79,7 +79,7 @@ class QueueInfo:
|
|
|
79
79
|
def size(self) -> int:
|
|
80
80
|
if self._snapshot is not None:
|
|
81
81
|
return self._snapshot["ready_count"]
|
|
82
|
-
return
|
|
82
|
+
return queues.queue_ready_count(
|
|
83
83
|
backend_alias=self.backend_alias,
|
|
84
84
|
queue_name=self.queue_name,
|
|
85
85
|
)
|
|
@@ -92,14 +92,14 @@ class QueueInfo:
|
|
|
92
92
|
return 0.0
|
|
93
93
|
return latency
|
|
94
94
|
|
|
95
|
-
paused =
|
|
95
|
+
paused = queues.queue_is_paused(
|
|
96
96
|
backend_alias=self.backend_alias,
|
|
97
97
|
queue_name=self.queue_name,
|
|
98
98
|
)
|
|
99
99
|
if paused:
|
|
100
100
|
return None
|
|
101
101
|
|
|
102
|
-
latency =
|
|
102
|
+
latency = queues.queue_latency_seconds(
|
|
103
103
|
backend_alias=self.backend_alias,
|
|
104
104
|
queue_name=self.queue_name,
|
|
105
105
|
paused=False,
|
|
@@ -110,7 +110,7 @@ class QueueInfo:
|
|
|
110
110
|
def paused(self) -> bool:
|
|
111
111
|
if self._snapshot is not None:
|
|
112
112
|
return self._snapshot["paused"]
|
|
113
|
-
return
|
|
113
|
+
return queues.queue_is_paused(
|
|
114
114
|
backend_alias=self.backend_alias,
|
|
115
115
|
queue_name=self.queue_name,
|
|
116
116
|
)
|
|
@@ -138,7 +138,7 @@ class QueueInfo:
|
|
|
138
138
|
|
|
139
139
|
@classmethod
|
|
140
140
|
def all(cls, *, backend_alias: str = "default") -> list[Self]:
|
|
141
|
-
queue_rows =
|
|
141
|
+
queue_rows = queues.queue_rows_for_backend(backend_alias=backend_alias)
|
|
142
142
|
return [cls(row["name"], backend_alias=backend_alias, snapshot=row) for row in queue_rows]
|
|
143
143
|
|
|
144
144
|
|
|
@@ -6,8 +6,8 @@ from django.tasks.exceptions import TaskResultDoesNotExist
|
|
|
6
6
|
from dj_queue.config import load_backend_config
|
|
7
7
|
from dj_queue.db import get_database_alias
|
|
8
8
|
from dj_queue.models import Job
|
|
9
|
-
from dj_queue.operations.
|
|
10
|
-
|
|
9
|
+
from dj_queue.operations.dispatch import DispatchOutcome
|
|
10
|
+
from dj_queue.operations.enqueue import (
|
|
11
11
|
enqueue_job_with_dispatch,
|
|
12
12
|
enqueue_jobs_bulk,
|
|
13
13
|
validate_priority,
|