dj-queue 0.13.0__tar.gz → 0.13.1__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.13.0 → dj_queue-0.13.1}/PKG-INFO +1 -1
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/db.py +74 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/_helpers.py +6 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/concurrency.py +35 -33
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/jobs.py +274 -302
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/recurring.py +22 -5
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/base.py +8 -1
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/mysql.py +19 -1
- {dj_queue-0.13.0 → dj_queue-0.13.1}/pyproject.toml +1 -1
- {dj_queue-0.13.0 → dj_queue-0.13.1}/LICENSE +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/README.md +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/admin.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/api.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/apps.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/backend.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/config.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/asgi.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/gunicorn.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/prometheus.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/cron.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/dashboard.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/dashboard_actions.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/exceptions.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/hooks.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/log.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_health.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_postgres_autovacuum.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_prune.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/metrics.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0001_initial.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0004_dashboard.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0005_remove_recurringexecution_dj_queue_recurring_executions_task_key_run_at_unique_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0006_blockedexecution_dj_queue_bl_concurr_2d8393_idx_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0008_remove_blockedexecution_dj_queue_bl_concurr_1ce730_idx_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0009_remove_process_dj_queue_processes_name_supervisor_unique_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0010_remove_process_djq_pr_name_parent_uniq_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0011_remove_blockedexecution_djq_bl_b_conc_idx_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0012_job_djq_jobs_b_queue_id_idx_job_djq_jobs_b_conc_idx_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0013_failedexecution_retry_at_and_more.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/jobs.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/recurring.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/runtime.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/observability.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/_insert.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/cleanup.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/queues.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/queue_selectors.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/queue_state.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/routers.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/connection_budget.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/dispatcher.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/errors.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/interruptible.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/notify.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/pidfile.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/pool.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/procline.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/scheduler.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/supervisor.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/topology.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/worker.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/common.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/postgres.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/sqlite.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/state.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/task_results.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templatetags/__init__.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templatetags/dj_queue_admin.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/urls.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/views.py +0 -0
- {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/wakeup.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from collections.abc import Iterator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
from dataclasses import dataclass
|
|
@@ -5,9 +6,36 @@ from typing import Literal
|
|
|
5
6
|
|
|
6
7
|
from django.core.exceptions import ImproperlyConfigured
|
|
7
8
|
from django.db import DEFAULT_DB_ALIAS, connections
|
|
9
|
+
from django.db.utils import OperationalError
|
|
8
10
|
|
|
9
11
|
from dj_queue.config import load_backend_config
|
|
10
12
|
|
|
13
|
+
TRANSIENT_DATABASE_RETRY_ATTEMPTS = 3
|
|
14
|
+
TRANSIENT_DATABASE_RETRY_SLEEP_BASE = 0.01
|
|
15
|
+
TRANSIENT_DATABASE_ERROR_SQLSTATES = {
|
|
16
|
+
"40001", # serialization failure
|
|
17
|
+
"40P01", # deadlock detected
|
|
18
|
+
"55P03", # lock not available
|
|
19
|
+
}
|
|
20
|
+
TRANSIENT_DATABASE_ERROR_ERRNOS = {
|
|
21
|
+
1205, # mysql lock wait timeout
|
|
22
|
+
1213, # mysql deadlock
|
|
23
|
+
}
|
|
24
|
+
TRANSIENT_DATABASE_SQLITE_CODES = {
|
|
25
|
+
5, # SQLITE_BUSY
|
|
26
|
+
6, # SQLITE_LOCKED
|
|
27
|
+
261, # SQLITE_BUSY_RECOVERY
|
|
28
|
+
262, # SQLITE_LOCKED_SHAREDCACHE
|
|
29
|
+
517, # SQLITE_BUSY_SNAPSHOT
|
|
30
|
+
}
|
|
31
|
+
TRANSIENT_DATABASE_ERROR_MESSAGES = (
|
|
32
|
+
"deadlock",
|
|
33
|
+
"lock wait timeout",
|
|
34
|
+
"try restarting transaction",
|
|
35
|
+
"could not serialize access",
|
|
36
|
+
"database is locked",
|
|
37
|
+
)
|
|
38
|
+
|
|
11
39
|
|
|
12
40
|
@dataclass(frozen=True, slots=True)
|
|
13
41
|
class DatabaseCapabilities:
|
|
@@ -72,6 +100,52 @@ def queue_cursor(backend_alias: str = "default") -> Iterator:
|
|
|
72
100
|
yield cursor
|
|
73
101
|
|
|
74
102
|
|
|
103
|
+
def retry_transient_database_errors(operation):
|
|
104
|
+
for attempt in range(TRANSIENT_DATABASE_RETRY_ATTEMPTS):
|
|
105
|
+
try:
|
|
106
|
+
return operation()
|
|
107
|
+
except OperationalError as error:
|
|
108
|
+
if attempt == TRANSIENT_DATABASE_RETRY_ATTEMPTS - 1 or not is_transient_database_error(
|
|
109
|
+
error
|
|
110
|
+
):
|
|
111
|
+
raise
|
|
112
|
+
time.sleep(TRANSIENT_DATABASE_RETRY_SLEEP_BASE * (attempt + 1))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_transient_database_error(error):
|
|
116
|
+
for candidate in _exception_chain(error):
|
|
117
|
+
if _transient_sqlstate(candidate) or _transient_errno(candidate):
|
|
118
|
+
return True
|
|
119
|
+
message = str(error).lower()
|
|
120
|
+
return any(marker in message for marker in TRANSIENT_DATABASE_ERROR_MESSAGES)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _exception_chain(error):
|
|
124
|
+
seen = set()
|
|
125
|
+
while error is not None and id(error) not in seen:
|
|
126
|
+
seen.add(id(error))
|
|
127
|
+
yield error
|
|
128
|
+
error = error.__cause__ or error.__context__
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _transient_sqlstate(error):
|
|
132
|
+
for name in ("pgcode", "sqlstate"):
|
|
133
|
+
value = getattr(error, name, None)
|
|
134
|
+
if value in TRANSIENT_DATABASE_ERROR_SQLSTATES:
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _transient_errno(error):
|
|
140
|
+
sqlite_code = getattr(error, "sqlite_errorcode", None)
|
|
141
|
+
if sqlite_code in TRANSIENT_DATABASE_SQLITE_CODES:
|
|
142
|
+
return True
|
|
143
|
+
errno = getattr(error, "errno", None)
|
|
144
|
+
if errno in TRANSIENT_DATABASE_ERROR_ERRNOS:
|
|
145
|
+
return True
|
|
146
|
+
return bool(error.args and error.args[0] in TRANSIENT_DATABASE_ERROR_ERRNOS)
|
|
147
|
+
|
|
148
|
+
|
|
75
149
|
def _backend_family(connection) -> Literal["postgresql", "mysql", "mariadb", "sqlite"]:
|
|
76
150
|
if connection.vendor == "postgresql":
|
|
77
151
|
return "postgresql"
|
|
@@ -343,6 +343,12 @@ def _execution_priority(job, priority):
|
|
|
343
343
|
|
|
344
344
|
|
|
345
345
|
def _consume_selected_rows(alias, model, rows):
|
|
346
|
+
if database_capabilities(alias).backend_family in {"mysql", "mariadb"}:
|
|
347
|
+
from dj_queue.sql.mysql import consume_ready_rows
|
|
348
|
+
|
|
349
|
+
if model is ReadyExecution:
|
|
350
|
+
return consume_ready_rows(alias, rows)
|
|
351
|
+
|
|
346
352
|
if not database_capabilities(alias).uses_serialized_writes:
|
|
347
353
|
model.objects.using(alias).filter(pk__in=[row.pk for row in rows]).delete()
|
|
348
354
|
return rows
|
|
@@ -10,13 +10,18 @@ from django.utils import timezone
|
|
|
10
10
|
from django.utils.module_loading import import_string
|
|
11
11
|
|
|
12
12
|
from dj_queue.config import load_backend_config
|
|
13
|
-
from dj_queue.db import
|
|
13
|
+
from dj_queue.db import (
|
|
14
|
+
database_capabilities,
|
|
15
|
+
get_database_alias,
|
|
16
|
+
locked_queryset,
|
|
17
|
+
retry_transient_database_errors,
|
|
18
|
+
)
|
|
14
19
|
from dj_queue.exceptions import EnqueueError
|
|
15
20
|
from dj_queue.log import log_event
|
|
16
21
|
from dj_queue.models import (
|
|
17
22
|
BlockedExecution,
|
|
18
23
|
ClaimedExecution,
|
|
19
|
-
|
|
24
|
+
Job,
|
|
20
25
|
Semaphore,
|
|
21
26
|
)
|
|
22
27
|
from dj_queue.operations._helpers import (
|
|
@@ -672,38 +677,35 @@ def _create_claimed_execution_after_blocked_consume(alias, *, job, process_id, c
|
|
|
672
677
|
def cleanup_expired_semaphores(*, batch_size=500, backend_alias="default"):
|
|
673
678
|
batch_size = _positive_int_option(batch_size, "batch_size")
|
|
674
679
|
alias = get_database_alias(backend_alias)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
680
|
+
use_skip_locked = load_backend_config(backend_alias).use_skip_locked
|
|
681
|
+
|
|
682
|
+
def cleanup_transition():
|
|
683
|
+
now = timezone.now()
|
|
684
|
+
with transaction.atomic(using=alias):
|
|
685
|
+
queryset = locked_queryset(
|
|
686
|
+
Semaphore.objects.using(alias).filter(expires_at__lte=now),
|
|
687
|
+
use_skip_locked=use_skip_locked,
|
|
688
|
+
)
|
|
689
|
+
semaphores = list(
|
|
690
|
+
queryset.order_by("expires_at", "key").values_list("pk", "key")[:batch_size]
|
|
691
|
+
)
|
|
692
|
+
if not semaphores:
|
|
693
|
+
return 0
|
|
694
|
+
semaphore_keys = [key for _semaphore_id, key in semaphores]
|
|
695
|
+
active_keys = set(
|
|
696
|
+
Job.objects.using(alias)
|
|
697
|
+
.filter(concurrency_key__in=semaphore_keys)
|
|
698
|
+
.filter(Q(claimed_execution__isnull=False) | Q(ready_execution__isnull=False))
|
|
699
|
+
.values_list("concurrency_key", flat=True)
|
|
700
|
+
.distinct()
|
|
701
|
+
)
|
|
702
|
+
semaphore_ids = [semaphore_id for semaphore_id, key in semaphores if key not in active_keys]
|
|
703
|
+
if not semaphore_ids:
|
|
704
|
+
return 0
|
|
705
|
+
deleted, _ = Semaphore.objects.using(alias).filter(pk__in=semaphore_ids).delete()
|
|
706
|
+
return deleted
|
|
687
707
|
|
|
688
|
-
|
|
689
|
-
claimed_concurrency_keys = (
|
|
690
|
-
ClaimedExecution.objects.using(alias)
|
|
691
|
-
.exclude(job__concurrency_key__isnull=True)
|
|
692
|
-
.exclude(job__concurrency_key="")
|
|
693
|
-
.values_list("job__concurrency_key", flat=True)
|
|
694
|
-
)
|
|
695
|
-
ready_concurrency_keys = (
|
|
696
|
-
ReadyExecution.objects.using(alias)
|
|
697
|
-
.exclude(job__concurrency_key__isnull=True)
|
|
698
|
-
.exclude(job__concurrency_key="")
|
|
699
|
-
.values_list("job__concurrency_key", flat=True)
|
|
700
|
-
)
|
|
701
|
-
return (
|
|
702
|
-
Semaphore.objects.using(alias)
|
|
703
|
-
.filter(expires_at__lte=now)
|
|
704
|
-
.exclude(key__in=claimed_concurrency_keys)
|
|
705
|
-
.exclude(key__in=ready_concurrency_keys)
|
|
706
|
-
)
|
|
708
|
+
return retry_transient_database_errors(cleanup_transition)
|
|
707
709
|
|
|
708
710
|
|
|
709
711
|
def promote_expired_blocked_jobs(*, batch_size=500, backend_alias="default", use_skip_locked=None):
|