django-overseer 0.1.0__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.
- django_overseer-0.1.0/.gitignore +12 -0
- django_overseer-0.1.0/CHANGELOG.md +27 -0
- django_overseer-0.1.0/LICENSE +21 -0
- django_overseer-0.1.0/PKG-INFO +351 -0
- django_overseer-0.1.0/README.md +304 -0
- django_overseer-0.1.0/docs/design.md +99 -0
- django_overseer-0.1.0/docs/settings.md +88 -0
- django_overseer-0.1.0/pyproject.toml +98 -0
- django_overseer-0.1.0/src/overseer/__init__.py +15 -0
- django_overseer-0.1.0/src/overseer/adapters/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/adapters/base.py +54 -0
- django_overseer-0.1.0/src/overseer/adapters/django_tasks_db.py +49 -0
- django_overseer-0.1.0/src/overseer/alerts.py +182 -0
- django_overseer-0.1.0/src/overseer/api.py +210 -0
- django_overseer-0.1.0/src/overseer/apps.py +21 -0
- django_overseer-0.1.0/src/overseer/checks.py +49 -0
- django_overseer-0.1.0/src/overseer/conf.py +48 -0
- django_overseer-0.1.0/src/overseer/decorators.py +192 -0
- django_overseer-0.1.0/src/overseer/exceptions.py +10 -0
- django_overseer-0.1.0/src/overseer/management/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/management/commands/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_alerts.py +13 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_prune.py +19 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_rescue.py +11 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_rollup.py +16 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_scheduler.py +24 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_sync_schedules.py +14 -0
- django_overseer-0.1.0/src/overseer/management/commands/overseer_worker.py +67 -0
- django_overseer-0.1.0/src/overseer/metrics.py +155 -0
- django_overseer-0.1.0/src/overseer/migrations/0001_initial.py +378 -0
- django_overseer-0.1.0/src/overseer/migrations/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/models.py +266 -0
- django_overseer-0.1.0/src/overseer/prune.py +31 -0
- django_overseer-0.1.0/src/overseer/recorders.py +231 -0
- django_overseer-0.1.0/src/overseer/registry.py +61 -0
- django_overseer-0.1.0/src/overseer/rescue.py +71 -0
- django_overseer-0.1.0/src/overseer/retry.py +115 -0
- django_overseer-0.1.0/src/overseer/scheduling/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/scheduling/cron.py +150 -0
- django_overseer-0.1.0/src/overseer/scheduling/registry.py +40 -0
- django_overseer-0.1.0/src/overseer/scheduling/scheduler.py +179 -0
- django_overseer-0.1.0/src/overseer/scheduling/sync.py +77 -0
- django_overseer-0.1.0/src/overseer/signals.py +10 -0
- django_overseer-0.1.0/src/overseer/static/overseer/overseer.css +66 -0
- django_overseer-0.1.0/src/overseer/static/overseer/overseer.js +45 -0
- django_overseer-0.1.0/src/overseer/stats.py +246 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/_queues_table.html +15 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/_window.html +6 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/alerts.html +15 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/base.html +36 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/failed.html +37 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/job_detail.html +52 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/jobs.html +37 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/metrics.html +34 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/overview.html +50 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/queues.html +7 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/schedules.html +28 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/tasks.html +21 -0
- django_overseer-0.1.0/src/overseer/templates/overseer/workers.html +23 -0
- django_overseer-0.1.0/src/overseer/templatetags/__init__.py +0 -0
- django_overseer-0.1.0/src/overseer/templatetags/overseer_extras.py +64 -0
- django_overseer-0.1.0/src/overseer/urls.py +37 -0
- django_overseer-0.1.0/src/overseer/views.py +342 -0
- django_overseer-0.1.0/src/overseer/worker.py +97 -0
- django_overseer-0.1.0/tests/__init__.py +0 -0
- django_overseer-0.1.0/tests/conftest.py +72 -0
- django_overseer-0.1.0/tests/fresh_project.sh +132 -0
- django_overseer-0.1.0/tests/procs.py +115 -0
- django_overseer-0.1.0/tests/settings.py +82 -0
- django_overseer-0.1.0/tests/settings_postgres.py +19 -0
- django_overseer-0.1.0/tests/tasks.py +81 -0
- django_overseer-0.1.0/tests/test_alerts.py +198 -0
- django_overseer-0.1.0/tests/test_concurrency.py +120 -0
- django_overseer-0.1.0/tests/test_cron.py +74 -0
- django_overseer-0.1.0/tests/test_dashboard.py +329 -0
- django_overseer-0.1.0/tests/test_decorators.py +73 -0
- django_overseer-0.1.0/tests/test_e2e.py +218 -0
- django_overseer-0.1.0/tests/test_integration.py +219 -0
- django_overseer-0.1.0/tests/test_metrics.py +174 -0
- django_overseer-0.1.0/tests/test_perf.py +183 -0
- django_overseer-0.1.0/tests/test_recorders.py +119 -0
- django_overseer-0.1.0/tests/test_rescue.py +128 -0
- django_overseer-0.1.0/tests/test_retry.py +144 -0
- django_overseer-0.1.0/tests/test_scheduler.py +281 -0
- django_overseer-0.1.0/tests/test_worker.py +104 -0
- django_overseer-0.1.0/tests/urls.py +7 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Recording of every `django.tasks` enqueue, start and finish into `Job`, `Run` and
|
|
8
|
+
`Worker` rows through the framework's signals; works with any backend.
|
|
9
|
+
- `overseer.task`: retries with exponential, linear or constant backoff, jitter, cap,
|
|
10
|
+
`retry_on` filters, per-task timeouts, tags, and `unique` enqueues enforced by a database
|
|
11
|
+
constraint.
|
|
12
|
+
- `overseer.schedule`: cron expressions (dependency-free parser, timezone aware) and fixed
|
|
13
|
+
intervals, synced into the `Schedule` table and safe across several scheduler processes.
|
|
14
|
+
- `overseer_scheduler`, `overseer_worker` (heartbeats), `overseer_rescue`,
|
|
15
|
+
`overseer_rollup`, `overseer_alerts`, `overseer_prune` and `overseer_sync_schedules`
|
|
16
|
+
management commands.
|
|
17
|
+
- Rescue of abandoned runs with backend reset for `django-tasks-db`.
|
|
18
|
+
- Dashboard: overview, queues, tasks, jobs, job detail, failed, schedules, workers, metrics
|
|
19
|
+
and alerts pages with retry / cancel / dismiss / pause / run-now actions, auto refresh and
|
|
20
|
+
a JSON API with a health endpoint.
|
|
21
|
+
- Alerts for failure rate, queue wait, queue depth and offline workers with cooldowns,
|
|
22
|
+
resolution, e-mail, Slack and custom notifiers, plus `alert_fired`, `alert_resolved` and
|
|
23
|
+
`job_failed` signals.
|
|
24
|
+
- Per-minute metric rollups and retention pruning.
|
|
25
|
+
- System checks for settings and non-deferring backends.
|
|
26
|
+
- Test suite in four layers (unit, real-process integration, HTTP end-to-end, performance
|
|
27
|
+
budgets) on SQLite and PostgreSQL, Django 6.0 and 6.1.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mohamed ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-overseer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dashboard, retries and schedules for Django's built-in Tasks framework
|
|
5
|
+
Project-URL: Homepage, https://github.com/mohamed-alired/django-overseer
|
|
6
|
+
Project-URL: Repository, https://github.com/mohamed-alired/django-overseer.git
|
|
7
|
+
Project-URL: Issues, https://github.com/mohamed-alired/django-overseer/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/mohamed-alired/django-overseer/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: mohamed ali <mohamed.ali@redwoodcompliance.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: background,cron,dashboard,django,queue,retries,scheduler,tasks
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 6.0
|
|
17
|
+
Classifier: Framework :: Django :: 6.1
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: System :: Monitoring
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Requires-Dist: django>=6.0
|
|
27
|
+
Provides-Extra: db
|
|
28
|
+
Requires-Dist: django-tasks-db>=0.13; extra == 'db'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build; extra == 'dev'
|
|
31
|
+
Requires-Dist: django-tasks-db>=0.13; extra == 'dev'
|
|
32
|
+
Requires-Dist: freezegun>=1.5; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-django>=4.9; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
36
|
+
Requires-Dist: requests>=2.31; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine; extra == 'dev'
|
|
39
|
+
Provides-Extra: test
|
|
40
|
+
Requires-Dist: django-tasks-db>=0.13; extra == 'test'
|
|
41
|
+
Requires-Dist: freezegun>=1.5; extra == 'test'
|
|
42
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest-django>=4.9; extra == 'test'
|
|
44
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
45
|
+
Requires-Dist: requests>=2.31; extra == 'test'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# django-overseer
|
|
49
|
+
|
|
50
|
+
**Dashboard, retries, schedules and alerts for Django's built-in Tasks framework.**
|
|
51
|
+
|
|
52
|
+
Django 6 ships a Tasks framework (`django.tasks`) and a database-backed worker
|
|
53
|
+
(`django-tasks-db`), but no way to *see* what your tasks are doing, no retries, no cron and
|
|
54
|
+
no alarm when a queue backs up. Overseer adds the operational layer that every other
|
|
55
|
+
ecosystem takes for granted (Sidekiq's web UI, Celery's Flower, Oban's dashboard) without
|
|
56
|
+
adding a broker, a JavaScript build or a second framework.
|
|
57
|
+
|
|
58
|
+
- **Dashboard**: overview, queues, tasks, jobs with filters and search, job detail with the
|
|
59
|
+
full attempt chain and tracebacks, failed jobs with retry / dismiss, schedules, workers,
|
|
60
|
+
per-minute metrics and alerts. Plain Django views and templates, auto-refreshing, no
|
|
61
|
+
external assets.
|
|
62
|
+
- **Retries**: `@overseer.task(retries=3, backoff="exponential")` with constant, linear or
|
|
63
|
+
exponential backoff, jitter, a cap, `retry_on=` exception filters and per-task timeouts.
|
|
64
|
+
Retries are real re-enqueues through `django.tasks`, deferred with `run_after` on backends
|
|
65
|
+
that support it.
|
|
66
|
+
- **Unique tasks**: `unique=True` (or a key callable) collapses identical pending enqueues,
|
|
67
|
+
enforced by a database constraint so concurrent enqueues cannot both win.
|
|
68
|
+
- **Schedules**: `@overseer.schedule("*/5 * * * *")` or `every=300`, synced into the database,
|
|
69
|
+
pausable and triggerable from the dashboard, with a scheduler that is safe to run on
|
|
70
|
+
several hosts at once (`SELECT ... FOR UPDATE SKIP LOCKED`).
|
|
71
|
+
- **Rescue**: runs whose worker died are detected by timeout, marked abandoned, reset in the
|
|
72
|
+
backend and retried under the task's policy.
|
|
73
|
+
- **Workers**: `overseer_worker` wraps `db_worker` with heartbeats, so the dashboard shows
|
|
74
|
+
which workers are alive, on which host, processing what.
|
|
75
|
+
- **Alerts**: failure rate, queue wait, queue depth and offline workers, with cooldowns,
|
|
76
|
+
automatic resolution, e-mail, Slack and custom notifiers, plus Django signals.
|
|
77
|
+
- **JSON API** and a `/api/health/` endpoint for your own monitoring.
|
|
78
|
+
- **Retention**: per-minute metric rollups and pruning of old jobs, runs and alerts.
|
|
79
|
+
|
|
80
|
+
Everything is recorded through the three `django.tasks` signals, so Overseer works with
|
|
81
|
+
**any** task backend. Cancelling pending tasks and resetting stuck ones needs backend
|
|
82
|
+
knowledge; an adapter ships for `django-tasks-db` and the interface is open for others.
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
- Python 3.12+
|
|
87
|
+
- Django 6.0 or 6.1 (tested against `main` too)
|
|
88
|
+
- A `django.tasks` backend. The reference setup is `django-tasks-db` (`pip install
|
|
89
|
+
"django-overseer[db]"`).
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install "django-overseer[db]"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# settings.py
|
|
99
|
+
INSTALLED_APPS = [
|
|
100
|
+
...
|
|
101
|
+
"django_tasks_db",
|
|
102
|
+
"overseer",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
TASKS = {
|
|
106
|
+
"default": {
|
|
107
|
+
"BACKEND": "django_tasks_db.backend.DatabaseBackend",
|
|
108
|
+
"QUEUES": ["default", "emails"],
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# urls.py
|
|
115
|
+
from django.urls import include, path
|
|
116
|
+
|
|
117
|
+
urlpatterns = [
|
|
118
|
+
path("admin/", admin.site.urls),
|
|
119
|
+
path("overseer/", include("overseer.urls")),
|
|
120
|
+
]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python manage.py migrate
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then run the processes you need:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python manage.py overseer_worker --queue-name='*' # a worker with heartbeats
|
|
131
|
+
python manage.py overseer_scheduler # schedules, rescue, metrics, alerts
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Open `/overseer/` as a staff user with the `overseer.view_dashboard` permission (superusers
|
|
135
|
+
always have it). `overseer.manage_jobs` allows retry / cancel / dismiss and
|
|
136
|
+
`overseer.manage_schedules` allows pausing and triggering schedules.
|
|
137
|
+
|
|
138
|
+
## Declaring tasks
|
|
139
|
+
|
|
140
|
+
`overseer.task` is `django.tasks.task` plus a policy. It accepts the same arguments
|
|
141
|
+
(`priority`, `queue_name`, `backend`, `takes_context`) and returns a normal `Task`, so
|
|
142
|
+
`.enqueue()`, `.using()`, `.call()` and `.get_result()` all work as documented by Django.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import overseer
|
|
146
|
+
from django.tasks import task
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@overseer.task(retries=3, backoff="exponential", backoff_base=30, backoff_max=600)
|
|
150
|
+
def charge_card(order_id):
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@overseer.task(retries=5, retry_on=(ConnectionError, TimeoutError), timeout=120)
|
|
155
|
+
def sync_crm(account_id):
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@overseer.task(unique=True, queue_name="emails", tags=("mail",))
|
|
160
|
+
def send_receipt(email):
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@overseer.task(unique=lambda report_id, **kwargs: f"report:{report_id}")
|
|
165
|
+
def build_report(report_id, force=False):
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@overseer.schedule("0 2 * * *", name="nightly-cleanup", timezone="Europe/Paris")
|
|
170
|
+
@overseer.task(retries=1)
|
|
171
|
+
def nightly_cleanup():
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@overseer.schedule(every=300)
|
|
176
|
+
@task # schedules work on plain django.tasks tasks as well
|
|
177
|
+
def refresh_rates():
|
|
178
|
+
...
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Argument | Meaning |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `retries` | attempts *after* the first (default `OVERSEER_DEFAULT_RETRIES`, 0) |
|
|
184
|
+
| `backoff` | `"exponential"` (default), `"linear"` or `"constant"` |
|
|
185
|
+
| `backoff_base` / `backoff_max` | seconds; the delay before attempt *n* is `base * 2**(n-2)` capped at `max` for exponential, `base * (n-1)` for linear, `base` for constant |
|
|
186
|
+
| `jitter` | multiply the delay by a random factor between 0.8 and 1.2 (default on) |
|
|
187
|
+
| `retry_on` | tuple of exception classes that trigger a retry (default: any `Exception`) |
|
|
188
|
+
| `timeout` | seconds after which a running attempt is treated as abandoned and rescued |
|
|
189
|
+
| `unique` | `True` (key from task path + arguments), a string, or a callable receiving the task arguments |
|
|
190
|
+
| `tags` | labels shown in the dashboard |
|
|
191
|
+
|
|
192
|
+
Tasks declared with plain `django.tasks.task` are recorded too, with the
|
|
193
|
+
`OVERSEER_DEFAULT_*` policy. Task modules named `tasks.py` in installed apps are imported at
|
|
194
|
+
startup (`OVERSEER_AUTODISCOVER`), so schedules and policies exist in every process.
|
|
195
|
+
|
|
196
|
+
Policy only ever applies on failure. Overseer never changes what your task does, when it
|
|
197
|
+
runs, or what the backend returns.
|
|
198
|
+
|
|
199
|
+
## Schedules
|
|
200
|
+
|
|
201
|
+
`overseer.schedule` registers a declaration. `overseer_scheduler` (or `python manage.py
|
|
202
|
+
overseer_sync_schedules`) writes it to the `Schedule` table, from which the dashboard can
|
|
203
|
+
pause, resume or trigger it. Rows created in the dashboard or admin, with
|
|
204
|
+
`declared_in_code=False`, are never touched by sync. A declaration removed from the code
|
|
205
|
+
disables its row rather than deleting it, so its history stays.
|
|
206
|
+
|
|
207
|
+
Cron expressions are the standard five fields with ranges, steps, lists, month and weekday
|
|
208
|
+
names, `@hourly`-style aliases and the usual "day-of-month OR day-of-week" rule. The
|
|
209
|
+
expression is evaluated in the schedule's `timezone`, else `OVERSEER_SCHEDULER_TIMEZONE`,
|
|
210
|
+
else `TIME_ZONE`. A schedule that was missed while no scheduler was running fires once and
|
|
211
|
+
continues from now; missed occurrences are not replayed.
|
|
212
|
+
|
|
213
|
+
The scheduler loop also runs the rescue every `OVERSEER_RESCUE_INTERVAL` seconds and the
|
|
214
|
+
metrics rollup and alert evaluation every `OVERSEER_MAINTENANCE_INTERVAL` seconds. Run
|
|
215
|
+
several schedulers for availability; schedules are claimed with row locks, so each fires
|
|
216
|
+
once.
|
|
217
|
+
|
|
218
|
+
## Commands
|
|
219
|
+
|
|
220
|
+
| Command | What it does |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| `overseer_worker` | `db_worker` with `--heartbeat` seconds (default 10) and a `Worker` row. Accepts every `db_worker` option (`--queue-name`, `--exclude-queues`, `--interval`, `--batch`, `--max-tasks`, `--worker-id`, `--backend`, `--no-startup-delay`, `--reload`). SIGTERM/SIGINT finishes the current task then exits. |
|
|
223
|
+
| `overseer_scheduler [--interval S] [--once]` | syncs schedules, fires what is due, rescues abandoned runs, rolls up metrics, evaluates alerts |
|
|
224
|
+
| `overseer_sync_schedules` | one-off sync of code declarations into the `Schedule` table |
|
|
225
|
+
| `overseer_rescue` | one-off pass over running attempts that exceeded their timeout |
|
|
226
|
+
| `overseer_rollup [--since ISO]` | recompute per-minute metric buckets |
|
|
227
|
+
| `overseer_alerts` | one-off alert evaluation and notification |
|
|
228
|
+
| `overseer_prune [--days N] [--metrics-days N]` | delete finished jobs, runs, alerts, metric buckets and silent workers older than the retention |
|
|
229
|
+
|
|
230
|
+
If you run `db_worker` directly instead of `overseer_worker`, everything still works: the
|
|
231
|
+
worker shows up in the dashboard from the task signals, only the heartbeat and the host /
|
|
232
|
+
pid details are missing.
|
|
233
|
+
|
|
234
|
+
## Dashboard
|
|
235
|
+
|
|
236
|
+
| Page | Content |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| Overview | throughput, failure rate, runtimes, waiting / running / scheduled counts, workers online, runs-per-minute chart, queue table, recent failures, open alerts |
|
|
239
|
+
| Queues | per queue: waiting, running, processed, failed, failure rate, average and p95 runtime, average wait and the backend's own depth when the adapter supports it |
|
|
240
|
+
| Tasks | the same per task path, with the declared policy |
|
|
241
|
+
| Jobs | filter by status, queue, task, worker; free-text search over task path, job id, result id and unique key |
|
|
242
|
+
| Job | arguments, policy, every attempt with timing, worker, exception and traceback, return value; retry / cancel / dismiss |
|
|
243
|
+
| Failed | open failures with the last error; retry all / dismiss all |
|
|
244
|
+
| Schedules | next and last run, run count, pause / resume / run now / sync |
|
|
245
|
+
| Workers | host, pid, queues, last heartbeat, current run, counters; offline after `OVERSEER_WORKER_OFFLINE_AFTER` |
|
|
246
|
+
| Metrics | per-minute runs and p95 runtime for the last 15 minutes to 24 hours, per queue |
|
|
247
|
+
| Alerts | open and resolved alerts |
|
|
248
|
+
|
|
249
|
+
Every page accepts `?minutes=15|60|360|1440`. Panels refresh every
|
|
250
|
+
`OVERSEER_REFRESH_SECONDS` seconds (there is a pause button). The same data is available as
|
|
251
|
+
JSON under `/overseer/api/...` for the logged-in user, plus `/overseer/api/health/`, which
|
|
252
|
+
returns HTTP 503 when tasks are waiting and no worker is online.
|
|
253
|
+
|
|
254
|
+
## Alerts
|
|
255
|
+
|
|
256
|
+
`overseer_scheduler` (or `overseer_alerts`) evaluates four conditions over the last
|
|
257
|
+
`OVERSEER_ALERT_WINDOW_MINUTES` minutes:
|
|
258
|
+
|
|
259
|
+
| Kind | Fires when |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `failure_rate` | a queue's failure rate exceeds `OVERSEER_ALERT_FAILURE_RATE` (after at least 5 finished runs) |
|
|
262
|
+
| `queue_wait` | the oldest waiting run has waited more than `OVERSEER_ALERT_WAIT_SECONDS` |
|
|
263
|
+
| `queue_depth` | more than `OVERSEER_ALERT_QUEUE_DEPTH` runs are waiting |
|
|
264
|
+
| `worker_offline` | a worker that did not stop cleanly has not been seen for `OVERSEER_WORKER_OFFLINE_AFTER` seconds |
|
|
265
|
+
|
|
266
|
+
An alert fires once, then not again for `OVERSEER_ALERT_COOLDOWN_MINUTES`, and resolves
|
|
267
|
+
itself when the condition clears. Notifications go to `OVERSEER_ALERT_EMAILS` (or `ADMINS` when that is empty),
|
|
268
|
+
`OVERSEER_SLACK_WEBHOOK_URL` and every callable in `OVERSEER_NOTIFIERS` (`callable(alert)`);
|
|
269
|
+
a failing notifier is logged, never raised. The signals `overseer.signals.alert_fired`,
|
|
270
|
+
`alert_resolved` and `job_failed` (`job`, `run`) let you hook anything else.
|
|
271
|
+
|
|
272
|
+
## Settings
|
|
273
|
+
|
|
274
|
+
All settings are optional. See [docs/settings.md](docs/settings.md) for the full reference.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
OVERSEER_DEFAULT_RETRIES = 0 # policy for tasks not declared with overseer.task
|
|
278
|
+
OVERSEER_DEFAULT_BACKOFF = "exponential"
|
|
279
|
+
OVERSEER_DEFAULT_BACKOFF_BASE = 30.0
|
|
280
|
+
OVERSEER_DEFAULT_BACKOFF_MAX = 3600.0
|
|
281
|
+
OVERSEER_DEFAULT_JITTER = True
|
|
282
|
+
OVERSEER_DEFAULT_TIMEOUT = None
|
|
283
|
+
OVERSEER_STALE_AFTER = 3600 # a running attempt with no timeout is abandoned after this
|
|
284
|
+
OVERSEER_WORKER_OFFLINE_AFTER = 120
|
|
285
|
+
OVERSEER_RETENTION_DAYS = 14
|
|
286
|
+
OVERSEER_METRICS_RETENTION_DAYS = 30
|
|
287
|
+
OVERSEER_RECORD_ARGS = True # False to keep task arguments out of the database
|
|
288
|
+
OVERSEER_MAX_TRACEBACK_CHARS = 20_000
|
|
289
|
+
OVERSEER_PERMISSION = "overseer.view_dashboard" # None: any staff user
|
|
290
|
+
OVERSEER_REFRESH_SECONDS = 5
|
|
291
|
+
OVERSEER_ALERT_WINDOW_MINUTES = 5
|
|
292
|
+
OVERSEER_ALERT_FAILURE_RATE = 0.25
|
|
293
|
+
OVERSEER_ALERT_WAIT_SECONDS = 60
|
|
294
|
+
OVERSEER_ALERT_QUEUE_DEPTH = 1000
|
|
295
|
+
OVERSEER_ALERT_COOLDOWN_MINUTES = 15
|
|
296
|
+
OVERSEER_NOTIFIERS = [] # dotted paths or callables taking an Alert
|
|
297
|
+
OVERSEER_ALERT_EMAILS = []
|
|
298
|
+
OVERSEER_SLACK_WEBHOOK_URL = None
|
|
299
|
+
OVERSEER_SCHEDULER_INTERVAL = 1.0
|
|
300
|
+
OVERSEER_RESCUE_INTERVAL = 30.0
|
|
301
|
+
OVERSEER_MAINTENANCE_INTERVAL = 60.0
|
|
302
|
+
OVERSEER_SCHEDULER_TIMEZONE = None # None: TIME_ZONE
|
|
303
|
+
OVERSEER_AUTODISCOVER = True # import <app>.tasks for every installed app
|
|
304
|
+
OVERSEER_TASK_MODULES = [] # extra modules to import at startup
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`python manage.py check` warns when a task declares retries on a backend that cannot defer
|
|
308
|
+
(`supports_defer` is false): retries then run immediately instead of after the backoff.
|
|
309
|
+
|
|
310
|
+
## How it works
|
|
311
|
+
|
|
312
|
+
See [docs/design.md](docs/design.md). In short: `task_enqueued`, `task_started` and
|
|
313
|
+
`task_finished` create and update `Job` (one logical unit of work), `Run` (one attempt) and
|
|
314
|
+
`Worker` rows. Failure handling happens in the `task_finished` receiver, inside the worker
|
|
315
|
+
process, so a retry is enqueued by the same process that saw the failure and is visible
|
|
316
|
+
immediately. Recorders never raise: a bug in Overseer cannot break your worker.
|
|
317
|
+
|
|
318
|
+
## Production notes
|
|
319
|
+
|
|
320
|
+
- Run `overseer_prune` daily (a schedule works: `@overseer.schedule("0 3 * * *")` on a task
|
|
321
|
+
that calls `overseer.prune.prune()`), or the tables grow forever.
|
|
322
|
+
- Put the dashboard behind your usual staff authentication; it is a normal Django app under
|
|
323
|
+
your `LOGIN_URL`.
|
|
324
|
+
- Set `OVERSEER_RECORD_ARGS = False` if task arguments may contain secrets or personal
|
|
325
|
+
data; tracebacks are still stored, capped at `OVERSEER_MAX_TRACEBACK_CHARS`.
|
|
326
|
+
- On SQLite with several processes (workers, the scheduler, the web app) writing to one
|
|
327
|
+
file, configure the database as Django recommends for concurrent writers:
|
|
328
|
+
`"OPTIONS": {"transaction_mode": "IMMEDIATE", "timeout": 20, "init_command": "PRAGMA
|
|
329
|
+
journal_mode=WAL;"}`. Without it a writer that already read in the same transaction can
|
|
330
|
+
fail with "database is locked" instead of waiting.
|
|
331
|
+
- `ENQUEUE_ON_COMMIT` is honoured: a task enqueued inside a transaction is recorded when the
|
|
332
|
+
transaction commits, and never if it rolls back.
|
|
333
|
+
|
|
334
|
+
## Development
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
pip install -e ".[dev]"
|
|
338
|
+
pytest # SQLite, in memory
|
|
339
|
+
OVERSEER_SQLITE_FILE=/tmp/o.sqlite3 pytest tests/test_integration.py # real subprocesses
|
|
340
|
+
pytest --ds=tests.settings_postgres # everything, including the concurrency tests
|
|
341
|
+
ruff check src tests && ruff format --check src tests
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The suite has four layers: unit tests for every module, integration tests that spawn real
|
|
345
|
+
`overseer_worker` and `overseer_scheduler` processes against a shared database,
|
|
346
|
+
end-to-end tests that drive the dashboard over HTTP with `requests`, and performance
|
|
347
|
+
guards that assert flat query counts and time budgets with thousands of rows.
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|