simple-module-background-tasks 0.0.23__tar.gz → 0.0.25__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.
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/PKG-INFO +8 -8
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/README.md +3 -3
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/module.py +1 -1
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/worker_inspector.py +27 -5
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/pyproject.toml +5 -5
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_worker_inspector.py +83 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/.gitignore +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/LICENSE +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/__init__.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/_signal_support.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/celery_app.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/constants.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/contracts/__init__.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/contracts/events.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/contracts/schemas.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/deps.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/endpoints/__init__.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/endpoints/api_admin.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/endpoints/views.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/locales/en.json +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/log_context.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/models.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/Detail.tsx +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/Index.tsx +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/Workers.tsx +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/components/ExecutionRow.tsx +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/constants.ts +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/pages/retry.ts +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/py.typed +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/service.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/services.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/settings.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/signals.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/sync_db.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/background_tasks/tasks.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/package.json +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/conftest.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_admin_api.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_beat_schedule.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_bg_service.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_log_context.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_signals.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_sweep_stuck.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_sync_db.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/test_workers_endpoints.py +0 -0
- {simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_background_tasks
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary: Celery + Redis task queue with admin UI for monitoring and retrying failed/stuck tasks
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -23,10 +23,10 @@ Classifier: Typing :: Typed
|
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Requires-Dist: celery[redis]>=5.4
|
|
25
25
|
Requires-Dist: redis>=5
|
|
26
|
-
Requires-Dist: simple-module-core==0.0.
|
|
27
|
-
Requires-Dist: simple-module-db==0.0.
|
|
28
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
29
|
-
Requires-Dist: simple-module-settings==0.0.
|
|
26
|
+
Requires-Dist: simple-module-core==0.0.25
|
|
27
|
+
Requires-Dist: simple-module-db==0.0.25
|
|
28
|
+
Requires-Dist: simple-module-hosting==0.0.25
|
|
29
|
+
Requires-Dist: simple-module-settings==0.0.25
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
# simple_module_background_tasks
|
|
@@ -57,8 +57,7 @@ from celery import shared_task
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
@shared_task(name="reports.generate")
|
|
60
|
-
def generate_report(report_id: int) -> None:
|
|
61
|
-
...
|
|
60
|
+
def generate_report(report_id: int) -> None: ...
|
|
62
61
|
```
|
|
63
62
|
|
|
64
63
|
Declaring `background_tasks` as a `depends_on` ensures the Celery app is built before your tasks run:
|
|
@@ -100,10 +99,11 @@ domain `job_id` that named a Celery task is the canonical example):
|
|
|
100
99
|
from background_tasks import bind_task_context
|
|
101
100
|
from celery import shared_task
|
|
102
101
|
|
|
102
|
+
|
|
103
103
|
@shared_task
|
|
104
104
|
def process_dataset(job_id: int) -> None:
|
|
105
105
|
with bind_task_context(job_id=job_id):
|
|
106
|
-
logger.info("starting ingest")
|
|
106
|
+
logger.info("starting ingest") # now carries job_id too
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
Bindings nest cleanly and restore on exit. structlog users can mount the
|
|
@@ -26,8 +26,7 @@ from celery import shared_task
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@shared_task(name="reports.generate")
|
|
29
|
-
def generate_report(report_id: int) -> None:
|
|
30
|
-
...
|
|
29
|
+
def generate_report(report_id: int) -> None: ...
|
|
31
30
|
```
|
|
32
31
|
|
|
33
32
|
Declaring `background_tasks` as a `depends_on` ensures the Celery app is built before your tasks run:
|
|
@@ -69,10 +68,11 @@ domain `job_id` that named a Celery task is the canonical example):
|
|
|
69
68
|
from background_tasks import bind_task_context
|
|
70
69
|
from celery import shared_task
|
|
71
70
|
|
|
71
|
+
|
|
72
72
|
@shared_task
|
|
73
73
|
def process_dataset(job_id: int) -> None:
|
|
74
74
|
with bind_task_context(job_id=job_id):
|
|
75
|
-
logger.info("starting ingest")
|
|
75
|
+
logger.info("starting ingest") # now carries job_id too
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Bindings nest cleanly and restore on exit. structlog users can mount the
|
|
@@ -9,6 +9,7 @@ instead of the endpoint returning a 5xx.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
13
|
from datetime import UTC, datetime
|
|
13
14
|
from typing import TYPE_CHECKING, Any
|
|
14
15
|
|
|
@@ -23,6 +24,10 @@ if TYPE_CHECKING:
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
# The four independent inspect broadcasts, issued concurrently.
|
|
28
|
+
_PROBES = ("ping", "stats", "active_queues", "active")
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
class WorkerInspector:
|
|
27
32
|
"""Synchronous adapter; call from async code via ``asyncio.to_thread``."""
|
|
28
33
|
|
|
@@ -30,6 +35,11 @@ class WorkerInspector:
|
|
|
30
35
|
self.celery = celery
|
|
31
36
|
self.timeout = timeout
|
|
32
37
|
|
|
38
|
+
def _probe(self, name: str) -> dict:
|
|
39
|
+
"""Run one inspect broadcast on its own handle. ``None`` means nobody replied."""
|
|
40
|
+
inspect = self.celery.control.inspect(timeout=self.timeout)
|
|
41
|
+
return getattr(inspect, name)() or {}
|
|
42
|
+
|
|
33
43
|
def snapshot(self) -> WorkerSnapshot:
|
|
34
44
|
polled_at = datetime.now(UTC)
|
|
35
45
|
|
|
@@ -48,12 +58,24 @@ class WorkerInspector:
|
|
|
48
58
|
error=str(exc),
|
|
49
59
|
)
|
|
50
60
|
|
|
51
|
-
inspect
|
|
61
|
+
# Each inspect call broadcasts and then waits the *full* timeout for
|
|
62
|
+
# replies — it cannot know whether a slow worker is still coming. Run
|
|
63
|
+
# sequentially that is 4 x timeout, so with the broker up and no
|
|
64
|
+
# workers this page took ~4s: precisely the state an admin opens it to
|
|
65
|
+
# diagnose. The four probes are independent, so issue them together
|
|
66
|
+
# and the page costs one timeout instead of four.
|
|
67
|
+
#
|
|
68
|
+
# Each gets its own inspect handle rather than sharing one across
|
|
69
|
+
# threads. Results are still merged by hostname afterwards, so a
|
|
70
|
+
# worker that answers some probes but not others (a degraded worker)
|
|
71
|
+
# is still reported.
|
|
52
72
|
try:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
with ThreadPoolExecutor(max_workers=len(_PROBES)) as pool:
|
|
74
|
+
futures = {name: pool.submit(self._probe, name) for name in _PROBES}
|
|
75
|
+
ping = futures["ping"].result()
|
|
76
|
+
stats = futures["stats"].result()
|
|
77
|
+
queues = futures["active_queues"].result()
|
|
78
|
+
active = futures["active"].result()
|
|
57
79
|
except (OperationalError, RedisError, ConnectionError, OSError) as exc:
|
|
58
80
|
logger.info("inspect() failed mid-call: %s", exc)
|
|
59
81
|
return WorkerSnapshot(
|
{simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_background_tasks"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.25"
|
|
4
4
|
description = "Celery + Redis task queue with admin UI for monitoring and retrying failed/stuck tasks"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,10 +21,10 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
27
|
-
"simple_module_settings==0.0.
|
|
24
|
+
"simple_module_core==0.0.25",
|
|
25
|
+
"simple_module_db==0.0.25",
|
|
26
|
+
"simple_module_hosting==0.0.25",
|
|
27
|
+
"simple_module_settings==0.0.25",
|
|
28
28
|
"celery[redis]>=5.4",
|
|
29
29
|
"redis>=5",
|
|
30
30
|
]
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import time
|
|
5
6
|
from unittest.mock import MagicMock
|
|
6
7
|
|
|
7
8
|
from celery import Celery
|
|
8
9
|
|
|
10
|
+
PROBE_DELAY = 0.25
|
|
11
|
+
# Sequential would be 4x PROBE_DELAY; allow well under that but above one probe.
|
|
12
|
+
SEQUENTIAL_SLACK = 2.5
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
def _make_celery_with_dead_broker() -> Celery:
|
|
11
16
|
"""Build a Celery app pointed at an unreachable broker.
|
|
@@ -104,3 +109,81 @@ def test_redis_error_during_probe_returns_unreachable_snapshot():
|
|
|
104
109
|
assert snapshot.workers == []
|
|
105
110
|
assert snapshot.error is not None
|
|
106
111
|
assert "invalid password" in snapshot.error
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _celery_with_live_broker_no_workers() -> tuple[Celery, MagicMock]:
|
|
115
|
+
"""Broker reachable, but no worker ever answers — the common outage case."""
|
|
116
|
+
celery = MagicMock(spec=Celery)
|
|
117
|
+
celery.connection.return_value.ensure_connection.return_value = None
|
|
118
|
+
inspect = celery.control.inspect.return_value
|
|
119
|
+
# Celery returns None when nobody replies before the timeout.
|
|
120
|
+
inspect.ping.return_value = None
|
|
121
|
+
inspect.stats.return_value = None
|
|
122
|
+
inspect.active_queues.return_value = None
|
|
123
|
+
inspect.active.return_value = None
|
|
124
|
+
return celery, inspect
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_probes_run_concurrently_not_sequentially():
|
|
128
|
+
"""Four sequential probes cost 4 x timeout; concurrent ones cost about one.
|
|
129
|
+
|
|
130
|
+
With the broker up and no workers running, every inspect call waits its
|
|
131
|
+
full timeout for replies that never come. Sequentially that made
|
|
132
|
+
/admin/background-tasks/workers take ~4s — exactly the state an admin
|
|
133
|
+
opens that page to diagnose.
|
|
134
|
+
|
|
135
|
+
Each mocked probe sleeps for PROBE_DELAY. Sequential would be 4x that;
|
|
136
|
+
the assertion allows generous slack so this cannot flake on a loaded box
|
|
137
|
+
while still failing outright if the calls go back to being serial.
|
|
138
|
+
"""
|
|
139
|
+
from background_tasks.worker_inspector import WorkerInspector
|
|
140
|
+
|
|
141
|
+
celery, _ = _celery_with_live_broker_no_workers()
|
|
142
|
+
inspect = celery.control.inspect.return_value
|
|
143
|
+
for name in ("ping", "stats", "active_queues", "active"):
|
|
144
|
+
getattr(inspect, name).side_effect = lambda *_, **__: (time.sleep(PROBE_DELAY), None)[1]
|
|
145
|
+
|
|
146
|
+
start = time.perf_counter()
|
|
147
|
+
snapshot = WorkerInspector(celery, timeout=PROBE_DELAY).snapshot()
|
|
148
|
+
elapsed = time.perf_counter() - start
|
|
149
|
+
|
|
150
|
+
assert elapsed < PROBE_DELAY * SEQUENTIAL_SLACK, (
|
|
151
|
+
f"probes took {elapsed:.2f}s; {PROBE_DELAY * 4:.2f}s means they are still sequential"
|
|
152
|
+
)
|
|
153
|
+
assert snapshot.broker_reachable is True
|
|
154
|
+
assert snapshot.workers == []
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_all_four_probes_are_still_issued_when_nobody_replies():
|
|
158
|
+
"""Concurrency must not drop probes — a worker that answers only stats
|
|
159
|
+
(degraded, not answering ping) must still be reported. See
|
|
160
|
+
test_worker_in_stats_but_not_ping_is_offline."""
|
|
161
|
+
from background_tasks.worker_inspector import WorkerInspector
|
|
162
|
+
|
|
163
|
+
celery, inspect = _celery_with_live_broker_no_workers()
|
|
164
|
+
WorkerInspector(celery, timeout=0.05).snapshot()
|
|
165
|
+
|
|
166
|
+
inspect.ping.assert_called_once()
|
|
167
|
+
inspect.stats.assert_called_once()
|
|
168
|
+
inspect.active_queues.assert_called_once()
|
|
169
|
+
inspect.active.assert_called_once()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_workers_present_still_collects_every_detail():
|
|
173
|
+
"""The short-circuit must not skip the calls when workers do reply."""
|
|
174
|
+
from background_tasks.worker_inspector import WorkerInspector
|
|
175
|
+
|
|
176
|
+
celery = MagicMock(spec=Celery)
|
|
177
|
+
celery.connection.return_value.ensure_connection.return_value = None
|
|
178
|
+
inspect = celery.control.inspect.return_value
|
|
179
|
+
inspect.ping.return_value = {"celery@host-a": {"ok": "pong"}}
|
|
180
|
+
inspect.stats.return_value = {"celery@host-a": {"pool": {"max-concurrency": 2}}}
|
|
181
|
+
inspect.active_queues.return_value = {"celery@host-a": [{"name": "default"}]}
|
|
182
|
+
inspect.active.return_value = {"celery@host-a": []}
|
|
183
|
+
|
|
184
|
+
snapshot = WorkerInspector(celery, timeout=1.0).snapshot()
|
|
185
|
+
|
|
186
|
+
inspect.stats.assert_called_once()
|
|
187
|
+
inspect.active_queues.assert_called_once()
|
|
188
|
+
inspect.active.assert_called_once()
|
|
189
|
+
assert [w.hostname for w in snapshot.workers] == ["celery@host-a"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/package.json
RENAMED
|
File without changes
|
{simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tests/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_background_tasks-0.0.23 → simple_module_background_tasks-0.0.25}/tsconfig.json
RENAMED
|
File without changes
|