pgwerk 0.1.0__py3-none-any.whl
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.
- pgwerk/__init__.py +72 -0
- pgwerk/api/__init__.py +4 -0
- pgwerk/api/app.py +148 -0
- pgwerk/api/models.py +458 -0
- pgwerk/api/routes.py +543 -0
- pgwerk/app.py +1071 -0
- pgwerk/cli/__init__.py +35 -0
- pgwerk/cli/api.py +77 -0
- pgwerk/cli/cron.py +39 -0
- pgwerk/cli/dashboard.py +160 -0
- pgwerk/cli/info.py +58 -0
- pgwerk/cli/jobs.py +116 -0
- pgwerk/cli/purge.py +41 -0
- pgwerk/cli/slowest.py +103 -0
- pgwerk/cli/stats.py +137 -0
- pgwerk/cli/throughput.py +98 -0
- pgwerk/cli/utils.py +103 -0
- pgwerk/cli/worker.py +55 -0
- pgwerk/commons.py +41 -0
- pgwerk/config.py +76 -0
- pgwerk/cron.py +247 -0
- pgwerk/database.py +289 -0
- pgwerk/exceptions.py +26 -0
- pgwerk/exporter/__init__.py +256 -0
- pgwerk/logging.py +132 -0
- pgwerk/py.typed +0 -0
- pgwerk/repos.py +1168 -0
- pgwerk/schemas.py +497 -0
- pgwerk/serializers.py +166 -0
- pgwerk/utils.py +245 -0
- pgwerk/worker/__init__.py +14 -0
- pgwerk/worker/aio.py +72 -0
- pgwerk/worker/base.py +642 -0
- pgwerk/worker/fork.py +138 -0
- pgwerk/worker/process.py +81 -0
- pgwerk/worker/thread.py +76 -0
- pgwerk-0.1.0.dist-info/METADATA +279 -0
- pgwerk-0.1.0.dist-info/RECORD +41 -0
- pgwerk-0.1.0.dist-info/WHEEL +4 -0
- pgwerk-0.1.0.dist-info/entry_points.txt +2 -0
- pgwerk-0.1.0.dist-info/licenses/LICENSE +21 -0
pgwerk/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from .app import Werk
|
|
2
|
+
from .cron import CronJob
|
|
3
|
+
from .cron import CronScheduler
|
|
4
|
+
from .config import WerkConfig
|
|
5
|
+
from .worker import BaseWorker
|
|
6
|
+
from .worker import ForkWorker
|
|
7
|
+
from .worker import AsyncWorker
|
|
8
|
+
from .worker import ThreadWorker
|
|
9
|
+
from .worker import ProcessWorker
|
|
10
|
+
from .commons import JobStatus
|
|
11
|
+
from .commons import FailureMode
|
|
12
|
+
from .commons import DequeueStrategy
|
|
13
|
+
from .commons import ExecutionStatus
|
|
14
|
+
from .logging import configure_logging
|
|
15
|
+
from .schemas import Job
|
|
16
|
+
from .schemas import Retry
|
|
17
|
+
from .schemas import Repeat
|
|
18
|
+
from .schemas import Context
|
|
19
|
+
from .schemas import Callback
|
|
20
|
+
from .schemas import Dependency
|
|
21
|
+
from .schemas import JobExecution
|
|
22
|
+
from .schemas import EnqueueParams
|
|
23
|
+
from .exceptions import JobError
|
|
24
|
+
from .exceptions import WerkError
|
|
25
|
+
from .exceptions import JobTimeout
|
|
26
|
+
from .exceptions import JobNotFound
|
|
27
|
+
from .exceptions import DependencyFailed
|
|
28
|
+
from .serializers import Serializer
|
|
29
|
+
from .serializers import JSONSerializer
|
|
30
|
+
from .serializers import PickleSerializer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# App
|
|
35
|
+
"Werk",
|
|
36
|
+
"WerkConfig",
|
|
37
|
+
"EnqueueParams",
|
|
38
|
+
# Job types
|
|
39
|
+
"Job",
|
|
40
|
+
"JobExecution",
|
|
41
|
+
"JobStatus",
|
|
42
|
+
"ExecutionStatus",
|
|
43
|
+
"Retry",
|
|
44
|
+
"Dependency",
|
|
45
|
+
"Callback",
|
|
46
|
+
"Repeat",
|
|
47
|
+
# Workers
|
|
48
|
+
"BaseWorker",
|
|
49
|
+
"AsyncWorker",
|
|
50
|
+
"ThreadWorker",
|
|
51
|
+
"ProcessWorker",
|
|
52
|
+
"ForkWorker",
|
|
53
|
+
"DequeueStrategy",
|
|
54
|
+
"FailureMode",
|
|
55
|
+
# Scheduler
|
|
56
|
+
"CronJob",
|
|
57
|
+
"CronScheduler",
|
|
58
|
+
# Serializers
|
|
59
|
+
"Serializer",
|
|
60
|
+
"JSONSerializer",
|
|
61
|
+
"PickleSerializer",
|
|
62
|
+
# Logging
|
|
63
|
+
"configure_logging",
|
|
64
|
+
# Exceptions
|
|
65
|
+
"WerkError",
|
|
66
|
+
"JobNotFound",
|
|
67
|
+
"JobTimeout",
|
|
68
|
+
"JobError",
|
|
69
|
+
"DependencyFailed",
|
|
70
|
+
# Context
|
|
71
|
+
"Context",
|
|
72
|
+
]
|
pgwerk/api/__init__.py
ADDED
pgwerk/api/app.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from litestar import Request
|
|
10
|
+
from litestar import Litestar
|
|
11
|
+
from litestar import Response
|
|
12
|
+
from litestar import get
|
|
13
|
+
from litestar.di import Provide
|
|
14
|
+
from litestar.static_files import StaticFilesConfig
|
|
15
|
+
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_STATIC_DIR = pathlib.Path(__file__).parent / "static"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _static_config() -> list[StaticFilesConfig]:
|
|
22
|
+
if (_STATIC_DIR / "index.html").exists():
|
|
23
|
+
return [StaticFilesConfig(directories=[_STATIC_DIR], path="/", html_mode=True)]
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
from ..app import Werk # noqa: E402
|
|
28
|
+
from .routes import router # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from ..exporter import WerkExporter
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("pgwerk.api")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _server_error_handler(request: Request, exc: Exception) -> Response:
|
|
39
|
+
logger.exception("Unhandled exception on %s %s", request.method, request.url.path, exc_info=exc)
|
|
40
|
+
return Response(
|
|
41
|
+
content={"detail": "Internal server error"},
|
|
42
|
+
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _create_pgwerk() -> Werk:
|
|
47
|
+
dsn = os.environ["PGWERK_DSN"]
|
|
48
|
+
app = Werk(dsn)
|
|
49
|
+
await app.connect()
|
|
50
|
+
return app
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _close_pgwerk(werk: Werk) -> None:
|
|
54
|
+
await werk.disconnect()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_app(
|
|
58
|
+
werk: Werk | None = None,
|
|
59
|
+
exporter: "WerkExporter | None" = None,
|
|
60
|
+
exporter_interval: float | None = None,
|
|
61
|
+
) -> Litestar:
|
|
62
|
+
"""Create the Litestar observability app.
|
|
63
|
+
|
|
64
|
+
Pass an already-connected ``Werk`` instance, or set ``PGWERK_DSN`` in the
|
|
65
|
+
environment and one will be created on startup.
|
|
66
|
+
|
|
67
|
+
To serve Prometheus metrics at ``GET /metrics`` on the same server:
|
|
68
|
+
- pass a pre-built ``WerkExporter`` via *exporter*, or
|
|
69
|
+
- pass *exporter_interval* (seconds) and one will be created automatically.
|
|
70
|
+
"""
|
|
71
|
+
dependencies: dict = {}
|
|
72
|
+
on_startup = []
|
|
73
|
+
on_shutdown = []
|
|
74
|
+
|
|
75
|
+
if werk is not None:
|
|
76
|
+
dependencies["werk"] = Provide(lambda: werk, use_cache=True, sync_to_thread=False)
|
|
77
|
+
|
|
78
|
+
# Exporter needs the Werk instance — create it now if only interval was given.
|
|
79
|
+
if exporter is None and exporter_interval is not None:
|
|
80
|
+
from ..exporter import WerkExporter
|
|
81
|
+
|
|
82
|
+
exporter = WerkExporter(werk, interval=exporter_interval)
|
|
83
|
+
else:
|
|
84
|
+
_state: dict = {}
|
|
85
|
+
|
|
86
|
+
async def _startup() -> None:
|
|
87
|
+
_state["werk"] = await _create_pgwerk()
|
|
88
|
+
if exporter_interval is not None and "exporter" not in _state:
|
|
89
|
+
from ..exporter import WerkExporter
|
|
90
|
+
|
|
91
|
+
_state["exporter"] = WerkExporter(_state["werk"], interval=exporter_interval)
|
|
92
|
+
await _state["exporter"].start()
|
|
93
|
+
|
|
94
|
+
async def _shutdown() -> None:
|
|
95
|
+
if "exporter" in _state:
|
|
96
|
+
await _state["exporter"].stop()
|
|
97
|
+
if "werk" in _state:
|
|
98
|
+
await _close_pgwerk(_state["werk"])
|
|
99
|
+
|
|
100
|
+
async def _get_pgwerk() -> Werk:
|
|
101
|
+
return _state["werk"]
|
|
102
|
+
|
|
103
|
+
dependencies["werk"] = Provide(_get_pgwerk, use_cache=True, sync_to_thread=False)
|
|
104
|
+
on_startup.append(_startup)
|
|
105
|
+
on_shutdown.append(_shutdown)
|
|
106
|
+
|
|
107
|
+
# Metrics handler for the env-var path — reads exporter from _state at request time.
|
|
108
|
+
if exporter_interval is not None:
|
|
109
|
+
|
|
110
|
+
@get("/metrics", media_type="text/plain", sync_to_thread=False)
|
|
111
|
+
async def _metrics_from_state() -> Response[bytes]:
|
|
112
|
+
exp = _state.get("exporter")
|
|
113
|
+
if exp is None:
|
|
114
|
+
return Response(content=b"", media_type="text/plain", status_code=503)
|
|
115
|
+
body, content_type = exp.metrics_bytes()
|
|
116
|
+
return Response(content=body, media_type=content_type)
|
|
117
|
+
|
|
118
|
+
return Litestar(
|
|
119
|
+
route_handlers=[router, _metrics_from_state],
|
|
120
|
+
static_files_config=_static_config(),
|
|
121
|
+
dependencies=dependencies,
|
|
122
|
+
on_startup=on_startup,
|
|
123
|
+
on_shutdown=on_shutdown,
|
|
124
|
+
exception_handlers={Exception: _server_error_handler},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
route_handlers: list = [router]
|
|
128
|
+
|
|
129
|
+
if exporter is not None:
|
|
130
|
+
_exporter = exporter
|
|
131
|
+
|
|
132
|
+
@get("/metrics", media_type="text/plain", sync_to_thread=False)
|
|
133
|
+
async def metrics_handler() -> Response[bytes]:
|
|
134
|
+
body, content_type = _exporter.metrics_bytes()
|
|
135
|
+
return Response(content=body, media_type=content_type)
|
|
136
|
+
|
|
137
|
+
route_handlers.append(metrics_handler)
|
|
138
|
+
on_startup.append(_exporter.start)
|
|
139
|
+
on_shutdown.append(_exporter.stop)
|
|
140
|
+
|
|
141
|
+
return Litestar(
|
|
142
|
+
route_handlers=route_handlers,
|
|
143
|
+
static_files_config=_static_config(),
|
|
144
|
+
dependencies=dependencies,
|
|
145
|
+
on_startup=on_startup,
|
|
146
|
+
on_shutdown=on_shutdown,
|
|
147
|
+
exception_handlers={Exception: _server_error_handler},
|
|
148
|
+
)
|
pgwerk/api/models.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Response models and request bodies for the wrk REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import Any
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..schemas import Job
|
|
14
|
+
from ..schemas import JobExecution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class JobResponse:
|
|
19
|
+
"""Serialisable view of a job row returned by the API.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
id: UUID of the job.
|
|
23
|
+
function: Dotted import path of the job handler.
|
|
24
|
+
queue: Queue the job belongs to.
|
|
25
|
+
status: Current lifecycle state as a plain string.
|
|
26
|
+
priority: Dequeue priority.
|
|
27
|
+
attempts: Number of execution attempts so far.
|
|
28
|
+
max_attempts: Maximum total attempts.
|
|
29
|
+
scheduled_at: Earliest time the job may be dequeued.
|
|
30
|
+
enqueued_at: Wall-clock insertion time.
|
|
31
|
+
key: Optional deduplication key.
|
|
32
|
+
group_key: Optional group concurrency key.
|
|
33
|
+
error: Error message or traceback from the last failed attempt.
|
|
34
|
+
timeout_secs: Hard per-attempt timeout in seconds.
|
|
35
|
+
heartbeat_secs: Maximum heartbeat gap before the job is considered lost.
|
|
36
|
+
started_at: When the most recent attempt began.
|
|
37
|
+
completed_at: When the job reached a terminal state.
|
|
38
|
+
worker_id: UUID of the worker currently or last executing this job.
|
|
39
|
+
meta: User-supplied metadata.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
id: str
|
|
43
|
+
function: str
|
|
44
|
+
queue: str
|
|
45
|
+
status: str
|
|
46
|
+
priority: int
|
|
47
|
+
attempts: int
|
|
48
|
+
max_attempts: int
|
|
49
|
+
scheduled_at: datetime
|
|
50
|
+
enqueued_at: datetime
|
|
51
|
+
key: str | None = None
|
|
52
|
+
group_key: str | None = None
|
|
53
|
+
error: str | None = None
|
|
54
|
+
timeout_secs: int | None = None
|
|
55
|
+
heartbeat_secs: int | None = None
|
|
56
|
+
started_at: datetime | None = None
|
|
57
|
+
completed_at: datetime | None = None
|
|
58
|
+
worker_id: str | None = None
|
|
59
|
+
meta: dict[str, Any] | None = None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_job(cls, job: "Job") -> "JobResponse":
|
|
63
|
+
"""Construct a JobResponse from a :class:`~wrk.schemas.Job` instance.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
job: The source Job domain object.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A JobResponse suitable for JSON serialisation.
|
|
70
|
+
"""
|
|
71
|
+
return cls(
|
|
72
|
+
id=job.id,
|
|
73
|
+
function=job.function,
|
|
74
|
+
queue=job.queue,
|
|
75
|
+
status=str(job.status.value),
|
|
76
|
+
priority=job.priority,
|
|
77
|
+
attempts=job.attempts,
|
|
78
|
+
max_attempts=job.max_attempts,
|
|
79
|
+
scheduled_at=job.scheduled_at,
|
|
80
|
+
enqueued_at=job.enqueued_at,
|
|
81
|
+
key=job.key,
|
|
82
|
+
group_key=job.group_key,
|
|
83
|
+
error=job.error,
|
|
84
|
+
timeout_secs=job.timeout_secs,
|
|
85
|
+
heartbeat_secs=job.heartbeat_secs,
|
|
86
|
+
started_at=job.started_at,
|
|
87
|
+
completed_at=job.completed_at,
|
|
88
|
+
worker_id=job.worker_id,
|
|
89
|
+
meta=job.meta,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclasses.dataclass
|
|
94
|
+
class ExecutionResponse:
|
|
95
|
+
"""Serialisable view of a single job execution attempt.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
id: UUID of the execution row.
|
|
99
|
+
job_id: UUID of the parent job.
|
|
100
|
+
attempt: 1-based attempt number.
|
|
101
|
+
status: Current or final state of this execution.
|
|
102
|
+
worker_id: UUID of the worker that ran this attempt.
|
|
103
|
+
error: Error message or traceback if the attempt failed.
|
|
104
|
+
started_at: When this attempt began.
|
|
105
|
+
completed_at: When this attempt reached a terminal state.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
id: str
|
|
109
|
+
job_id: str
|
|
110
|
+
attempt: int
|
|
111
|
+
status: str
|
|
112
|
+
worker_id: str | None = None
|
|
113
|
+
error: str | None = None
|
|
114
|
+
started_at: datetime | None = None
|
|
115
|
+
completed_at: datetime | None = None
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_execution(cls, e: "JobExecution") -> "ExecutionResponse":
|
|
119
|
+
"""Construct an ExecutionResponse from a :class:`~wrk.schemas.JobExecution`.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
e: The source JobExecution domain object.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
An ExecutionResponse suitable for JSON serialisation.
|
|
126
|
+
"""
|
|
127
|
+
return cls(
|
|
128
|
+
id=e.id,
|
|
129
|
+
job_id=e.job_id,
|
|
130
|
+
attempt=e.attempt,
|
|
131
|
+
status=str(e.status.value),
|
|
132
|
+
worker_id=e.worker_id,
|
|
133
|
+
error=e.error,
|
|
134
|
+
started_at=e.started_at,
|
|
135
|
+
completed_at=e.completed_at,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclasses.dataclass
|
|
140
|
+
class WorkerResponse:
|
|
141
|
+
"""Serialisable view of a registered worker row.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
id: UUID of the worker.
|
|
145
|
+
name: Human-readable ``hostname.pid`` identifier.
|
|
146
|
+
queue: Primary queue the worker is consuming.
|
|
147
|
+
status: Current worker status (e.g. ``"idle"``, ``"busy"``).
|
|
148
|
+
metadata: JSON blob with runtime info (PID, concurrency, strategy, etc.).
|
|
149
|
+
heartbeat_at: Timestamp of the last heartbeat.
|
|
150
|
+
started_at: When the worker registered.
|
|
151
|
+
expires_at: Time after which the worker row may be cleaned up.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
id: str
|
|
155
|
+
name: str
|
|
156
|
+
queue: str
|
|
157
|
+
status: str
|
|
158
|
+
metadata: dict[str, Any] | None = None
|
|
159
|
+
heartbeat_at: datetime | None = None
|
|
160
|
+
started_at: datetime | None = None
|
|
161
|
+
expires_at: datetime | None = None
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_row(cls, r: dict[str, Any]) -> "WorkerResponse":
|
|
165
|
+
"""Construct a WorkerResponse from a raw database row dict.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
r: Database row as a mapping of column names to values.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A WorkerResponse suitable for JSON serialisation.
|
|
172
|
+
"""
|
|
173
|
+
return cls(
|
|
174
|
+
id=r["id"],
|
|
175
|
+
name=r["name"],
|
|
176
|
+
queue=r["queue"],
|
|
177
|
+
status=r["status"],
|
|
178
|
+
metadata=r["metadata"],
|
|
179
|
+
heartbeat_at=r["heartbeat_at"],
|
|
180
|
+
started_at=r["started_at"],
|
|
181
|
+
expires_at=r["expires_at"],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclasses.dataclass
|
|
186
|
+
class QueueStats:
|
|
187
|
+
"""Per-queue job counts broken down by status.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
queue: Name of the queue.
|
|
191
|
+
scheduled: Jobs waiting for their ``scheduled_at`` to arrive.
|
|
192
|
+
queued: Jobs ready for immediate dequeue.
|
|
193
|
+
active: Jobs currently being executed.
|
|
194
|
+
waiting: Jobs blocked on dependencies.
|
|
195
|
+
failed: Jobs that exhausted all retry attempts.
|
|
196
|
+
complete: Successfully completed jobs.
|
|
197
|
+
aborted: Jobs that were manually aborted.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
queue: str
|
|
201
|
+
scheduled: int
|
|
202
|
+
queued: int
|
|
203
|
+
active: int
|
|
204
|
+
waiting: int
|
|
205
|
+
failed: int
|
|
206
|
+
complete: int
|
|
207
|
+
aborted: int
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def from_row(cls, r: dict[str, Any]) -> "QueueStats":
|
|
211
|
+
"""Construct QueueStats from a raw database row dict.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
r: Database row as a mapping of column names to integer counts.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
A QueueStats instance.
|
|
218
|
+
"""
|
|
219
|
+
return cls(
|
|
220
|
+
queue=r["queue"],
|
|
221
|
+
scheduled=r["scheduled"],
|
|
222
|
+
queued=r["queued"],
|
|
223
|
+
active=r["active"],
|
|
224
|
+
waiting=r["waiting"],
|
|
225
|
+
failed=r["failed"],
|
|
226
|
+
complete=r["complete"],
|
|
227
|
+
aborted=r["aborted"],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclasses.dataclass
|
|
232
|
+
class StatsResponse:
|
|
233
|
+
"""Aggregate statistics response returned by the stats endpoint.
|
|
234
|
+
|
|
235
|
+
Attributes:
|
|
236
|
+
queues: Per-queue job counts.
|
|
237
|
+
total_jobs: Total number of job rows across all queues.
|
|
238
|
+
workers_online: Number of workers with a recent heartbeat.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
queues: list[QueueStats]
|
|
242
|
+
total_jobs: int
|
|
243
|
+
workers_online: int
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclasses.dataclass
|
|
247
|
+
class WorkerThroughputPoint:
|
|
248
|
+
"""A single data point in a worker throughput time series.
|
|
249
|
+
|
|
250
|
+
Attributes:
|
|
251
|
+
time: Truncated timestamp for this bucket (e.g. per-minute).
|
|
252
|
+
worker_id: UUID of the worker, or ``None`` for an aggregate row.
|
|
253
|
+
worker_name: Human-readable worker name, or ``None`` for an aggregate row.
|
|
254
|
+
count: Number of jobs completed in this bucket.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
time: datetime
|
|
258
|
+
worker_id: str | None
|
|
259
|
+
worker_name: str | None
|
|
260
|
+
count: int
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def from_row(cls, r: dict[str, Any]) -> "WorkerThroughputPoint":
|
|
264
|
+
"""Construct a WorkerThroughputPoint from a raw database row dict.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
r: Database row with keys ``time``, ``worker_id``, ``worker_name``,
|
|
268
|
+
and ``count``.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
A WorkerThroughputPoint instance.
|
|
272
|
+
"""
|
|
273
|
+
return cls(
|
|
274
|
+
time=r["time"],
|
|
275
|
+
worker_id=r["worker_id"],
|
|
276
|
+
worker_name=r["worker_name"],
|
|
277
|
+
count=r["count"],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclasses.dataclass
|
|
282
|
+
class QueueDepthPoint:
|
|
283
|
+
"""A single data point in a queue depth time series.
|
|
284
|
+
|
|
285
|
+
Attributes:
|
|
286
|
+
time: Truncated timestamp for this bucket (e.g. per-minute).
|
|
287
|
+
queued: Number of jobs in ``queued`` status at this point in time.
|
|
288
|
+
active: Number of jobs in ``active`` status at this point in time.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
time: datetime
|
|
292
|
+
queued: int
|
|
293
|
+
active: int
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def from_row(cls, r: dict[str, Any]) -> "QueueDepthPoint":
|
|
297
|
+
"""Construct a QueueDepthPoint from a raw database row dict.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
r: Database row with keys ``time``, ``queued``, and ``active``.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
A QueueDepthPoint instance.
|
|
304
|
+
"""
|
|
305
|
+
return cls(
|
|
306
|
+
time=r["time"],
|
|
307
|
+
queued=r["queued"],
|
|
308
|
+
active=r["active"],
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclasses.dataclass
|
|
313
|
+
class TableInfo:
|
|
314
|
+
"""Size and row-count metadata for a single wrk database table.
|
|
315
|
+
|
|
316
|
+
Attributes:
|
|
317
|
+
name: Fully-qualified table name.
|
|
318
|
+
size_bytes: Total on-disk size of the table in bytes.
|
|
319
|
+
row_count: Estimated number of rows (from ``pg_class``).
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
name: str
|
|
323
|
+
size_bytes: int
|
|
324
|
+
row_count: int
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclasses.dataclass
|
|
328
|
+
class ServerInfo:
|
|
329
|
+
"""Server-level information returned by the server info endpoint.
|
|
330
|
+
|
|
331
|
+
Attributes:
|
|
332
|
+
pg_version: Postgres server version string.
|
|
333
|
+
db_size_bytes: Total database size in bytes.
|
|
334
|
+
tables: Per-table size and row-count metadata.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
pg_version: str
|
|
338
|
+
db_size_bytes: int
|
|
339
|
+
tables: list[TableInfo]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclasses.dataclass
|
|
343
|
+
class PurgeRequest:
|
|
344
|
+
"""Request body for the bulk job purge endpoint.
|
|
345
|
+
|
|
346
|
+
Attributes:
|
|
347
|
+
statuses: Job status values to purge (defaults to ``complete``,
|
|
348
|
+
``failed``, and ``aborted``).
|
|
349
|
+
older_than_days: Only delete jobs that completed more than this many
|
|
350
|
+
days ago.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
statuses: list[str] = dataclasses.field(default_factory=lambda: ["complete", "failed", "aborted"])
|
|
354
|
+
older_than_days: int = 30
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclasses.dataclass
|
|
358
|
+
class BulkRequeueRequest:
|
|
359
|
+
"""Request body for the bulk requeue endpoint.
|
|
360
|
+
|
|
361
|
+
Attributes:
|
|
362
|
+
queue: Limit requeue to this queue; ``None`` targets all queues.
|
|
363
|
+
function_name: Limit requeue to jobs with this function name.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
queue: str | None = None
|
|
367
|
+
function_name: str | None = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@dataclasses.dataclass
|
|
371
|
+
class BulkCancelRequest:
|
|
372
|
+
"""Request body for the bulk cancel endpoint.
|
|
373
|
+
|
|
374
|
+
Attributes:
|
|
375
|
+
queue: Limit cancellation to this queue; ``None`` targets all queues.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
queue: str | None = None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclasses.dataclass
|
|
382
|
+
class EnqueueRequest:
|
|
383
|
+
"""Request body for the single-job enqueue endpoint.
|
|
384
|
+
|
|
385
|
+
Attributes:
|
|
386
|
+
function: Dotted import path of the handler to invoke.
|
|
387
|
+
queue: Target queue name.
|
|
388
|
+
priority: Dequeue priority — higher values are processed first.
|
|
389
|
+
args: Positional arguments forwarded to the handler.
|
|
390
|
+
kwargs: Keyword arguments forwarded to the handler.
|
|
391
|
+
key: Optional deduplication key; duplicate enqueues are silently dropped.
|
|
392
|
+
delay: Seconds to wait before the job becomes eligible for dequeue.
|
|
393
|
+
scheduled_at: Absolute time at which the job becomes eligible; overrides
|
|
394
|
+
``delay`` when both are set.
|
|
395
|
+
max_attempts: Maximum total execution attempts.
|
|
396
|
+
timeout_secs: Hard per-attempt wall-clock timeout in seconds.
|
|
397
|
+
meta: Arbitrary user-supplied metadata stored with the job.
|
|
398
|
+
cron_name: Cron job name to associate with this enqueue.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
function: str
|
|
402
|
+
queue: str = "default"
|
|
403
|
+
priority: int = 0
|
|
404
|
+
args: list[Any] = dataclasses.field(default_factory=list)
|
|
405
|
+
kwargs: dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
406
|
+
key: str | None = None
|
|
407
|
+
delay: int | None = None
|
|
408
|
+
scheduled_at: datetime | None = None
|
|
409
|
+
max_attempts: int = 1
|
|
410
|
+
timeout_secs: int | None = None
|
|
411
|
+
meta: dict[str, Any] | None = None
|
|
412
|
+
cron_name: str | None = None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@dataclasses.dataclass
|
|
416
|
+
class CronJobStats:
|
|
417
|
+
"""Aggregated statistics for a single registered cron job.
|
|
418
|
+
|
|
419
|
+
Attributes:
|
|
420
|
+
name: Cron job name (``module.qualname`` by default).
|
|
421
|
+
function: Dotted import path of the handler.
|
|
422
|
+
queue: Queue the cron job enqueues into.
|
|
423
|
+
total_runs: Total number of times the job has been enqueued.
|
|
424
|
+
failed_runs: Number of runs that ended in a failed or aborted state.
|
|
425
|
+
last_status: Status of the most recent execution.
|
|
426
|
+
last_enqueued_at: Timestamp of the most recent enqueue.
|
|
427
|
+
last_completed_at: Timestamp of the most recent completion.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
name: str
|
|
431
|
+
function: str
|
|
432
|
+
queue: str
|
|
433
|
+
total_runs: int
|
|
434
|
+
failed_runs: int
|
|
435
|
+
last_status: str | None = None
|
|
436
|
+
last_enqueued_at: datetime | None = None
|
|
437
|
+
last_completed_at: datetime | None = None
|
|
438
|
+
|
|
439
|
+
@classmethod
|
|
440
|
+
def from_row(cls, r: dict[str, Any]) -> "CronJobStats":
|
|
441
|
+
"""Construct CronJobStats from a raw database row dict.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
r: Database row with aggregated cron statistics columns.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
A CronJobStats instance.
|
|
448
|
+
"""
|
|
449
|
+
return cls(
|
|
450
|
+
name=r["cron_name"],
|
|
451
|
+
function=r["function"],
|
|
452
|
+
queue=r["queue"],
|
|
453
|
+
total_runs=r["total_runs"],
|
|
454
|
+
failed_runs=r["failed_runs"],
|
|
455
|
+
last_status=r["last_status"],
|
|
456
|
+
last_enqueued_at=r["last_enqueued_at"],
|
|
457
|
+
last_completed_at=r["last_completed_at"],
|
|
458
|
+
)
|