simple-module-background-tasks 0.0.35__tar.gz → 0.0.36__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.35 → simple_module_background_tasks-0.0.36}/PKG-INFO +5 -5
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/__init__.py +2 -0
- simple_module_background_tasks-0.0.36/background_tasks/_signal_bus.py +61 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/constants.py +4 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/contracts/schemas.py +2 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/endpoints/api_admin.py +12 -3
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/endpoints/views.py +13 -2
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/filters.py +14 -3
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/locales/en.json +9 -3
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/log_context.py +2 -2
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/models.py +13 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/module.py +3 -2
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/Index.tsx +27 -5
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/DetailFacts.tsx +4 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/ExecutionRow.tsx +3 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/ExecutionsTable.tsx +5 -2
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/TaskFilters.tsx +42 -1
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/constants.ts +4 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/retry.ts +5 -1
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/retry_service.py +16 -3
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/service.py +33 -5
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/signals.py +44 -58
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/sync_db.py +23 -7
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/tenant_context.py +31 -3
- simple_module_background_tasks-0.0.36/background_tasks/worker_settings.py +57 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/pyproject.toml +5 -5
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/conftest.py +2 -0
- simple_module_background_tasks-0.0.36/tests/test_background_tasks_tz_columns.py +56 -0
- simple_module_background_tasks-0.0.36/tests/test_bg_worker_settings.py +56 -0
- simple_module_background_tasks-0.0.36/tests/test_execution_tenancy.py +194 -0
- simple_module_background_tasks-0.0.36/tests/test_signal_tenant_stamp.py +99 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_signals.py +2 -2
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_worker_tenancy.py +50 -1
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/.gitignore +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/LICENSE +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/README.md +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/_signal_support.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/celery_app.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/contracts/__init__.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/contracts/events.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/deps.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/endpoints/__init__.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/health.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/invalidation.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/Detail.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/Workers.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/PayloadCards.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/RetryAllDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/RetryConfirmDialog.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/StatusPill.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/StatusStrip.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/TasksEmpty.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/TracebackCard.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/WorkerCard.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/WorkerEmptyStates.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/components/WorkerHealthBanner.tsx +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/pages/worker-health.ts +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/py.typed +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/services.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/settings.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/tasks.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/worker_inspector.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/background_tasks/workers_state.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/package.json +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_admin_api.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_background_tasks_settings.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_beat_schedule.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_index_worker_presence.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_invalidation.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_invalidation_degraded.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_invalidation_redis.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_list_item_payload.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_page_clamp.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_redis_health.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_redis_url.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_service.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_bg_settings_env.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_detail_props.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_index_filters.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_log_context.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_retry_failed_bulk.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_retry_failed_guards.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_retry_sweep_ordering.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_sweep_stuck.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_sync_db.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_tenant_context.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_worker_inspector.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_workers_endpoints.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests/test_workers_uptime.py +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tests-js/task-formatters.test.ts +0 -0
- {simple_module_background_tasks-0.0.35 → simple_module_background_tasks-0.0.36}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_background_tasks
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.36
|
|
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.36
|
|
27
|
+
Requires-Dist: simple-module-db==0.0.36
|
|
28
|
+
Requires-Dist: simple-module-hosting==0.0.36
|
|
29
|
+
Requires-Dist: simple-module-settings==0.0.36
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
# simple_module_background_tasks
|
|
@@ -5,9 +5,11 @@ from background_tasks.log_context import (
|
|
|
5
5
|
get_log_context,
|
|
6
6
|
install_log_filter,
|
|
7
7
|
)
|
|
8
|
+
from background_tasks.worker_settings import settings_for
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"bind_task_context",
|
|
11
12
|
"get_log_context",
|
|
12
13
|
"install_log_filter",
|
|
14
|
+
"settings_for",
|
|
13
15
|
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Bridge from Celery's sync signal thread to the app's async event bus.
|
|
2
|
+
|
|
3
|
+
Split out of :mod:`.signals` to keep it under the 300-line cap; ``signals``
|
|
4
|
+
re-exports :func:`bind_event_bus` / :func:`unbind_event_bus`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from simple_module_core.events import EventBus
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("background_tasks.signals")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_bus: EventBus | None = None
|
|
19
|
+
_loop: asyncio.AbstractEventLoop | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def bind_event_bus(bus: EventBus, loop: asyncio.AbstractEventLoop) -> None:
|
|
23
|
+
"""Bind an event bus + its running loop so signals can publish events.
|
|
24
|
+
|
|
25
|
+
Signals fire on the Celery sync thread; `run_coroutine_threadsafe`
|
|
26
|
+
bridges back to ``loop`` so handlers run on the API event loop
|
|
27
|
+
regardless of which thread triggered the signal.
|
|
28
|
+
"""
|
|
29
|
+
global _bus, _loop
|
|
30
|
+
_bus = bus
|
|
31
|
+
_loop = loop
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unbind_event_bus() -> None:
|
|
35
|
+
"""Drop the bound bus — called from ``on_shutdown`` so tests stay isolated."""
|
|
36
|
+
global _bus, _loop
|
|
37
|
+
_bus = None
|
|
38
|
+
_loop = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def publish_from_signal(event: Any) -> None:
|
|
42
|
+
"""Dispatch ``event`` onto the bound bus without blocking the signal thread."""
|
|
43
|
+
if _bus is None or _loop is None:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
future = asyncio.run_coroutine_threadsafe(_bus.publish(event), _loop)
|
|
47
|
+
except RuntimeError:
|
|
48
|
+
# Loop has stopped (shutdown race). The DB row is already written.
|
|
49
|
+
logger.debug("Event bus loop is not running; skipping %s", type(event).__name__)
|
|
50
|
+
return
|
|
51
|
+
# Surface subscriber exceptions — run_coroutine_threadsafe otherwise only
|
|
52
|
+
# logs them when the Future is GC'd, which happens far from the failure.
|
|
53
|
+
future.add_done_callback(_log_publish_failure)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _log_publish_failure(future: asyncio.Future[Any]) -> None:
|
|
57
|
+
if future.cancelled():
|
|
58
|
+
return
|
|
59
|
+
exc = future.exception()
|
|
60
|
+
if exc is not None:
|
|
61
|
+
logger.error("Event publish raised: %s", exc, exc_info=exc)
|
|
@@ -102,3 +102,7 @@ INTERNAL_TASK_PURGE_OLD = "background_tasks.purge_old_executions"
|
|
|
102
102
|
# Harmless round-trip task; only wired into CI smoke tests and local dev —
|
|
103
103
|
# not scheduled, not invoked by other modules.
|
|
104
104
|
DEMO_ECHO_TASK = "background_tasks.demo_echo"
|
|
105
|
+
|
|
106
|
+
TENANT_ID_MAX_LENGTH = 50
|
|
107
|
+
# Filter value meaning "executions with no tenant" (beat / platform publishes).
|
|
108
|
+
PLATFORM_TENANT_FILTER = "__platform__"
|
|
@@ -28,6 +28,7 @@ class TaskExecutionListItem(SQLModel):
|
|
|
28
28
|
task_name: str
|
|
29
29
|
status: TaskStatus
|
|
30
30
|
queue: str
|
|
31
|
+
tenant_id: str | None = None
|
|
31
32
|
args: list[Any] = []
|
|
32
33
|
kwargs: dict[str, Any] = {}
|
|
33
34
|
retries: int
|
|
@@ -49,6 +50,7 @@ class TaskExecutionDetail(SQLModel):
|
|
|
49
50
|
task_name: str
|
|
50
51
|
status: TaskStatus
|
|
51
52
|
queue: str
|
|
53
|
+
tenant_id: str | None = None
|
|
52
54
|
args: list[Any] = []
|
|
53
55
|
kwargs: dict[str, Any] = {}
|
|
54
56
|
result: dict[str, Any] | None = None
|
|
@@ -37,6 +37,7 @@ async def list_executions(
|
|
|
37
37
|
q: str | None = Query(default=None),
|
|
38
38
|
task_name: str | None = Query(default=None, deprecated=True),
|
|
39
39
|
queue: str | None = Query(default=None),
|
|
40
|
+
tenant_id: str | None = Query(default=None),
|
|
40
41
|
page: int = Query(default=1, ge=1),
|
|
41
42
|
per_page: int = Query(default=20, ge=1, le=200),
|
|
42
43
|
service: BackgroundTaskService = Depends(get_background_task_service),
|
|
@@ -50,7 +51,12 @@ async def list_executions(
|
|
|
50
51
|
existing API callers keep working, and ``q`` wins when both are given.
|
|
51
52
|
"""
|
|
52
53
|
return await service.list(
|
|
53
|
-
status=status,
|
|
54
|
+
status=status,
|
|
55
|
+
task_name=q or task_name,
|
|
56
|
+
queue=queue,
|
|
57
|
+
tenant_id=tenant_id,
|
|
58
|
+
page=page,
|
|
59
|
+
per_page=per_page,
|
|
54
60
|
)
|
|
55
61
|
|
|
56
62
|
|
|
@@ -74,15 +80,18 @@ async def retry_failed_executions(
|
|
|
74
80
|
status: TaskStatus | None = Query(default=None),
|
|
75
81
|
task_name: str | None = Query(default=None, alias="q"),
|
|
76
82
|
queue: str | None = Query(default=None),
|
|
83
|
+
tenant_id: str | None = Query(default=None),
|
|
77
84
|
service: BackgroundTaskService = Depends(get_background_task_service),
|
|
78
85
|
) -> RetryFailedResult:
|
|
79
86
|
"""Re-enqueue every failed or stuck execution the current filter can see.
|
|
80
87
|
|
|
81
|
-
Takes the same
|
|
88
|
+
Takes the same filters as the listing — status, search, queue, tenant — so
|
|
82
89
|
the sweep covers exactly the rows the operator is looking at and nothing
|
|
83
90
|
else. Capped per call; the response says how many eligible rows are left.
|
|
84
91
|
"""
|
|
85
|
-
return await service.retry_failed(
|
|
92
|
+
return await service.retry_failed(
|
|
93
|
+
status=status, task_name=task_name, queue=queue, tenant_id=tenant_id
|
|
94
|
+
)
|
|
86
95
|
|
|
87
96
|
|
|
88
97
|
@router.post(
|
|
@@ -12,6 +12,7 @@ from simple_module_inertia import InertiaResponse
|
|
|
12
12
|
|
|
13
13
|
from background_tasks.constants import (
|
|
14
14
|
PERM_VIEW,
|
|
15
|
+
PLATFORM_TENANT_FILTER,
|
|
15
16
|
TaskStatus,
|
|
16
17
|
)
|
|
17
18
|
from background_tasks.deps import get_background_task_service
|
|
@@ -53,6 +54,7 @@ async def index(
|
|
|
53
54
|
status: TaskStatus | None = Query(default=None),
|
|
54
55
|
task_name: str = Query(default="", alias="q"),
|
|
55
56
|
queue: str = Query(default=""),
|
|
57
|
+
tenant_id: str = Query(default="", alias="tenant"),
|
|
56
58
|
page: int = Query(default=1),
|
|
57
59
|
service: BackgroundTaskService = Depends(get_background_task_service),
|
|
58
60
|
) -> InertiaResponse:
|
|
@@ -60,22 +62,28 @@ async def index(
|
|
|
60
62
|
status=status,
|
|
61
63
|
task_name=task_name or None,
|
|
62
64
|
queue=queue or None,
|
|
65
|
+
tenant_id=tenant_id or None,
|
|
63
66
|
page=page,
|
|
64
67
|
per_page=PER_PAGE,
|
|
65
68
|
)
|
|
66
69
|
# A filtered-empty list says nothing about the fleet, so don't pay to poll
|
|
67
70
|
# it: the screen already knows to blame the filter.
|
|
68
|
-
unfiltered_and_empty =
|
|
71
|
+
unfiltered_and_empty = (
|
|
72
|
+
response.total == 0 and not task_name and not queue and not tenant_id and status is None
|
|
73
|
+
)
|
|
69
74
|
|
|
70
75
|
async def strip_data() -> tuple[dict[str, int], list[str]]:
|
|
71
76
|
"""Everything above the table. Serial: one session, one connection."""
|
|
72
|
-
counts = await service.status_counts(
|
|
77
|
+
counts = await service.status_counts(
|
|
78
|
+
task_name=task_name or None, queue=queue or None, tenant_id=tenant_id or None
|
|
79
|
+
)
|
|
73
80
|
# The strip's third tile is a windowed throughput reading rather than a
|
|
74
81
|
# status total, so it needs its own count — see `success_count_since`.
|
|
75
82
|
counts["success_24h"] = await service.success_count_since(
|
|
76
83
|
hours=SUCCESS_WINDOW_HOURS,
|
|
77
84
|
task_name=task_name or None,
|
|
78
85
|
queue=queue or None,
|
|
86
|
+
tenant_id=tenant_id or None,
|
|
79
87
|
)
|
|
80
88
|
# Deliberately unfiltered: the dropdown is how an operator leaves the
|
|
81
89
|
# queue they are in, so it keeps offering the ones they are not in.
|
|
@@ -97,6 +105,8 @@ async def index(
|
|
|
97
105
|
"success_24h": counts["success_24h"],
|
|
98
106
|
},
|
|
99
107
|
"queues": queues,
|
|
108
|
+
"tenant_ids": await service.tenant_ids(),
|
|
109
|
+
"platform_tenant_value": PLATFORM_TENANT_FILTER,
|
|
100
110
|
"worker_presence": presence,
|
|
101
111
|
"pagination": {
|
|
102
112
|
"page": response.page,
|
|
@@ -107,6 +117,7 @@ async def index(
|
|
|
107
117
|
"status": status.value if status else "",
|
|
108
118
|
"task_name": task_name,
|
|
109
119
|
"queue": queue,
|
|
120
|
+
"tenant": tenant_id,
|
|
110
121
|
},
|
|
111
122
|
},
|
|
112
123
|
)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from simple_module_db import LIKE_ESCAPE_CHAR, like_contains_pattern
|
|
12
12
|
from sqlalchemy import ColumnElement, select
|
|
13
13
|
|
|
14
|
-
from background_tasks.constants import RETRYABLE_STATUSES, TaskStatus
|
|
14
|
+
from background_tasks.constants import PLATFORM_TENANT_FILTER, RETRYABLE_STATUSES, TaskStatus
|
|
15
15
|
from background_tasks.models import TaskExecution
|
|
16
16
|
|
|
17
17
|
Conditions = list[ColumnElement[bool]]
|
|
@@ -22,9 +22,19 @@ def execution_filters(
|
|
|
22
22
|
status: TaskStatus | None = None,
|
|
23
23
|
task_name: str | None = None,
|
|
24
24
|
queue: str | None = None,
|
|
25
|
+
tenant_id: str | None = None,
|
|
25
26
|
) -> Conditions:
|
|
26
|
-
"""The
|
|
27
|
+
"""The axes the executions screen filters on.
|
|
28
|
+
|
|
29
|
+
*tenant_id* is a tenant id, or :data:`PLATFORM_TENANT_FILTER` for
|
|
30
|
+
executions published with no tenant. The screen is platform-wide; tenant is
|
|
31
|
+
a filter, not a scope.
|
|
32
|
+
"""
|
|
27
33
|
conditions: Conditions = []
|
|
34
|
+
if tenant_id == PLATFORM_TENANT_FILTER:
|
|
35
|
+
conditions.append(TaskExecution.tenant_id.is_(None))
|
|
36
|
+
elif tenant_id:
|
|
37
|
+
conditions.append(TaskExecution.tenant_id == tenant_id)
|
|
28
38
|
if status is not None:
|
|
29
39
|
conditions.append(TaskExecution.status == status)
|
|
30
40
|
if task_name:
|
|
@@ -41,6 +51,7 @@ def bulk_retry_conditions(
|
|
|
41
51
|
status: TaskStatus | None = None,
|
|
42
52
|
task_name: str | None = None,
|
|
43
53
|
queue: str | None = None,
|
|
54
|
+
tenant_id: str | None = None,
|
|
44
55
|
) -> Conditions | None:
|
|
45
56
|
"""What the bulk sweep may touch, or ``None`` when it may touch nothing.
|
|
46
57
|
|
|
@@ -64,6 +75,6 @@ def bulk_retry_conditions(
|
|
|
64
75
|
already_retried = select(child.c.id).where(child.c.retried_from_id == TaskExecution.id).exists()
|
|
65
76
|
return [
|
|
66
77
|
TaskExecution.status.in_(sorted(wanted)),
|
|
67
|
-
*execution_filters(task_name=task_name, queue=queue),
|
|
78
|
+
*execution_filters(task_name=task_name, queue=queue, tenant_id=tenant_id),
|
|
68
79
|
~already_retried,
|
|
69
80
|
]
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"stuck": "Stuck",
|
|
27
27
|
"queue_label": "Queue:",
|
|
28
28
|
"queue_all": "all",
|
|
29
|
-
"queue_aria": "Filter by queue"
|
|
29
|
+
"queue_aria": "Filter by queue",
|
|
30
|
+
"tenant_label": "Tenant:",
|
|
31
|
+
"tenant_all": "all",
|
|
32
|
+
"tenant_platform": "Platform",
|
|
33
|
+
"tenant_aria": "Filter by tenant"
|
|
30
34
|
},
|
|
31
35
|
"status": {
|
|
32
36
|
"queued": "queued",
|
|
@@ -44,7 +48,8 @@
|
|
|
44
48
|
"queued_at": "Queued",
|
|
45
49
|
"duration": "Duration",
|
|
46
50
|
"actions": "Actions",
|
|
47
|
-
"retry": "Retry"
|
|
51
|
+
"retry": "Retry",
|
|
52
|
+
"tenant": "Tenant"
|
|
48
53
|
},
|
|
49
54
|
"detail": {
|
|
50
55
|
"head_title": "Task detail",
|
|
@@ -69,7 +74,8 @@
|
|
|
69
74
|
"traceback": "Traceback",
|
|
70
75
|
"copy": "Copy",
|
|
71
76
|
"copied": "Copied",
|
|
72
|
-
"no_traceback": "No traceback recorded."
|
|
77
|
+
"no_traceback": "No traceback recorded.",
|
|
78
|
+
"tenant": "Tenant"
|
|
73
79
|
},
|
|
74
80
|
"retry_dialog": {
|
|
75
81
|
"title": "Retry {name}?",
|
|
@@ -10,7 +10,7 @@ Celery signals plus arbitrary domain identifiers via
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
-
from collections.abc import
|
|
13
|
+
from collections.abc import Generator, Mapping
|
|
14
14
|
from contextlib import contextmanager
|
|
15
15
|
from contextvars import ContextVar, Token
|
|
16
16
|
from types import MappingProxyType
|
|
@@ -46,7 +46,7 @@ def get_log_context() -> dict[str, Any]:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@contextmanager
|
|
49
|
-
def bind_task_context(**identifiers: Any) ->
|
|
49
|
+
def bind_task_context(**identifiers: Any) -> Generator[None]:
|
|
50
50
|
"""Layer ``identifiers`` onto the current task's log context.
|
|
51
51
|
|
|
52
52
|
Nests cleanly. Raises ``ValueError`` if any key collides with a
|
|
@@ -15,6 +15,7 @@ from background_tasks.constants import (
|
|
|
15
15
|
DEFAULT_QUEUE,
|
|
16
16
|
MODULE_NAME,
|
|
17
17
|
TABLE_TASK_EXECUTION,
|
|
18
|
+
TENANT_ID_MAX_LENGTH,
|
|
18
19
|
TaskStatus,
|
|
19
20
|
)
|
|
20
21
|
|
|
@@ -48,6 +49,12 @@ class TaskExecution(Base, AuditMixin, table=True): # ty: ignore[unsupported-bas
|
|
|
48
49
|
)
|
|
49
50
|
queue: str = Field(default=DEFAULT_QUEUE, max_length=64)
|
|
50
51
|
|
|
52
|
+
# The tenant the task was published for; NULL for beat and other platform
|
|
53
|
+
# publishes. Deliberately not MultiTenantMixin: those rows have no tenant
|
|
54
|
+
# and strict mode would raise on their insert. The admin screens are
|
|
55
|
+
# platform-wide and filter on this column.
|
|
56
|
+
tenant_id: str | None = Field(default=None, index=True, max_length=TENANT_ID_MAX_LENGTH)
|
|
57
|
+
|
|
51
58
|
args: list[Any] = Field(default_factory=list, sa_column=Column(JSON))
|
|
52
59
|
kwargs: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
|
|
53
60
|
result: dict[str, Any] | None = Field(default=None, sa_column=Column(JSON))
|
|
@@ -79,4 +86,10 @@ class TaskExecution(Base, AuditMixin, table=True): # ty: ignore[unsupported-bas
|
|
|
79
86
|
"status",
|
|
80
87
|
"queued_at",
|
|
81
88
|
),
|
|
89
|
+
Index(
|
|
90
|
+
"ix_background_tasks_task_execution_tenant_status_queued",
|
|
91
|
+
"tenant_id",
|
|
92
|
+
"status",
|
|
93
|
+
"queued_at",
|
|
94
|
+
),
|
|
82
95
|
)
|
|
@@ -161,8 +161,9 @@ class BackgroundTasksModule(ModuleBase):
|
|
|
161
161
|
# SQLite default and silently drop ``TaskExecution`` rows.
|
|
162
162
|
host = app.state.sm.settings
|
|
163
163
|
multi = bool(getattr(host, "multi_tenant", False))
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
default_tenant = None if multi else getattr(host, "default_tenant", "") or None
|
|
165
|
+
set_database_url(host.database_url, tenant_strict=multi, default_tenant=default_tenant)
|
|
166
|
+
set_default_tenant(default_tenant)
|
|
166
167
|
# build_celery imports `signals` for side effects and runs
|
|
167
168
|
# `autodiscover_tasks` across every installed module.
|
|
168
169
|
services.celery = build_celery(services.settings)
|
|
@@ -12,7 +12,7 @@ import { type StatusCounts, StatusStrip } from './components/StatusStrip';
|
|
|
12
12
|
import { TaskFilters } from './components/TaskFilters';
|
|
13
13
|
import { TasksEmptyRow, type WorkerPresence } from './components/TasksEmpty';
|
|
14
14
|
import { WorkerHealthBanner } from './components/WorkerHealthBanner';
|
|
15
|
-
import { QUEUE_ALL, STATUS_ALL, TASK_STATUS, VIEW_BASE } from './constants';
|
|
15
|
+
import { QUEUE_ALL, STATUS_ALL, TASK_STATUS, TENANT_ALL, VIEW_BASE } from './constants';
|
|
16
16
|
import { type Execution, retryAllFailed, retryExecution } from './retry';
|
|
17
17
|
|
|
18
18
|
interface Pagination {
|
|
@@ -25,6 +25,7 @@ interface Filters {
|
|
|
25
25
|
status: string;
|
|
26
26
|
task_name: string;
|
|
27
27
|
queue: string;
|
|
28
|
+
tenant: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
interface Props {
|
|
@@ -34,6 +35,10 @@ interface Props {
|
|
|
34
35
|
status_counts: StatusCounts;
|
|
35
36
|
/** Every queue that has run work, for the dropdown. */
|
|
36
37
|
queues: string[];
|
|
38
|
+
/** Tenants that have published work, for the dropdown. */
|
|
39
|
+
tenant_ids: string[];
|
|
40
|
+
/** Filter value selecting executions with no tenant. */
|
|
41
|
+
platform_tenant_value: string;
|
|
37
42
|
/** Null unless the unfiltered list came back empty — see the index view. */
|
|
38
43
|
worker_presence: WorkerPresence | null;
|
|
39
44
|
}
|
|
@@ -47,6 +52,7 @@ function pushFilters(filters: Filters, page: number): void {
|
|
|
47
52
|
if (filters.task_name) params.q = filters.task_name;
|
|
48
53
|
if (filters.status && filters.status !== STATUS_ALL) params.status = filters.status;
|
|
49
54
|
if (filters.queue && filters.queue !== QUEUE_ALL) params.queue = filters.queue;
|
|
55
|
+
if (filters.tenant && filters.tenant !== TENANT_ALL) params.tenant = filters.tenant;
|
|
50
56
|
if (page > 1) params.page = String(page);
|
|
51
57
|
router.get(VIEW_BASE, params, { preserveState: true, preserveScroll: true });
|
|
52
58
|
}
|
|
@@ -58,6 +64,8 @@ function Index() {
|
|
|
58
64
|
filters: initialFilters,
|
|
59
65
|
status_counts: statusCounts,
|
|
60
66
|
queues,
|
|
67
|
+
tenant_ids: tenantIds,
|
|
68
|
+
platform_tenant_value: platformTenantValue,
|
|
61
69
|
worker_presence: workerPresence,
|
|
62
70
|
} = usePage<{ props: Props }>().props as unknown as Props;
|
|
63
71
|
|
|
@@ -71,16 +79,21 @@ function Index() {
|
|
|
71
79
|
const [busy, setBusy] = useState(false);
|
|
72
80
|
const statusValue = initialFilters.status || STATUS_ALL;
|
|
73
81
|
const queueValue = initialFilters.queue || QUEUE_ALL;
|
|
82
|
+
const tenantValue = initialFilters.tenant || TENANT_ALL;
|
|
74
83
|
// Derived from the server-confirmed filters, not the live `search` input:
|
|
75
84
|
// `workerPresence` reflects the last committed request, so mixing it with
|
|
76
85
|
// unsubmitted local state would flash the wrong empty-state copy during the
|
|
77
86
|
// debounce window between a keystroke and the resulting navigation.
|
|
78
87
|
const isFiltered =
|
|
79
|
-
!!(initialFilters.task_name ?? '') ||
|
|
88
|
+
!!(initialFilters.task_name ?? '') ||
|
|
89
|
+
statusValue !== STATUS_ALL ||
|
|
90
|
+
queueValue !== QUEUE_ALL ||
|
|
91
|
+
tenantValue !== TENANT_ALL;
|
|
80
92
|
const filters: Filters = {
|
|
81
93
|
status: statusValue,
|
|
82
94
|
task_name: search,
|
|
83
95
|
queue: queueValue,
|
|
96
|
+
tenant: tenantValue,
|
|
84
97
|
};
|
|
85
98
|
|
|
86
99
|
// Work that is supposed to be moving. Counting `running` too is deliberate:
|
|
@@ -101,7 +114,7 @@ function Index() {
|
|
|
101
114
|
// clear, and an armed flag would swallow the user's next keystroke.
|
|
102
115
|
if (search !== '') skipNextDebounceRef.current = true;
|
|
103
116
|
setSearch('');
|
|
104
|
-
pushFilters({ status: STATUS_ALL, task_name: '', queue: QUEUE_ALL }, 1);
|
|
117
|
+
pushFilters({ status: STATUS_ALL, task_name: '', queue: QUEUE_ALL, tenant: TENANT_ALL }, 1);
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
// Debounce search: any change from the server-provided value kicks off a
|
|
@@ -113,11 +126,15 @@ function Index() {
|
|
|
113
126
|
}
|
|
114
127
|
if (search === (initialFilters.task_name ?? '')) return;
|
|
115
128
|
const timeout = setTimeout(
|
|
116
|
-
() =>
|
|
129
|
+
() =>
|
|
130
|
+
pushFilters(
|
|
131
|
+
{ status: statusValue, task_name: search, queue: queueValue, tenant: tenantValue },
|
|
132
|
+
1,
|
|
133
|
+
),
|
|
117
134
|
300,
|
|
118
135
|
);
|
|
119
136
|
return () => clearTimeout(timeout);
|
|
120
|
-
}, [search, initialFilters.task_name, statusValue, queueValue]);
|
|
137
|
+
}, [search, initialFilters.task_name, statusValue, queueValue, tenantValue]);
|
|
121
138
|
|
|
122
139
|
async function handleRetry() {
|
|
123
140
|
if (!retryTarget) return;
|
|
@@ -134,6 +151,7 @@ function Index() {
|
|
|
134
151
|
status: statusValue,
|
|
135
152
|
taskName: initialFilters.task_name ?? '',
|
|
136
153
|
queue: queueValue,
|
|
154
|
+
tenant: tenantValue,
|
|
137
155
|
});
|
|
138
156
|
setBusy(false);
|
|
139
157
|
setRetryAllOpen(false);
|
|
@@ -188,6 +206,10 @@ function Index() {
|
|
|
188
206
|
queue={queueValue}
|
|
189
207
|
onQueueChange={(queue) => pushFilters({ ...filters, queue }, 1)}
|
|
190
208
|
queues={queues ?? []}
|
|
209
|
+
tenant={tenantValue}
|
|
210
|
+
onTenantChange={(tenant) => pushFilters({ ...filters, tenant }, 1)}
|
|
211
|
+
tenantIds={tenantIds ?? []}
|
|
212
|
+
platformTenantValue={platformTenantValue}
|
|
191
213
|
/>
|
|
192
214
|
|
|
193
215
|
<ExecutionsTable
|
|
@@ -45,6 +45,10 @@ export function DetailFacts({ execution, className }: Props) {
|
|
|
45
45
|
|
|
46
46
|
const facts: Fact[] = [
|
|
47
47
|
{ label: t(keys.background_tasks.detail.queue), value: execution.queue },
|
|
48
|
+
{
|
|
49
|
+
label: t(keys.background_tasks.detail.tenant),
|
|
50
|
+
value: execution.tenant_id ?? t(keys.background_tasks.filters.tenant_platform),
|
|
51
|
+
},
|
|
48
52
|
{
|
|
49
53
|
label: t(keys.background_tasks.detail.worker),
|
|
50
54
|
value: execution.worker ? (
|
|
@@ -43,6 +43,9 @@ export function ExecutionRow({ execution, canRetry, onRetry }: Props) {
|
|
|
43
43
|
<TableCell className="hidden text-sm text-muted-foreground md:table-cell">
|
|
44
44
|
{execution.queue}
|
|
45
45
|
</TableCell>
|
|
46
|
+
<TableCell className="hidden font-mono text-xs text-muted-foreground md:table-cell">
|
|
47
|
+
{execution.tenant_id ?? t(keys.background_tasks.filters.tenant_platform)}
|
|
48
|
+
</TableCell>
|
|
46
49
|
<TableCell className="hidden text-sm text-muted-foreground lg:table-cell">
|
|
47
50
|
{ago(execution.queued_at)}
|
|
48
51
|
</TableCell>
|
|
@@ -12,8 +12,8 @@ import type { ReactNode } from 'react';
|
|
|
12
12
|
import type { Execution } from '../retry';
|
|
13
13
|
import { ExecutionRow } from './ExecutionRow';
|
|
14
14
|
|
|
15
|
-
/** Task, Status, Queue, Queued, Duration, Actions. */
|
|
16
|
-
export const COLUMN_COUNT =
|
|
15
|
+
/** Task, Status, Queue, Tenant, Queued, Duration, Actions. */
|
|
16
|
+
export const COLUMN_COUNT = 7;
|
|
17
17
|
|
|
18
18
|
// Same header treatment as the other admin tables (users, audit log, flags).
|
|
19
19
|
const TH = 'text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground';
|
|
@@ -63,6 +63,9 @@ export function ExecutionsTable({
|
|
|
63
63
|
<TableHead className={`${TH} hidden md:table-cell`}>
|
|
64
64
|
{t(keys.background_tasks.table.queue)}
|
|
65
65
|
</TableHead>
|
|
66
|
+
<TableHead className={`${TH} hidden md:table-cell`}>
|
|
67
|
+
{t(keys.background_tasks.table.tenant)}
|
|
68
|
+
</TableHead>
|
|
66
69
|
<TableHead className={`${TH} hidden lg:table-cell`}>
|
|
67
70
|
{t(keys.background_tasks.table.queued_at)}
|
|
68
71
|
</TableHead>
|
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
SelectValue,
|
|
10
10
|
} from '@simple-module-py/ui/components/ui/select';
|
|
11
11
|
import { Search } from 'lucide-react';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
QUEUE_ALL,
|
|
14
|
+
SEGMENT_LABEL_KEY,
|
|
15
|
+
SEGMENT_STATUSES,
|
|
16
|
+
STATUS_ALL,
|
|
17
|
+
TENANT_ALL,
|
|
18
|
+
} from '../constants';
|
|
13
19
|
|
|
14
20
|
interface Props {
|
|
15
21
|
search: string;
|
|
@@ -22,6 +28,13 @@ interface Props {
|
|
|
22
28
|
onQueueChange: (next: string) => void;
|
|
23
29
|
/** Every queue that has run work, for the dropdown. */
|
|
24
30
|
queues: string[];
|
|
31
|
+
/** Active tenant filter, or `TENANT_ALL`. */
|
|
32
|
+
tenant: string;
|
|
33
|
+
onTenantChange: (next: string) => void;
|
|
34
|
+
/** Tenants that have published work, for the dropdown. */
|
|
35
|
+
tenantIds: string[];
|
|
36
|
+
/** Filter value selecting executions with no tenant (platform publishes). */
|
|
37
|
+
platformTenantValue: string;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
/**
|
|
@@ -40,6 +53,10 @@ export function TaskFilters({
|
|
|
40
53
|
queue,
|
|
41
54
|
onQueueChange,
|
|
42
55
|
queues,
|
|
56
|
+
tenant,
|
|
57
|
+
onTenantChange,
|
|
58
|
+
tenantIds,
|
|
59
|
+
platformTenantValue,
|
|
43
60
|
}: Props) {
|
|
44
61
|
const { t } = useT();
|
|
45
62
|
const options = SEGMENT_STATUSES.map((value) => ({
|
|
@@ -87,6 +104,30 @@ export function TaskFilters({
|
|
|
87
104
|
))}
|
|
88
105
|
</SelectContent>
|
|
89
106
|
</Select>
|
|
107
|
+
<Select value={tenant || TENANT_ALL} onValueChange={onTenantChange}>
|
|
108
|
+
<SelectTrigger
|
|
109
|
+
aria-label={t(keys.background_tasks.filters.tenant_aria)}
|
|
110
|
+
className="gap-1.5 max-lg:min-h-11"
|
|
111
|
+
>
|
|
112
|
+
<span className="text-muted-foreground">
|
|
113
|
+
{t(keys.background_tasks.filters.tenant_label)}
|
|
114
|
+
</span>
|
|
115
|
+
<SelectValue />
|
|
116
|
+
</SelectTrigger>
|
|
117
|
+
<SelectContent>
|
|
118
|
+
<SelectItem value={TENANT_ALL}>
|
|
119
|
+
{t(keys.background_tasks.filters.tenant_all)}
|
|
120
|
+
</SelectItem>
|
|
121
|
+
<SelectItem value={platformTenantValue}>
|
|
122
|
+
{t(keys.background_tasks.filters.tenant_platform)}
|
|
123
|
+
</SelectItem>
|
|
124
|
+
{tenantIds.map((id) => (
|
|
125
|
+
<SelectItem key={id} value={id} className="font-mono">
|
|
126
|
+
{id}
|
|
127
|
+
</SelectItem>
|
|
128
|
+
))}
|
|
129
|
+
</SelectContent>
|
|
130
|
+
</Select>
|
|
90
131
|
</div>
|
|
91
132
|
</div>
|
|
92
133
|
);
|
|
@@ -95,6 +95,7 @@ export interface TaskDetail {
|
|
|
95
95
|
task_name: string;
|
|
96
96
|
status: TaskStatus;
|
|
97
97
|
queue: string;
|
|
98
|
+
tenant_id: string | null;
|
|
98
99
|
args: unknown[];
|
|
99
100
|
kwargs: Record<string, unknown>;
|
|
100
101
|
result: Record<string, unknown> | null;
|
|
@@ -192,6 +193,9 @@ export const STATUS_ALL = 'all';
|
|
|
192
193
|
*/
|
|
193
194
|
export const QUEUE_ALL = '__all__';
|
|
194
195
|
|
|
196
|
+
/** Sentinel for "no tenant filter" in the tenant dropdown. */
|
|
197
|
+
export const TENANT_ALL = '__all__';
|
|
198
|
+
|
|
195
199
|
/**
|
|
196
200
|
* The four the segmented control offers: everything, plus the three states an
|
|
197
201
|
* operator triages. `pending`, `success`, `revoked` and `retrying` stay
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { keys, t } from '@simple-module-py/i18n';
|
|
6
6
|
import { toast } from 'sonner';
|
|
7
|
-
import { API_BASE, QUEUE_ALL, STATUS_ALL, type TaskStatus } from './constants';
|
|
7
|
+
import { API_BASE, QUEUE_ALL, STATUS_ALL, type TaskStatus, TENANT_ALL } from './constants';
|
|
8
8
|
|
|
9
9
|
export interface Execution {
|
|
10
10
|
id: string;
|
|
@@ -12,6 +12,8 @@ export interface Execution {
|
|
|
12
12
|
task_name: string;
|
|
13
13
|
status: TaskStatus;
|
|
14
14
|
queue: string;
|
|
15
|
+
/** Tenant that published the task; null for beat / platform publishes. */
|
|
16
|
+
tenant_id: string | null;
|
|
15
17
|
// Shown by the retry confirm before it re-enqueues them — see the list schema.
|
|
16
18
|
args: unknown[];
|
|
17
19
|
kwargs: Record<string, unknown>;
|
|
@@ -63,11 +65,13 @@ export async function retryAllFailed(filters: {
|
|
|
63
65
|
status: string;
|
|
64
66
|
taskName: string;
|
|
65
67
|
queue: string;
|
|
68
|
+
tenant: string;
|
|
66
69
|
}): Promise<number | null> {
|
|
67
70
|
const params = new URLSearchParams();
|
|
68
71
|
if (filters.status && filters.status !== STATUS_ALL) params.set('status', filters.status);
|
|
69
72
|
if (filters.taskName) params.set('q', filters.taskName);
|
|
70
73
|
if (filters.queue && filters.queue !== QUEUE_ALL) params.set('queue', filters.queue);
|
|
74
|
+
if (filters.tenant && filters.tenant !== TENANT_ALL) params.set('tenant_id', filters.tenant);
|
|
71
75
|
const query = params.toString();
|
|
72
76
|
try {
|
|
73
77
|
const res = await fetch(`${API_BASE}/executions/retry-failed${query ? `?${query}` : ''}`, {
|