dj-queue 0.10.3__tar.gz → 0.10.5__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.3 → dj_queue-0.10.5}/PKG-INFO +6 -7
- {dj_queue-0.10.3 → dj_queue-0.10.5}/README.md +5 -6
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/admin.py +13 -2
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/api.py +14 -20
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/backend.py +13 -2
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/config.py +60 -22
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/contrib/asgi.py +5 -3
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/contrib/gunicorn.py +29 -9
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/contrib/prometheus.py +6 -3
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/metrics.py +10 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/observability.py +64 -34
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/_helpers.py +17 -6
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/concurrency.py +111 -26
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/jobs.py +232 -48
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/recurring.py +82 -32
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/base.py +30 -4
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/pool.py +18 -1
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/supervisor.py +35 -14
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/worker.py +44 -5
- {dj_queue-0.10.3 → dj_queue-0.10.5}/pyproject.toml +1 -1
- {dj_queue-0.10.3 → dj_queue-0.10.5}/LICENSE +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/dashboard.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/db.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/log.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/cleanup.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/errors.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/notify.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/dj_queue/views.py +0 -0
- {dj_queue-0.10.3 → dj_queue-0.10.5}/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.5
|
|
4
4
|
Summary: Database-backed task queue backend for Django’s Tasks framework.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -61,6 +61,9 @@ It has a narrow, explicit shape:
|
|
|
61
61
|
For detailed comparisons with Celery, RQ, Procrastinate, and other alternatives,
|
|
62
62
|
see [COMPARISONS.md](docs/COMPARISONS.md).
|
|
63
63
|
|
|
64
|
+
For benchmarks on enqueue, promotion, recurring, claiming, worker-drain, and concurrency-contention scenarios,
|
|
65
|
+
see [docs/benchmarks/](docs/benchmarks/) for the latest published reports.
|
|
66
|
+
|
|
64
67
|
## Installation
|
|
65
68
|
|
|
66
69
|
`dj_queue` requires Python 3.12+ and Django 6.0+.
|
|
@@ -640,8 +643,9 @@ Run your normal application migrations on `default`, then migrate `dj_queue`
|
|
|
640
643
|
onto the queue database:
|
|
641
644
|
|
|
642
645
|
```bash
|
|
643
|
-
|
|
646
|
+
# migrate dj_queue on its queue alias first so django doesn't mark it applied on default
|
|
644
647
|
python manage.py migrate dj_queue --database queue
|
|
648
|
+
python manage.py migrate
|
|
645
649
|
```
|
|
646
650
|
|
|
647
651
|
With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
|
|
@@ -1012,11 +1016,6 @@ Both endpoints support bearer token authentication. Set
|
|
|
1012
1016
|
`Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
|
|
1013
1017
|
the network or proxy layer.
|
|
1014
1018
|
|
|
1015
|
-
## Benchmarks
|
|
1016
|
-
|
|
1017
|
-
The repository includes a standalone benchmark harness for enqueue, promotion, recurring, worker-drain, and concurrency-contention scenarios.
|
|
1018
|
-
See [`docs/benchmarks/`](docs/benchmarks/) for the latest published reports.
|
|
1019
|
-
|
|
1020
1019
|
## License
|
|
1021
1020
|
|
|
1022
1021
|
MIT
|
|
@@ -35,6 +35,9 @@ It has a narrow, explicit shape:
|
|
|
35
35
|
For detailed comparisons with Celery, RQ, Procrastinate, and other alternatives,
|
|
36
36
|
see [COMPARISONS.md](docs/COMPARISONS.md).
|
|
37
37
|
|
|
38
|
+
For benchmarks on enqueue, promotion, recurring, claiming, worker-drain, and concurrency-contention scenarios,
|
|
39
|
+
see [docs/benchmarks/](docs/benchmarks/) for the latest published reports.
|
|
40
|
+
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
40
43
|
`dj_queue` requires Python 3.12+ and Django 6.0+.
|
|
@@ -614,8 +617,9 @@ Run your normal application migrations on `default`, then migrate `dj_queue`
|
|
|
614
617
|
onto the queue database:
|
|
615
618
|
|
|
616
619
|
```bash
|
|
617
|
-
|
|
620
|
+
# migrate dj_queue on its queue alias first so django doesn't mark it applied on default
|
|
618
621
|
python manage.py migrate dj_queue --database queue
|
|
622
|
+
python manage.py migrate
|
|
619
623
|
```
|
|
620
624
|
|
|
621
625
|
With this setup, `dj_queue`'s ORM queries and raw SQL helpers stay on the queue
|
|
@@ -986,11 +990,6 @@ Both endpoints support bearer token authentication. Set
|
|
|
986
990
|
`Authorization: Bearer <token>`. Leave it unset if you protect these URLs at
|
|
987
991
|
the network or proxy layer.
|
|
988
992
|
|
|
989
|
-
## Benchmarks
|
|
990
|
-
|
|
991
|
-
The repository includes a standalone benchmark harness for enqueue, promotion, recurring, worker-drain, and concurrency-contention scenarios.
|
|
992
|
-
See [`docs/benchmarks/`](docs/benchmarks/) for the latest published reports.
|
|
993
|
-
|
|
994
993
|
## License
|
|
995
994
|
|
|
996
995
|
MIT
|
|
@@ -44,7 +44,7 @@ from dj_queue.queue_state import (
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class
|
|
47
|
+
class DjQueueAdminSiteMixin:
|
|
48
48
|
def _dashboard_app_url(self):
|
|
49
49
|
return reverse("admin:dj_queue_dashboard_changelist", current_app=self.name)
|
|
50
50
|
|
|
@@ -65,7 +65,18 @@ class DjQueueFirstAdminSite(admin.AdminSite):
|
|
|
65
65
|
return super().app_index(request, app_label, extra_context=extra_context)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
def _install_dj_queue_admin_site(site):
|
|
69
|
+
if isinstance(site, DjQueueAdminSiteMixin):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
site.__class__ = type(
|
|
73
|
+
f"DjQueue{site.__class__.__name__}",
|
|
74
|
+
(DjQueueAdminSiteMixin, site.__class__),
|
|
75
|
+
{"__module__": __name__},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_install_dj_queue_admin_site(admin.site)
|
|
69
80
|
|
|
70
81
|
|
|
71
82
|
def _format_admin_datetime(value):
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from datetime import timedelta
|
|
2
2
|
from functools import partial
|
|
3
3
|
|
|
4
|
-
from django.db.models.functions import Coalesce
|
|
5
4
|
from django.db import transaction
|
|
6
5
|
from django.utils import timezone
|
|
7
6
|
|
|
8
7
|
from dj_queue import observability
|
|
9
8
|
from dj_queue.config import load_backend_config
|
|
10
9
|
from dj_queue.db import get_database_alias
|
|
11
|
-
from dj_queue.models import
|
|
10
|
+
from dj_queue.models import ReadyExecution
|
|
12
11
|
from dj_queue.operations.jobs import (
|
|
13
12
|
ClaimedJob,
|
|
14
13
|
claim_ready_jobs,
|
|
@@ -53,30 +52,25 @@ class QueueInfo:
|
|
|
53
52
|
|
|
54
53
|
@property
|
|
55
54
|
def latency(self):
|
|
56
|
-
|
|
55
|
+
paused = observability.queue_is_paused(
|
|
56
|
+
backend_alias=self.backend_alias,
|
|
57
|
+
queue_name=self.queue_name,
|
|
58
|
+
)
|
|
59
|
+
if paused:
|
|
57
60
|
return None
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
self.
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
.values_list("latency_at", flat=True)
|
|
64
|
-
.first()
|
|
62
|
+
latency = observability.queue_latency_seconds(
|
|
63
|
+
backend_alias=self.backend_alias,
|
|
64
|
+
queue_name=self.queue_name,
|
|
65
|
+
paused=False,
|
|
65
66
|
)
|
|
66
|
-
if
|
|
67
|
-
return 0.0
|
|
68
|
-
return max((timezone.now() - oldest).total_seconds(), 0.0)
|
|
67
|
+
return 0.0 if latency is None else latency
|
|
69
68
|
|
|
70
69
|
@property
|
|
71
70
|
def paused(self):
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.filter(
|
|
76
|
-
backend_alias=self.backend_alias,
|
|
77
|
-
queue_name=self.queue_name,
|
|
78
|
-
)
|
|
79
|
-
.exists()
|
|
71
|
+
return observability.queue_is_paused(
|
|
72
|
+
backend_alias=self.backend_alias,
|
|
73
|
+
queue_name=self.queue_name,
|
|
80
74
|
)
|
|
81
75
|
|
|
82
76
|
def pause(self):
|
|
@@ -21,6 +21,11 @@ class DjQueueBackend(BaseTaskBackend):
|
|
|
21
21
|
supports_get_result = True
|
|
22
22
|
supports_priority = True
|
|
23
23
|
|
|
24
|
+
def __init__(self, alias, params):
|
|
25
|
+
if not params.get("QUEUES"):
|
|
26
|
+
params = {**params, "QUEUES": []}
|
|
27
|
+
super().__init__(alias, params)
|
|
28
|
+
|
|
24
29
|
def validate_task(self, task):
|
|
25
30
|
validate_queue_allowed(task.queue_name, backend_alias=self.alias)
|
|
26
31
|
validate_priority(task.priority)
|
|
@@ -28,7 +33,13 @@ class DjQueueBackend(BaseTaskBackend):
|
|
|
28
33
|
|
|
29
34
|
def enqueue(self, task, args, kwargs):
|
|
30
35
|
self.validate_task(task)
|
|
31
|
-
job, dispatch_outcome = enqueue_job_with_dispatch(
|
|
36
|
+
job, dispatch_outcome = enqueue_job_with_dispatch(
|
|
37
|
+
task,
|
|
38
|
+
args,
|
|
39
|
+
kwargs,
|
|
40
|
+
backend_alias=self.alias,
|
|
41
|
+
validate=False,
|
|
42
|
+
)
|
|
32
43
|
return task_result_from_enqueued_job(
|
|
33
44
|
job,
|
|
34
45
|
task,
|
|
@@ -49,7 +60,7 @@ class DjQueueBackend(BaseTaskBackend):
|
|
|
49
60
|
self.validate_task(task)
|
|
50
61
|
jobs.append((task, args, kwargs))
|
|
51
62
|
|
|
52
|
-
created_jobs = enqueue_jobs_bulk(jobs, backend_alias=self.alias)
|
|
63
|
+
created_jobs = enqueue_jobs_bulk(jobs, backend_alias=self.alias, validate=False)
|
|
53
64
|
return [
|
|
54
65
|
task_result_from_enqueued_job(
|
|
55
66
|
job,
|
|
@@ -82,7 +82,7 @@ class DispatcherConfig(ConfigValue):
|
|
|
82
82
|
batch_size: int = 500
|
|
83
83
|
polling_interval: float = 1
|
|
84
84
|
concurrency_maintenance: bool = True
|
|
85
|
-
concurrency_maintenance_interval:
|
|
85
|
+
concurrency_maintenance_interval: float = 600
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
@dataclass(frozen=True, slots=True)
|
|
@@ -112,9 +112,9 @@ class BackendConfig(ConfigValue):
|
|
|
112
112
|
dispatchers: tuple[DispatcherConfig, ...] = (DispatcherConfig(),)
|
|
113
113
|
scheduler: SchedulerConfig | None = field(default_factory=SchedulerConfig)
|
|
114
114
|
recurring: dict[str, RecurringTaskConfig] = field(default_factory=dict)
|
|
115
|
-
process_heartbeat_interval:
|
|
116
|
-
process_alive_threshold:
|
|
117
|
-
shutdown_timeout:
|
|
115
|
+
process_heartbeat_interval: float = 60
|
|
116
|
+
process_alive_threshold: float = 300
|
|
117
|
+
shutdown_timeout: float = 5
|
|
118
118
|
supervisor_pidfile: str | None = None
|
|
119
119
|
preserve_finished_jobs: bool = True
|
|
120
120
|
clear_finished_jobs_after: int | None = 86400
|
|
@@ -248,7 +248,9 @@ def _load_backend_config_uncached(
|
|
|
248
248
|
resolved_options["process_alive_threshold"], "process_alive_threshold"
|
|
249
249
|
),
|
|
250
250
|
shutdown_timeout=_nonnegative_float(resolved_options["shutdown_timeout"], "shutdown_timeout"),
|
|
251
|
-
supervisor_pidfile=
|
|
251
|
+
supervisor_pidfile=_optional_string_option(
|
|
252
|
+
resolved_options["supervisor_pidfile"], "supervisor_pidfile"
|
|
253
|
+
),
|
|
252
254
|
preserve_finished_jobs=preserve_finished_jobs,
|
|
253
255
|
clear_finished_jobs_after=_optional_nonnegative_int(
|
|
254
256
|
resolved_options["clear_finished_jobs_after"], "clear_finished_jobs_after"
|
|
@@ -264,7 +266,7 @@ def _load_backend_config_uncached(
|
|
|
264
266
|
resolved_options["default_concurrency_duration"],
|
|
265
267
|
"default_concurrency_duration",
|
|
266
268
|
),
|
|
267
|
-
database_alias=
|
|
269
|
+
database_alias=_string_option(resolved_options["database_alias"], "database_alias"),
|
|
268
270
|
use_skip_locked=_bool_option(resolved_options["use_skip_locked"], "use_skip_locked"),
|
|
269
271
|
listen_notify=_bool_option(resolved_options["listen_notify"], "listen_notify"),
|
|
270
272
|
silence_polling=_bool_option(resolved_options["silence_polling"], "silence_polling"),
|
|
@@ -428,7 +430,7 @@ def _validated_callback_path(callback_path: Any) -> str | None:
|
|
|
428
430
|
if callback_path in (None, ""):
|
|
429
431
|
return None
|
|
430
432
|
|
|
431
|
-
callback_path =
|
|
433
|
+
callback_path = _string_option(callback_path, "on_thread_error")
|
|
432
434
|
try:
|
|
433
435
|
import_string(callback_path)
|
|
434
436
|
except ImportError as exc:
|
|
@@ -546,24 +548,31 @@ def _build_recurring_config(
|
|
|
546
548
|
|
|
547
549
|
recurring: dict[str, RecurringTaskConfig] = {}
|
|
548
550
|
for key, raw_entry in raw_recurring.items():
|
|
551
|
+
key = _string_option(key, "recurring task key")
|
|
549
552
|
if not isinstance(raw_entry, Mapping):
|
|
550
553
|
raise ImproperlyConfigured("recurring entries must be mappings")
|
|
551
554
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if
|
|
555
|
+
raw_task_path = raw_entry.get("task_path")
|
|
556
|
+
raw_schedule = raw_entry.get("schedule")
|
|
557
|
+
if raw_task_path in (None, "") or raw_schedule in (None, ""):
|
|
555
558
|
raise ImproperlyConfigured(f"recurring task {key!r} requires task_path and schedule")
|
|
556
|
-
|
|
559
|
+
task_path = _string_option(raw_task_path, f"recurring task {key!r} task_path")
|
|
560
|
+
schedule = _string_option(raw_schedule, f"recurring task {key!r} schedule")
|
|
561
|
+
if not is_valid_cron(schedule):
|
|
557
562
|
raise ImproperlyConfigured(f"recurring task {key!r} has an invalid cron schedule")
|
|
558
563
|
|
|
559
|
-
queue_name =
|
|
564
|
+
queue_name = _string_option(
|
|
565
|
+
raw_entry.get("queue_name", "default"), f"recurring task {key!r} queue_name"
|
|
566
|
+
)
|
|
560
567
|
priority = _priority_int(raw_entry.get("priority", 0), f"recurring task {key!r} priority")
|
|
561
568
|
if allowed_queues and queue_name not in allowed_queues:
|
|
562
569
|
raise ImproperlyConfigured(
|
|
563
570
|
f"recurring task {key!r} is invalid: queue {queue_name!r} is not allowed for backend {backend_alias!r}"
|
|
564
571
|
)
|
|
572
|
+
args = _tuple_option(raw_entry.get("args", []), f"recurring task {key!r} args")
|
|
573
|
+
kwargs = _dict_option(raw_entry.get("kwargs", {}), f"recurring task {key!r} kwargs")
|
|
565
574
|
try:
|
|
566
|
-
task = import_string(
|
|
575
|
+
task = import_string(task_path)
|
|
567
576
|
except ImportError as exc:
|
|
568
577
|
raise ImproperlyConfigured(f"recurring task {key!r} is invalid: {exc}") from exc
|
|
569
578
|
if not hasattr(task, "using"):
|
|
@@ -571,15 +580,17 @@ def _build_recurring_config(
|
|
|
571
580
|
f"recurring task {key!r} is invalid: task_path must reference a Django task"
|
|
572
581
|
)
|
|
573
582
|
|
|
574
|
-
recurring[
|
|
575
|
-
key=
|
|
576
|
-
task_path=
|
|
577
|
-
schedule=
|
|
578
|
-
args=
|
|
579
|
-
kwargs=
|
|
583
|
+
recurring[key] = RecurringTaskConfig(
|
|
584
|
+
key=key,
|
|
585
|
+
task_path=task_path,
|
|
586
|
+
schedule=schedule,
|
|
587
|
+
args=args,
|
|
588
|
+
kwargs=kwargs,
|
|
580
589
|
queue_name=queue_name,
|
|
581
590
|
priority=priority,
|
|
582
|
-
description=
|
|
591
|
+
description=_string_option(
|
|
592
|
+
raw_entry.get("description", ""), f"recurring task {key!r} description"
|
|
593
|
+
),
|
|
583
594
|
)
|
|
584
595
|
return recurring
|
|
585
596
|
|
|
@@ -612,9 +623,36 @@ def _as_string_tuple(value: Any) -> tuple[str, ...]:
|
|
|
612
623
|
return ()
|
|
613
624
|
if isinstance(value, str):
|
|
614
625
|
return (value,)
|
|
615
|
-
if not isinstance(value, Sequence):
|
|
626
|
+
if not isinstance(value, Sequence) or isinstance(value, (bytes, bytearray)):
|
|
627
|
+
raise ImproperlyConfigured("expected a string or a sequence of strings")
|
|
628
|
+
values = tuple(value)
|
|
629
|
+
if not all(isinstance(item, str) for item in values):
|
|
616
630
|
raise ImproperlyConfigured("expected a string or a sequence of strings")
|
|
617
|
-
return
|
|
631
|
+
return values
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _string_option(value: Any, setting_name: str) -> str:
|
|
635
|
+
if not isinstance(value, str):
|
|
636
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a string")
|
|
637
|
+
return value
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _optional_string_option(value: Any, setting_name: str) -> str | None:
|
|
641
|
+
if value is None:
|
|
642
|
+
return None
|
|
643
|
+
return _string_option(value, setting_name)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _tuple_option(value: Any, setting_name: str) -> tuple[Any, ...]:
|
|
647
|
+
if isinstance(value, str) or not isinstance(value, Sequence):
|
|
648
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a sequence")
|
|
649
|
+
return tuple(value)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _dict_option(value: Any, setting_name: str) -> dict[str, Any]:
|
|
653
|
+
if not isinstance(value, Mapping):
|
|
654
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a mapping")
|
|
655
|
+
return dict(value)
|
|
618
656
|
|
|
619
657
|
|
|
620
658
|
def _optional_nonnegative_int(value: Any, setting_name: str) -> int | None:
|
|
@@ -104,8 +104,10 @@ class DjQueueLifespan:
|
|
|
104
104
|
|
|
105
105
|
receive_queue = asyncio.Queue()
|
|
106
106
|
send_queue = asyncio.Queue()
|
|
107
|
-
app_task = await self._start_wrapped_lifespan_app(scope, receive_queue.get, send_queue.put)
|
|
108
107
|
wrapped_app_supports_lifespan = self.forward_wrapped_lifespan
|
|
108
|
+
app_task = None
|
|
109
|
+
if wrapped_app_supports_lifespan:
|
|
110
|
+
app_task = await self._start_wrapped_lifespan_app(scope, receive_queue.get, send_queue.put)
|
|
109
111
|
|
|
110
112
|
try:
|
|
111
113
|
while True:
|
|
@@ -137,12 +139,12 @@ class DjQueueLifespan:
|
|
|
137
139
|
if response is None:
|
|
138
140
|
response = {"type": "lifespan.shutdown.complete"}
|
|
139
141
|
await send(response)
|
|
140
|
-
if wrapped_app_supports_lifespan:
|
|
142
|
+
if wrapped_app_supports_lifespan and app_task is not None:
|
|
141
143
|
await app_task
|
|
142
144
|
return
|
|
143
145
|
finally:
|
|
144
146
|
await self._stop_supervisor()
|
|
145
|
-
if not app_task.done():
|
|
147
|
+
if app_task is not None and not app_task.done():
|
|
146
148
|
app_task.cancel()
|
|
147
149
|
with suppress(asyncio.CancelledError):
|
|
148
150
|
await app_task
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fcntl
|
|
2
|
+
import hashlib
|
|
2
3
|
import tempfile
|
|
3
4
|
import threading
|
|
4
5
|
from pathlib import Path
|
|
@@ -6,7 +7,7 @@ from pathlib import Path
|
|
|
6
7
|
from dj_queue.runtime.errors import handle_thread_error
|
|
7
8
|
from dj_queue.runtime.supervisor import AsyncSupervisor
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
LOCK_PATH_PREFIX = "dj_queue_gunicorn_supervisor"
|
|
10
11
|
LOCK_RETRY_INTERVAL = 1.0
|
|
11
12
|
|
|
12
13
|
|
|
@@ -20,7 +21,7 @@ def _set_supervisor_state(worker, **state):
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _start_embedded_supervisor(worker, *, backend_alias="default"):
|
|
23
|
-
lock_file = _try_acquire_supervisor_lock()
|
|
24
|
+
lock_file = _try_acquire_supervisor_lock(backend_alias=backend_alias)
|
|
24
25
|
if lock_file is None:
|
|
25
26
|
return None
|
|
26
27
|
|
|
@@ -70,7 +71,12 @@ def _start_lock_retry_loop(worker, *, backend_alias="default"):
|
|
|
70
71
|
while retry_stop.wait(LOCK_RETRY_INTERVAL) is False:
|
|
71
72
|
if getattr(worker, "_dj_queue_supervisor", None) is not None:
|
|
72
73
|
return
|
|
73
|
-
|
|
74
|
+
try:
|
|
75
|
+
supervisor = _start_embedded_supervisor(worker, backend_alias=backend_alias)
|
|
76
|
+
except Exception as error:
|
|
77
|
+
handle_thread_error(error, context="gunicorn.supervisor", backend_alias=backend_alias)
|
|
78
|
+
continue
|
|
79
|
+
if supervisor is not None:
|
|
74
80
|
retry_stop.set()
|
|
75
81
|
return
|
|
76
82
|
|
|
@@ -81,7 +87,8 @@ def _start_lock_retry_loop(worker, *, backend_alias="default"):
|
|
|
81
87
|
retry_thread.start()
|
|
82
88
|
|
|
83
89
|
|
|
84
|
-
def post_fork(
|
|
90
|
+
def post_fork(server, worker):
|
|
91
|
+
backend_alias = _backend_alias(server, worker)
|
|
85
92
|
_set_supervisor_state(
|
|
86
93
|
worker,
|
|
87
94
|
supervisor=None,
|
|
@@ -91,11 +98,11 @@ def post_fork(_server, worker):
|
|
|
91
98
|
supervisor_retry_stop=None,
|
|
92
99
|
supervisor_retry_thread=None,
|
|
93
100
|
)
|
|
94
|
-
supervisor = _start_embedded_supervisor(worker)
|
|
101
|
+
supervisor = _start_embedded_supervisor(worker, backend_alias=backend_alias)
|
|
95
102
|
if supervisor is not None:
|
|
96
103
|
return supervisor
|
|
97
104
|
|
|
98
|
-
_start_lock_retry_loop(worker)
|
|
105
|
+
_start_lock_retry_loop(worker, backend_alias=backend_alias)
|
|
99
106
|
return None
|
|
100
107
|
|
|
101
108
|
|
|
@@ -133,9 +140,22 @@ def worker_exit(_server, worker):
|
|
|
133
140
|
return None
|
|
134
141
|
|
|
135
142
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
def _backend_alias(server, worker):
|
|
144
|
+
return getattr(worker, "dj_queue_backend_alias", None) or getattr(
|
|
145
|
+
server, "dj_queue_backend_alias", "default"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _supervisor_lock_path(*, backend_alias):
|
|
150
|
+
lock_scope = f"{Path.cwd()}:{backend_alias}"
|
|
151
|
+
digest = hashlib.sha256(lock_scope.encode()).hexdigest()[:12]
|
|
152
|
+
return Path(tempfile.gettempdir()) / f"{LOCK_PATH_PREFIX}_{backend_alias}_{digest}.lock"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _try_acquire_supervisor_lock(*, backend_alias="default"):
|
|
156
|
+
lock_path = _supervisor_lock_path(backend_alias=backend_alias)
|
|
157
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
lock_file = lock_path.open("a+")
|
|
139
159
|
try:
|
|
140
160
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
141
161
|
except BlockingIOError:
|
|
@@ -8,19 +8,22 @@ except ImportError:
|
|
|
8
8
|
else:
|
|
9
9
|
from dj_queue.metrics import metric_families
|
|
10
10
|
|
|
11
|
+
METRIC_TYPES = {"gauge": GaugeMetricFamily}
|
|
12
|
+
|
|
11
13
|
class DjQueueCollector:
|
|
12
14
|
"""Prometheus collector that exposes dj_queue metrics from the shared observability snapshot."""
|
|
13
15
|
|
|
14
16
|
def collect(self):
|
|
15
17
|
for family in metric_families():
|
|
16
|
-
|
|
18
|
+
metric_class = METRIC_TYPES[family.metric_type]
|
|
19
|
+
metric = metric_class(
|
|
17
20
|
family.name,
|
|
18
21
|
family.help_text,
|
|
19
22
|
labels=list(family.labels),
|
|
20
23
|
)
|
|
21
24
|
for sample in family.samples:
|
|
22
|
-
|
|
23
|
-
yield
|
|
25
|
+
metric.add_metric(list(sample.labels), sample.value)
|
|
26
|
+
yield metric
|
|
24
27
|
|
|
25
28
|
registry = CollectorRegistry(auto_describe=False)
|
|
26
29
|
registry.register(DjQueueCollector())
|
|
@@ -14,6 +14,7 @@ class MetricSample:
|
|
|
14
14
|
class MetricFamily:
|
|
15
15
|
name: str
|
|
16
16
|
help_text: str
|
|
17
|
+
metric_type: str
|
|
17
18
|
labels: tuple[str, ...]
|
|
18
19
|
samples: tuple[MetricSample, ...]
|
|
19
20
|
|
|
@@ -109,54 +110,63 @@ def metric_families(*, snapshots=None):
|
|
|
109
110
|
MetricFamily(
|
|
110
111
|
name="dj_queue_queue_jobs",
|
|
111
112
|
help_text="Current job count by backend, queue, and state",
|
|
113
|
+
metric_type="gauge",
|
|
112
114
|
labels=("backend", "queue", "state"),
|
|
113
115
|
samples=tuple(queue_jobs),
|
|
114
116
|
),
|
|
115
117
|
MetricFamily(
|
|
116
118
|
name="dj_queue_queue_paused",
|
|
117
119
|
help_text="Whether a queue is paused for a backend",
|
|
120
|
+
metric_type="gauge",
|
|
118
121
|
labels=("backend", "queue"),
|
|
119
122
|
samples=tuple(queue_paused),
|
|
120
123
|
),
|
|
121
124
|
MetricFamily(
|
|
122
125
|
name="dj_queue_queue_latency_seconds",
|
|
123
126
|
help_text="Latency of the oldest ready job in a backend queue",
|
|
127
|
+
metric_type="gauge",
|
|
124
128
|
labels=("backend", "queue"),
|
|
125
129
|
samples=tuple(queue_latency),
|
|
126
130
|
),
|
|
127
131
|
MetricFamily(
|
|
128
132
|
name="dj_queue_queue_live_workers",
|
|
129
133
|
help_text="Live workers that can service a backend queue",
|
|
134
|
+
metric_type="gauge",
|
|
130
135
|
labels=("backend", "queue"),
|
|
131
136
|
samples=tuple(queue_workers),
|
|
132
137
|
),
|
|
133
138
|
MetricFamily(
|
|
134
139
|
name="dj_queue_runner_processes",
|
|
135
140
|
help_text="Current runner process count by backend and liveness",
|
|
141
|
+
metric_type="gauge",
|
|
136
142
|
labels=("backend", "status"),
|
|
137
143
|
samples=tuple(runner_processes),
|
|
138
144
|
),
|
|
139
145
|
MetricFamily(
|
|
140
146
|
name="dj_queue_runner_processes_by_kind",
|
|
141
147
|
help_text="Current runner process count by backend, kind, and liveness",
|
|
148
|
+
metric_type="gauge",
|
|
142
149
|
labels=("backend", "kind", "status"),
|
|
143
150
|
samples=tuple(runner_processes_by_kind),
|
|
144
151
|
),
|
|
145
152
|
MetricFamily(
|
|
146
153
|
name="dj_queue_recurring_tasks",
|
|
147
154
|
help_text="Current recurring task count by backend",
|
|
155
|
+
metric_type="gauge",
|
|
148
156
|
labels=("backend",),
|
|
149
157
|
samples=tuple(recurring_tasks),
|
|
150
158
|
),
|
|
151
159
|
MetricFamily(
|
|
152
160
|
name="dj_queue_semaphores",
|
|
153
161
|
help_text="Current semaphore count by queue database",
|
|
162
|
+
metric_type="gauge",
|
|
154
163
|
labels=("queue_database",),
|
|
155
164
|
samples=tuple(semaphores),
|
|
156
165
|
),
|
|
157
166
|
MetricFamily(
|
|
158
167
|
name="dj_queue_process_rows",
|
|
159
168
|
help_text="Current process row count by backend",
|
|
169
|
+
metric_type="gauge",
|
|
160
170
|
labels=("backend",),
|
|
161
171
|
samples=tuple(process_rows),
|
|
162
172
|
),
|