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.
Files changed (96) hide show
  1. {dj_queue-0.13.0 → dj_queue-0.13.1}/PKG-INFO +1 -1
  2. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/db.py +74 -0
  3. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/_helpers.py +6 -0
  4. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/concurrency.py +35 -33
  5. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/jobs.py +274 -302
  6. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/recurring.py +22 -5
  7. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/base.py +8 -1
  8. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/mysql.py +19 -1
  9. {dj_queue-0.13.0 → dj_queue-0.13.1}/pyproject.toml +1 -1
  10. {dj_queue-0.13.0 → dj_queue-0.13.1}/LICENSE +0 -0
  11. {dj_queue-0.13.0 → dj_queue-0.13.1}/README.md +0 -0
  12. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/__init__.py +0 -0
  13. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/admin.py +0 -0
  14. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/api.py +0 -0
  15. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/apps.py +0 -0
  16. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/backend.py +0 -0
  17. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/config.py +0 -0
  18. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/__init__.py +0 -0
  19. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/asgi.py +0 -0
  20. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/gunicorn.py +0 -0
  21. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/contrib/prometheus.py +0 -0
  22. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/cron.py +0 -0
  23. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/dashboard.py +0 -0
  24. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/dashboard_actions.py +0 -0
  25. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/exceptions.py +0 -0
  26. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/hooks.py +0 -0
  27. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/log.py +0 -0
  28. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/__init__.py +0 -0
  29. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/__init__.py +0 -0
  30. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue.py +0 -0
  31. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_health.py +0 -0
  32. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_postgres_autovacuum.py +0 -0
  33. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/management/commands/dj_queue_prune.py +0 -0
  34. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/metrics.py +0 -0
  35. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0001_initial.py +0 -0
  36. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0002_pause_semaphore.py +0 -0
  37. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0003_recurringtask_recurringexecution.py +0 -0
  38. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0004_dashboard.py +0 -0
  39. {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
  40. {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
  41. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0007_recurringtask_next_run_at.py +0 -0
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/0013_failedexecution_retry_at_and_more.py +0 -0
  48. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/migrations/__init__.py +0 -0
  49. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/__init__.py +0 -0
  50. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/jobs.py +0 -0
  51. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/recurring.py +0 -0
  52. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/models/runtime.py +0 -0
  53. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/observability.py +0 -0
  54. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/__init__.py +0 -0
  55. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/_insert.py +0 -0
  56. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/cleanup.py +0 -0
  57. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/operations/queues.py +0 -0
  58. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/queue_selectors.py +0 -0
  59. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/queue_state.py +0 -0
  60. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/routers.py +0 -0
  61. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/__init__.py +0 -0
  62. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/connection_budget.py +0 -0
  63. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/dispatcher.py +0 -0
  64. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/errors.py +0 -0
  65. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/interruptible.py +0 -0
  66. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/notify.py +0 -0
  67. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/pidfile.py +0 -0
  68. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/pool.py +0 -0
  69. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/procline.py +0 -0
  70. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/scheduler.py +0 -0
  71. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/supervisor.py +0 -0
  72. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/topology.py +0 -0
  73. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/runtime/worker.py +0 -0
  74. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/__init__.py +0 -0
  75. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/common.py +0 -0
  76. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/postgres.py +0 -0
  77. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/sqlite.py +0 -0
  78. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/sql/state.py +0 -0
  79. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/task_results.py +0 -0
  80. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_process_rows.html +0 -0
  81. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_recurring_rows.html +0 -0
  82. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_section_table.html +0 -0
  83. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_dashboard_semaphore_rows.html +0 -0
  84. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_paginator.html +0 -0
  85. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_queue_controls.html +0 -0
  86. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/_sortable_header_cells.html +0 -0
  87. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/change_form.html +0 -0
  88. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/change_list.html +0 -0
  89. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/dashboard.html +0 -0
  90. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/includes/fieldset.html +0 -0
  91. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templates/admin/dj_queue/queue_jobs.html +0 -0
  92. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templatetags/__init__.py +0 -0
  93. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/templatetags/dj_queue_admin.py +0 -0
  94. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/urls.py +0 -0
  95. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/views.py +0 -0
  96. {dj_queue-0.13.0 → dj_queue-0.13.1}/dj_queue/wakeup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dj-queue
3
- Version: 0.13.0
3
+ Version: 0.13.1
4
4
  Summary: Database-backed task queue backend for Django’s Tasks framework.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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 database_capabilities, get_database_alias, locked_queryset
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
- ReadyExecution,
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
- now = timezone.now()
676
- queryset = _expired_semaphore_cleanup_queryset(alias, now=now)
677
- semaphore_ids = list(
678
- queryset.order_by("expires_at", "key").values_list("pk", flat=True)[:batch_size]
679
- )
680
- if not semaphore_ids:
681
- return 0
682
- deleted, _ = (
683
- _expired_semaphore_cleanup_queryset(alias, now=now).filter(pk__in=semaphore_ids).delete()
684
- )
685
- return deleted
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
- def _expired_semaphore_cleanup_queryset(alias, *, now):
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):