dj-queue 0.10.4__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.4 → dj_queue-0.10.5}/PKG-INFO +1 -1
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/admin.py +13 -2
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/api.py +14 -20
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/backend.py +5 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/config.py +60 -22
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/contrib/asgi.py +5 -3
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/contrib/gunicorn.py +29 -9
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/contrib/prometheus.py +6 -3
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/metrics.py +10 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/observability.py +64 -34
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/_helpers.py +9 -3
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/concurrency.py +3 -3
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/jobs.py +30 -13
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/recurring.py +82 -32
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/base.py +21 -2
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/pool.py +18 -1
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/supervisor.py +35 -14
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/worker.py +44 -5
- {dj_queue-0.10.4 → dj_queue-0.10.5}/pyproject.toml +1 -1
- {dj_queue-0.10.4 → dj_queue-0.10.5}/LICENSE +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/README.md +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/apps.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/cron.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/dashboard.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/db.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/hooks.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/log.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.10.4 → 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.4 → 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.4 → dj_queue-0.10.5}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.10.4 → 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.4 → 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.4 → 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.4 → dj_queue-0.10.5}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/cleanup.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/routers.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/errors.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/notify.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/task_results.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/urls.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/dj_queue/views.py +0 -0
- {dj_queue-0.10.4 → dj_queue-0.10.5}/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
|
|
@@ -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
|
),
|
|
@@ -29,6 +29,9 @@ from dj_queue.queue_selectors import queue_matches_selectors
|
|
|
29
29
|
from dj_queue.queue_state import queue_state_count_fields, queue_state_counts
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
_NOT_PROVIDED = object()
|
|
33
|
+
|
|
34
|
+
|
|
32
35
|
@dataclass(frozen=True, slots=True)
|
|
33
36
|
class BackendChoice:
|
|
34
37
|
alias: str
|
|
@@ -207,7 +210,6 @@ def queue_rows(*, backend_alias, now, process_cutoff):
|
|
|
207
210
|
failed_count=failed_counts.get(queue_name, 0),
|
|
208
211
|
finished_count=finished_counts.get(queue_name, 0),
|
|
209
212
|
paused=queue_name in paused_queues,
|
|
210
|
-
recurring=queue_name in recurring_queues,
|
|
211
213
|
oldest_ready_at=oldest_ready.get(queue_name),
|
|
212
214
|
oldest_scheduled_at=oldest_scheduled.get(queue_name),
|
|
213
215
|
oldest_blocked_at=oldest_blocked.get(queue_name),
|
|
@@ -229,11 +231,10 @@ def queue_snapshot(
|
|
|
229
231
|
blocked_count=None,
|
|
230
232
|
failed_count=None,
|
|
231
233
|
finished_count=None,
|
|
232
|
-
paused=
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
oldest_blocked_at=None,
|
|
234
|
+
paused=_NOT_PROVIDED,
|
|
235
|
+
oldest_ready_at=_NOT_PROVIDED,
|
|
236
|
+
oldest_scheduled_at=_NOT_PROVIDED,
|
|
237
|
+
oldest_blocked_at=_NOT_PROVIDED,
|
|
237
238
|
live_workers=None,
|
|
238
239
|
):
|
|
239
240
|
alias = get_database_alias(backend_alias)
|
|
@@ -245,37 +246,20 @@ def queue_snapshot(
|
|
|
245
246
|
blocked_count = state_counts["blocked"]
|
|
246
247
|
failed_count = state_counts["failed"]
|
|
247
248
|
finished_count = state_counts["finished"]
|
|
248
|
-
if paused is
|
|
249
|
-
paused = (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
.exists()
|
|
256
|
-
)
|
|
257
|
-
if recurring is None:
|
|
258
|
-
recurring = (
|
|
259
|
-
RecurringTask.objects.using(alias)
|
|
260
|
-
.filter(
|
|
261
|
-
backend_alias=backend_alias,
|
|
262
|
-
queue_name=queue_name,
|
|
263
|
-
)
|
|
264
|
-
.exists()
|
|
265
|
-
)
|
|
266
|
-
if oldest_ready_at is None:
|
|
267
|
-
oldest_ready_at = (
|
|
268
|
-
ReadyExecution.objects.using(alias)
|
|
269
|
-
.filter(backend_alias=backend_alias, queue_name=queue_name)
|
|
270
|
-
.aggregate(oldest=Min(Coalesce("latency_started_at", "created_at")))["oldest"]
|
|
249
|
+
if paused is _NOT_PROVIDED:
|
|
250
|
+
paused = queue_is_paused(backend_alias=backend_alias, queue_name=queue_name)
|
|
251
|
+
if oldest_ready_at is _NOT_PROVIDED:
|
|
252
|
+
oldest_ready_at = oldest_ready_at_for_queue(
|
|
253
|
+
backend_alias=backend_alias,
|
|
254
|
+
queue_name=queue_name,
|
|
271
255
|
)
|
|
272
|
-
if oldest_scheduled_at is
|
|
256
|
+
if oldest_scheduled_at is _NOT_PROVIDED:
|
|
273
257
|
oldest_scheduled_at = (
|
|
274
258
|
ScheduledExecution.objects.using(alias)
|
|
275
259
|
.filter(backend_alias=backend_alias, queue_name=queue_name)
|
|
276
260
|
.aggregate(oldest=Min("scheduled_at"))["oldest"]
|
|
277
261
|
)
|
|
278
|
-
if oldest_blocked_at is
|
|
262
|
+
if oldest_blocked_at is _NOT_PROVIDED:
|
|
279
263
|
oldest_blocked_at = (
|
|
280
264
|
BlockedExecution.objects.using(alias)
|
|
281
265
|
.filter(backend_alias=backend_alias, queue_name=queue_name)
|
|
@@ -291,9 +275,13 @@ def queue_snapshot(
|
|
|
291
275
|
)
|
|
292
276
|
)
|
|
293
277
|
|
|
294
|
-
latency_seconds =
|
|
295
|
-
|
|
296
|
-
|
|
278
|
+
latency_seconds = queue_latency_seconds(
|
|
279
|
+
backend_alias=backend_alias,
|
|
280
|
+
queue_name=queue_name,
|
|
281
|
+
now=now,
|
|
282
|
+
paused=paused,
|
|
283
|
+
oldest_ready_at=oldest_ready_at,
|
|
284
|
+
)
|
|
297
285
|
|
|
298
286
|
state_count_fields = queue_state_count_fields(
|
|
299
287
|
{
|
|
@@ -321,6 +309,46 @@ def queue_snapshot(
|
|
|
321
309
|
}
|
|
322
310
|
|
|
323
311
|
|
|
312
|
+
def queue_is_paused(*, backend_alias, queue_name):
|
|
313
|
+
alias = get_database_alias(backend_alias)
|
|
314
|
+
return (
|
|
315
|
+
Pause.objects.using(alias)
|
|
316
|
+
.filter(
|
|
317
|
+
backend_alias=backend_alias,
|
|
318
|
+
queue_name=queue_name,
|
|
319
|
+
)
|
|
320
|
+
.exists()
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def queue_latency_seconds(
|
|
325
|
+
*, backend_alias, queue_name, now=None, paused=None, oldest_ready_at=_NOT_PROVIDED
|
|
326
|
+
):
|
|
327
|
+
if now is None:
|
|
328
|
+
now = timezone.now()
|
|
329
|
+
if paused is None:
|
|
330
|
+
paused = queue_is_paused(backend_alias=backend_alias, queue_name=queue_name)
|
|
331
|
+
if paused:
|
|
332
|
+
return None
|
|
333
|
+
if oldest_ready_at is _NOT_PROVIDED:
|
|
334
|
+
oldest_ready_at = oldest_ready_at_for_queue(
|
|
335
|
+
backend_alias=backend_alias,
|
|
336
|
+
queue_name=queue_name,
|
|
337
|
+
)
|
|
338
|
+
if oldest_ready_at is None:
|
|
339
|
+
return None
|
|
340
|
+
return max((now - oldest_ready_at).total_seconds(), 0.0)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def oldest_ready_at_for_queue(*, backend_alias, queue_name):
|
|
344
|
+
alias = get_database_alias(backend_alias)
|
|
345
|
+
return (
|
|
346
|
+
ReadyExecution.objects.using(alias)
|
|
347
|
+
.filter(backend_alias=backend_alias, queue_name=queue_name)
|
|
348
|
+
.aggregate(oldest=Min(Coalesce("latency_started_at", "created_at")))["oldest"]
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
324
352
|
def process_rows(*, backend_alias, now, process_cutoff, scope):
|
|
325
353
|
alias = get_database_alias(backend_alias)
|
|
326
354
|
queryset = Process.objects.using(alias).select_related("supervisor")
|
|
@@ -412,6 +440,8 @@ def semaphore_rows_for_backend(*, backend_alias):
|
|
|
412
440
|
)
|
|
413
441
|
return [
|
|
414
442
|
{
|
|
443
|
+
"scope": "queue_database",
|
|
444
|
+
"queue_database_alias": alias,
|
|
415
445
|
"key": semaphore.key,
|
|
416
446
|
"available_slots": semaphore.value,
|
|
417
447
|
"limit": semaphore.limit,
|
|
@@ -220,7 +220,7 @@ def _ready_execution_fields(
|
|
|
220
220
|
):
|
|
221
221
|
fields = {
|
|
222
222
|
"job": job,
|
|
223
|
-
"backend_alias": backend_alias,
|
|
223
|
+
"backend_alias": _execution_backend_alias(job, backend_alias),
|
|
224
224
|
"queue_name": _execution_queue_name(job, queue_name),
|
|
225
225
|
"priority": _execution_priority(job, priority),
|
|
226
226
|
"latency_started_at": ready_at,
|
|
@@ -233,7 +233,7 @@ def _ready_execution_fields(
|
|
|
233
233
|
def _scheduled_execution_fields(job, *, backend_alias, scheduled_at=None, created_at=None):
|
|
234
234
|
fields = {
|
|
235
235
|
"job": job,
|
|
236
|
-
"backend_alias": backend_alias,
|
|
236
|
+
"backend_alias": _execution_backend_alias(job, backend_alias),
|
|
237
237
|
"queue_name": job.queue_name,
|
|
238
238
|
"priority": job.priority,
|
|
239
239
|
"scheduled_at": scheduled_at if scheduled_at is not None else job.scheduled_at,
|
|
@@ -254,7 +254,7 @@ def _blocked_execution_fields(
|
|
|
254
254
|
):
|
|
255
255
|
return {
|
|
256
256
|
"job": job,
|
|
257
|
-
"backend_alias": backend_alias,
|
|
257
|
+
"backend_alias": _execution_backend_alias(job, backend_alias),
|
|
258
258
|
"queue_name": _execution_queue_name(job, queue_name),
|
|
259
259
|
"priority": _execution_priority(job, priority),
|
|
260
260
|
"concurrency_key": concurrency_key if concurrency_key is not None else job.concurrency_key,
|
|
@@ -262,6 +262,12 @@ def _blocked_execution_fields(
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
|
|
265
|
+
def _execution_backend_alias(job, backend_alias):
|
|
266
|
+
if job.backend_alias != backend_alias:
|
|
267
|
+
raise EnqueueError(f"job {job.id} belongs to backend {job.backend_alias!r}")
|
|
268
|
+
return job.backend_alias
|
|
269
|
+
|
|
270
|
+
|
|
265
271
|
def _execution_queue_name(job, queue_name):
|
|
266
272
|
return job.queue_name if queue_name is None else queue_name
|
|
267
273
|
|