dj-queue 0.10.4__tar.gz → 0.10.6__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.4 → dj_queue-0.10.6}/PKG-INFO +1 -1
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/admin.py +13 -2
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/api.py +14 -20
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/backend.py +5 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/config.py +63 -23
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/contrib/asgi.py +11 -4
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/contrib/gunicorn.py +56 -14
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/contrib/prometheus.py +6 -3
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/dashboard.py +6 -6
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/metrics.py +17 -7
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/observability.py +112 -164
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/_helpers.py +71 -13
- dj_queue-0.10.6/dj_queue/operations/_insert.py +36 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/cleanup.py +11 -5
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/concurrency.py +52 -27
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/jobs.py +99 -69
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/recurring.py +82 -32
- dj_queue-0.10.6/dj_queue/queue_state.py +270 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/base.py +28 -3
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/errors.py +0 -1
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/notify.py +2 -1
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/pool.py +18 -1
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/supervisor.py +40 -17
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/worker.py +44 -5
- {dj_queue-0.10.4 → dj_queue-0.10.6}/pyproject.toml +1 -1
- dj_queue-0.10.4/dj_queue/operations/_insert.py +0 -24
- dj_queue-0.10.4/dj_queue/queue_state.py +0 -138
- {dj_queue-0.10.4 → dj_queue-0.10.6}/LICENSE +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/README.md +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/db.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/log.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/views.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.6}/dj_queue/wakeup.py +0 -0
|
@@ -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)
|
|
@@ -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
|
|
@@ -149,12 +149,14 @@ def load_backend_config(
|
|
|
149
149
|
if tasks_settings is None:
|
|
150
150
|
tasks_settings = getattr(settings, "TASKS", {})
|
|
151
151
|
|
|
152
|
+
ensure_dj_queue_backend_alias(tasks_settings, backend_alias)
|
|
153
|
+
backend_block = _backend_block(tasks_settings, backend_alias)
|
|
152
154
|
env_values = {key: env.get(key) for key in CONFIG_ENV_KEYS if env.get(key) is not None}
|
|
153
155
|
cache_key = (
|
|
154
156
|
backend_alias,
|
|
155
157
|
_cache_key(cli_overrides),
|
|
156
158
|
_cache_key(env_values),
|
|
157
|
-
_cache_key(
|
|
159
|
+
_cache_key(backend_block),
|
|
158
160
|
)
|
|
159
161
|
if cache_key not in _BACKEND_CONFIG_CACHE:
|
|
160
162
|
_BACKEND_CONFIG_CACHE[cache_key] = _load_backend_config_uncached(
|
|
@@ -248,7 +250,9 @@ def _load_backend_config_uncached(
|
|
|
248
250
|
resolved_options["process_alive_threshold"], "process_alive_threshold"
|
|
249
251
|
),
|
|
250
252
|
shutdown_timeout=_nonnegative_float(resolved_options["shutdown_timeout"], "shutdown_timeout"),
|
|
251
|
-
supervisor_pidfile=
|
|
253
|
+
supervisor_pidfile=_optional_string_option(
|
|
254
|
+
resolved_options["supervisor_pidfile"], "supervisor_pidfile"
|
|
255
|
+
),
|
|
252
256
|
preserve_finished_jobs=preserve_finished_jobs,
|
|
253
257
|
clear_finished_jobs_after=_optional_nonnegative_int(
|
|
254
258
|
resolved_options["clear_finished_jobs_after"], "clear_finished_jobs_after"
|
|
@@ -264,7 +268,7 @@ def _load_backend_config_uncached(
|
|
|
264
268
|
resolved_options["default_concurrency_duration"],
|
|
265
269
|
"default_concurrency_duration",
|
|
266
270
|
),
|
|
267
|
-
database_alias=
|
|
271
|
+
database_alias=_string_option(resolved_options["database_alias"], "database_alias"),
|
|
268
272
|
use_skip_locked=_bool_option(resolved_options["use_skip_locked"], "use_skip_locked"),
|
|
269
273
|
listen_notify=_bool_option(resolved_options["listen_notify"], "listen_notify"),
|
|
270
274
|
silence_polling=_bool_option(resolved_options["silence_polling"], "silence_polling"),
|
|
@@ -428,7 +432,7 @@ def _validated_callback_path(callback_path: Any) -> str | None:
|
|
|
428
432
|
if callback_path in (None, ""):
|
|
429
433
|
return None
|
|
430
434
|
|
|
431
|
-
callback_path =
|
|
435
|
+
callback_path = _string_option(callback_path, "on_thread_error")
|
|
432
436
|
try:
|
|
433
437
|
import_string(callback_path)
|
|
434
438
|
except ImportError as exc:
|
|
@@ -546,24 +550,31 @@ def _build_recurring_config(
|
|
|
546
550
|
|
|
547
551
|
recurring: dict[str, RecurringTaskConfig] = {}
|
|
548
552
|
for key, raw_entry in raw_recurring.items():
|
|
553
|
+
key = _string_option(key, "recurring task key")
|
|
549
554
|
if not isinstance(raw_entry, Mapping):
|
|
550
555
|
raise ImproperlyConfigured("recurring entries must be mappings")
|
|
551
556
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if
|
|
557
|
+
raw_task_path = raw_entry.get("task_path")
|
|
558
|
+
raw_schedule = raw_entry.get("schedule")
|
|
559
|
+
if raw_task_path in (None, "") or raw_schedule in (None, ""):
|
|
555
560
|
raise ImproperlyConfigured(f"recurring task {key!r} requires task_path and schedule")
|
|
556
|
-
|
|
561
|
+
task_path = _string_option(raw_task_path, f"recurring task {key!r} task_path")
|
|
562
|
+
schedule = _string_option(raw_schedule, f"recurring task {key!r} schedule")
|
|
563
|
+
if not is_valid_cron(schedule):
|
|
557
564
|
raise ImproperlyConfigured(f"recurring task {key!r} has an invalid cron schedule")
|
|
558
565
|
|
|
559
|
-
queue_name =
|
|
566
|
+
queue_name = _string_option(
|
|
567
|
+
raw_entry.get("queue_name", "default"), f"recurring task {key!r} queue_name"
|
|
568
|
+
)
|
|
560
569
|
priority = _priority_int(raw_entry.get("priority", 0), f"recurring task {key!r} priority")
|
|
561
570
|
if allowed_queues and queue_name not in allowed_queues:
|
|
562
571
|
raise ImproperlyConfigured(
|
|
563
572
|
f"recurring task {key!r} is invalid: queue {queue_name!r} is not allowed for backend {backend_alias!r}"
|
|
564
573
|
)
|
|
574
|
+
args = _tuple_option(raw_entry.get("args", []), f"recurring task {key!r} args")
|
|
575
|
+
kwargs = _dict_option(raw_entry.get("kwargs", {}), f"recurring task {key!r} kwargs")
|
|
565
576
|
try:
|
|
566
|
-
task = import_string(
|
|
577
|
+
task = import_string(task_path)
|
|
567
578
|
except ImportError as exc:
|
|
568
579
|
raise ImproperlyConfigured(f"recurring task {key!r} is invalid: {exc}") from exc
|
|
569
580
|
if not hasattr(task, "using"):
|
|
@@ -571,15 +582,17 @@ def _build_recurring_config(
|
|
|
571
582
|
f"recurring task {key!r} is invalid: task_path must reference a Django task"
|
|
572
583
|
)
|
|
573
584
|
|
|
574
|
-
recurring[
|
|
575
|
-
key=
|
|
576
|
-
task_path=
|
|
577
|
-
schedule=
|
|
578
|
-
args=
|
|
579
|
-
kwargs=
|
|
585
|
+
recurring[key] = RecurringTaskConfig(
|
|
586
|
+
key=key,
|
|
587
|
+
task_path=task_path,
|
|
588
|
+
schedule=schedule,
|
|
589
|
+
args=args,
|
|
590
|
+
kwargs=kwargs,
|
|
580
591
|
queue_name=queue_name,
|
|
581
592
|
priority=priority,
|
|
582
|
-
description=
|
|
593
|
+
description=_string_option(
|
|
594
|
+
raw_entry.get("description", ""), f"recurring task {key!r} description"
|
|
595
|
+
),
|
|
583
596
|
)
|
|
584
597
|
return recurring
|
|
585
598
|
|
|
@@ -612,9 +625,36 @@ def _as_string_tuple(value: Any) -> tuple[str, ...]:
|
|
|
612
625
|
return ()
|
|
613
626
|
if isinstance(value, str):
|
|
614
627
|
return (value,)
|
|
615
|
-
if not isinstance(value, Sequence):
|
|
628
|
+
if not isinstance(value, Sequence) or isinstance(value, (bytes, bytearray)):
|
|
629
|
+
raise ImproperlyConfigured("expected a string or a sequence of strings")
|
|
630
|
+
values = tuple(value)
|
|
631
|
+
if not all(isinstance(item, str) for item in values):
|
|
616
632
|
raise ImproperlyConfigured("expected a string or a sequence of strings")
|
|
617
|
-
return
|
|
633
|
+
return values
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _string_option(value: Any, setting_name: str) -> str:
|
|
637
|
+
if not isinstance(value, str):
|
|
638
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a string")
|
|
639
|
+
return value
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _optional_string_option(value: Any, setting_name: str) -> str | None:
|
|
643
|
+
if value is None:
|
|
644
|
+
return None
|
|
645
|
+
return _string_option(value, setting_name)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _tuple_option(value: Any, setting_name: str) -> tuple[Any, ...]:
|
|
649
|
+
if isinstance(value, str) or not isinstance(value, Sequence):
|
|
650
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a sequence")
|
|
651
|
+
return tuple(value)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _dict_option(value: Any, setting_name: str) -> dict[str, Any]:
|
|
655
|
+
if not isinstance(value, Mapping):
|
|
656
|
+
raise ImproperlyConfigured(f"dj_queue {setting_name} must be a mapping")
|
|
657
|
+
return dict(value)
|
|
618
658
|
|
|
619
659
|
|
|
620
660
|
def _optional_nonnegative_int(value: Any, setting_name: str) -> int | None:
|
|
@@ -24,13 +24,18 @@ class DjQueueLifespan:
|
|
|
24
24
|
self.supervisor is not None and self._poll_stop is not None and not self._poll_stop.is_set()
|
|
25
25
|
):
|
|
26
26
|
try:
|
|
27
|
-
|
|
27
|
+
poll_once = getattr(self.supervisor, "poll_once_if_running", self.supervisor.poll_once)
|
|
28
|
+
keep_polling = await asyncio.to_thread(poll_once)
|
|
28
29
|
except Exception as error:
|
|
29
30
|
handle_thread_error(
|
|
30
31
|
error,
|
|
31
32
|
context="supervisor.run",
|
|
32
33
|
backend_alias=self.supervisor.backend_alias,
|
|
33
34
|
)
|
|
35
|
+
keep_polling = True
|
|
36
|
+
|
|
37
|
+
if keep_polling is False:
|
|
38
|
+
return
|
|
34
39
|
|
|
35
40
|
if self.supervisor is None or self._poll_stop is None or self._poll_stop.is_set():
|
|
36
41
|
return
|
|
@@ -104,8 +109,10 @@ class DjQueueLifespan:
|
|
|
104
109
|
|
|
105
110
|
receive_queue = asyncio.Queue()
|
|
106
111
|
send_queue = asyncio.Queue()
|
|
107
|
-
app_task = await self._start_wrapped_lifespan_app(scope, receive_queue.get, send_queue.put)
|
|
108
112
|
wrapped_app_supports_lifespan = self.forward_wrapped_lifespan
|
|
113
|
+
app_task = None
|
|
114
|
+
if wrapped_app_supports_lifespan:
|
|
115
|
+
app_task = await self._start_wrapped_lifespan_app(scope, receive_queue.get, send_queue.put)
|
|
109
116
|
|
|
110
117
|
try:
|
|
111
118
|
while True:
|
|
@@ -137,12 +144,12 @@ class DjQueueLifespan:
|
|
|
137
144
|
if response is None:
|
|
138
145
|
response = {"type": "lifespan.shutdown.complete"}
|
|
139
146
|
await send(response)
|
|
140
|
-
if wrapped_app_supports_lifespan:
|
|
147
|
+
if wrapped_app_supports_lifespan and app_task is not None:
|
|
141
148
|
await app_task
|
|
142
149
|
return
|
|
143
150
|
finally:
|
|
144
151
|
await self._stop_supervisor()
|
|
145
|
-
if not app_task.done():
|
|
152
|
+
if app_task is not None and not app_task.done():
|
|
146
153
|
app_task.cancel()
|
|
147
154
|
with suppress(asyncio.CancelledError):
|
|
148
155
|
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,11 +21,17 @@ def _set_supervisor_state(worker, **state):
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _start_embedded_supervisor(worker, *, backend_alias="default"):
|
|
23
|
-
|
|
24
|
+
if getattr(worker, "_dj_queue_supervisor_exiting", False):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
lock_file = _try_acquire_supervisor_lock(backend_alias=backend_alias)
|
|
24
28
|
if lock_file is None:
|
|
25
29
|
return None
|
|
26
30
|
|
|
27
31
|
try:
|
|
32
|
+
if getattr(worker, "_dj_queue_supervisor_exiting", False):
|
|
33
|
+
_release_supervisor_lock(lock_file)
|
|
34
|
+
return None
|
|
28
35
|
supervisor = build_supervisor(backend_alias=backend_alias)
|
|
29
36
|
poll_stop = threading.Event()
|
|
30
37
|
_set_supervisor_state(
|
|
@@ -34,6 +41,17 @@ def _start_embedded_supervisor(worker, *, backend_alias="default"):
|
|
|
34
41
|
supervisor_poll_stop=poll_stop,
|
|
35
42
|
)
|
|
36
43
|
supervisor.start()
|
|
44
|
+
if getattr(worker, "_dj_queue_supervisor_exiting", False):
|
|
45
|
+
supervisor.stop()
|
|
46
|
+
_release_supervisor_lock(lock_file)
|
|
47
|
+
_set_supervisor_state(
|
|
48
|
+
worker,
|
|
49
|
+
supervisor_lock=None,
|
|
50
|
+
supervisor=None,
|
|
51
|
+
supervisor_poll_stop=None,
|
|
52
|
+
supervisor_poll_thread=None,
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
37
55
|
except Exception:
|
|
38
56
|
_release_supervisor_lock(lock_file)
|
|
39
57
|
_set_supervisor_state(
|
|
@@ -49,7 +67,9 @@ def _start_embedded_supervisor(worker, *, backend_alias="default"):
|
|
|
49
67
|
stop_event = worker._dj_queue_supervisor_poll_stop
|
|
50
68
|
while stop_event.wait(supervisor.polling_interval) is False:
|
|
51
69
|
try:
|
|
52
|
-
supervisor.poll_once
|
|
70
|
+
poll_once = getattr(supervisor, "poll_once_if_running", supervisor.poll_once)
|
|
71
|
+
if poll_once() is False:
|
|
72
|
+
return
|
|
53
73
|
except Exception as error:
|
|
54
74
|
handle_thread_error(
|
|
55
75
|
error,
|
|
@@ -70,7 +90,12 @@ def _start_lock_retry_loop(worker, *, backend_alias="default"):
|
|
|
70
90
|
while retry_stop.wait(LOCK_RETRY_INTERVAL) is False:
|
|
71
91
|
if getattr(worker, "_dj_queue_supervisor", None) is not None:
|
|
72
92
|
return
|
|
73
|
-
|
|
93
|
+
try:
|
|
94
|
+
supervisor = _start_embedded_supervisor(worker, backend_alias=backend_alias)
|
|
95
|
+
except Exception as error:
|
|
96
|
+
handle_thread_error(error, context="gunicorn.supervisor", backend_alias=backend_alias)
|
|
97
|
+
continue
|
|
98
|
+
if supervisor is not None:
|
|
74
99
|
retry_stop.set()
|
|
75
100
|
return
|
|
76
101
|
|
|
@@ -81,7 +106,8 @@ def _start_lock_retry_loop(worker, *, backend_alias="default"):
|
|
|
81
106
|
retry_thread.start()
|
|
82
107
|
|
|
83
108
|
|
|
84
|
-
def post_fork(
|
|
109
|
+
def post_fork(server, worker):
|
|
110
|
+
backend_alias = _backend_alias(server, worker)
|
|
85
111
|
_set_supervisor_state(
|
|
86
112
|
worker,
|
|
87
113
|
supervisor=None,
|
|
@@ -90,20 +116,18 @@ def post_fork(_server, worker):
|
|
|
90
116
|
supervisor_poll_thread=None,
|
|
91
117
|
supervisor_retry_stop=None,
|
|
92
118
|
supervisor_retry_thread=None,
|
|
119
|
+
supervisor_exiting=False,
|
|
93
120
|
)
|
|
94
|
-
supervisor = _start_embedded_supervisor(worker)
|
|
121
|
+
supervisor = _start_embedded_supervisor(worker, backend_alias=backend_alias)
|
|
95
122
|
if supervisor is not None:
|
|
96
123
|
return supervisor
|
|
97
124
|
|
|
98
|
-
_start_lock_retry_loop(worker)
|
|
125
|
+
_start_lock_retry_loop(worker, backend_alias=backend_alias)
|
|
99
126
|
return None
|
|
100
127
|
|
|
101
128
|
|
|
102
129
|
def worker_exit(_server, worker):
|
|
103
|
-
|
|
104
|
-
lock_file = getattr(worker, "_dj_queue_supervisor_lock", None)
|
|
105
|
-
stop_event = getattr(worker, "_dj_queue_supervisor_poll_stop", None)
|
|
106
|
-
poll_thread = getattr(worker, "_dj_queue_supervisor_poll_thread", None)
|
|
130
|
+
worker._dj_queue_supervisor_exiting = True
|
|
107
131
|
retry_stop = getattr(worker, "_dj_queue_supervisor_retry_stop", None)
|
|
108
132
|
retry_thread = getattr(worker, "_dj_queue_supervisor_retry_thread", None)
|
|
109
133
|
|
|
@@ -114,6 +138,11 @@ def worker_exit(_server, worker):
|
|
|
114
138
|
worker._dj_queue_supervisor_retry_thread = None
|
|
115
139
|
worker._dj_queue_supervisor_retry_stop = None
|
|
116
140
|
|
|
141
|
+
supervisor = getattr(worker, "_dj_queue_supervisor", None)
|
|
142
|
+
lock_file = getattr(worker, "_dj_queue_supervisor_lock", None)
|
|
143
|
+
stop_event = getattr(worker, "_dj_queue_supervisor_poll_stop", None)
|
|
144
|
+
poll_thread = getattr(worker, "_dj_queue_supervisor_poll_thread", None)
|
|
145
|
+
|
|
117
146
|
if stop_event is not None:
|
|
118
147
|
stop_event.set()
|
|
119
148
|
if poll_thread is not None:
|
|
@@ -133,9 +162,22 @@ def worker_exit(_server, worker):
|
|
|
133
162
|
return None
|
|
134
163
|
|
|
135
164
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
def _backend_alias(server, worker):
|
|
166
|
+
return getattr(worker, "dj_queue_backend_alias", None) or getattr(
|
|
167
|
+
server, "dj_queue_backend_alias", "default"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _supervisor_lock_path(*, backend_alias):
|
|
172
|
+
lock_scope = f"{Path.cwd()}:{backend_alias}"
|
|
173
|
+
digest = hashlib.sha256(lock_scope.encode()).hexdigest()[:12]
|
|
174
|
+
return Path(tempfile.gettempdir()) / f"{LOCK_PATH_PREFIX}_{backend_alias}_{digest}.lock"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _try_acquire_supervisor_lock(*, backend_alias="default"):
|
|
178
|
+
lock_path = _supervisor_lock_path(backend_alias=backend_alias)
|
|
179
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
lock_file = lock_path.open("a+")
|
|
139
181
|
try:
|
|
140
182
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
141
183
|
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())
|
|
@@ -268,8 +268,8 @@ def dashboard_context(*, backend_alias, query_params=None):
|
|
|
268
268
|
query_params = {}
|
|
269
269
|
|
|
270
270
|
snapshot = observability.backend_snapshot(backend_alias=backend_alias)
|
|
271
|
-
queue_rows = snapshot
|
|
272
|
-
process_rows = snapshot
|
|
271
|
+
queue_rows = snapshot.queue_rows
|
|
272
|
+
process_rows = snapshot.process_rows
|
|
273
273
|
recurring_rows = [
|
|
274
274
|
{
|
|
275
275
|
**row,
|
|
@@ -278,7 +278,7 @@ def dashboard_context(*, backend_alias, query_params=None):
|
|
|
278
278
|
recurring_task_key=row["key"],
|
|
279
279
|
),
|
|
280
280
|
}
|
|
281
|
-
for row in snapshot
|
|
281
|
+
for row in snapshot.recurring_rows
|
|
282
282
|
]
|
|
283
283
|
semaphore_rows = [
|
|
284
284
|
{
|
|
@@ -288,14 +288,14 @@ def dashboard_context(*, backend_alias, query_params=None):
|
|
|
288
288
|
concurrency_key=row["key"],
|
|
289
289
|
),
|
|
290
290
|
}
|
|
291
|
-
for row in snapshot
|
|
291
|
+
for row in snapshot.semaphore_rows
|
|
292
292
|
]
|
|
293
293
|
|
|
294
294
|
return {
|
|
295
295
|
"backend_alias": backend_alias,
|
|
296
296
|
"backend_choices": backend_choices(),
|
|
297
297
|
"config": config,
|
|
298
|
-
"queue_database_alias": snapshot
|
|
298
|
+
"queue_database_alias": snapshot.queue_database_alias,
|
|
299
299
|
"summary_cards": _summary_cards(
|
|
300
300
|
backend_alias=backend_alias,
|
|
301
301
|
queue_rows=queue_rows,
|
|
@@ -305,7 +305,7 @@ def dashboard_context(*, backend_alias, query_params=None):
|
|
|
305
305
|
),
|
|
306
306
|
"backend_facts": _backend_facts(
|
|
307
307
|
config=config,
|
|
308
|
-
queue_database_alias=snapshot
|
|
308
|
+
queue_database_alias=snapshot.queue_database_alias,
|
|
309
309
|
recurring_count=len(recurring_rows),
|
|
310
310
|
semaphore_count=len(semaphore_rows),
|
|
311
311
|
),
|
|
@@ -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
|
|
|
@@ -34,11 +35,11 @@ def metric_families(*, snapshots=None):
|
|
|
34
35
|
seen_queue_databases = set()
|
|
35
36
|
|
|
36
37
|
for snapshot in snapshots:
|
|
37
|
-
backend_alias = snapshot
|
|
38
|
-
queue_database_alias = snapshot
|
|
39
|
-
runner_metrics = snapshot
|
|
38
|
+
backend_alias = snapshot.backend_alias
|
|
39
|
+
queue_database_alias = snapshot.queue_database_alias
|
|
40
|
+
runner_metrics = snapshot.runner_metrics
|
|
40
41
|
|
|
41
|
-
for queue in snapshot
|
|
42
|
+
for queue in snapshot.queue_rows:
|
|
42
43
|
for definition in QUEUE_STATE_DEFINITIONS:
|
|
43
44
|
queue_jobs.append(
|
|
44
45
|
MetricSample(
|
|
@@ -85,13 +86,13 @@ def metric_families(*, snapshots=None):
|
|
|
85
86
|
recurring_tasks.append(
|
|
86
87
|
MetricSample(
|
|
87
88
|
labels=(backend_alias,),
|
|
88
|
-
value=len(snapshot
|
|
89
|
+
value=len(snapshot.recurring_rows),
|
|
89
90
|
)
|
|
90
91
|
)
|
|
91
92
|
process_rows.append(
|
|
92
93
|
MetricSample(
|
|
93
94
|
labels=(backend_alias,),
|
|
94
|
-
value=len(snapshot
|
|
95
|
+
value=len(snapshot.process_rows),
|
|
95
96
|
)
|
|
96
97
|
)
|
|
97
98
|
|
|
@@ -101,7 +102,7 @@ def metric_families(*, snapshots=None):
|
|
|
101
102
|
semaphores.append(
|
|
102
103
|
MetricSample(
|
|
103
104
|
labels=(queue_database_alias,),
|
|
104
|
-
value=len(snapshot
|
|
105
|
+
value=len(snapshot.semaphore_rows),
|
|
105
106
|
)
|
|
106
107
|
)
|
|
107
108
|
|
|
@@ -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
|
),
|