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.
Files changed (86) hide show
  1. django_overseer-0.1.0/.gitignore +12 -0
  2. django_overseer-0.1.0/CHANGELOG.md +27 -0
  3. django_overseer-0.1.0/LICENSE +21 -0
  4. django_overseer-0.1.0/PKG-INFO +351 -0
  5. django_overseer-0.1.0/README.md +304 -0
  6. django_overseer-0.1.0/docs/design.md +99 -0
  7. django_overseer-0.1.0/docs/settings.md +88 -0
  8. django_overseer-0.1.0/pyproject.toml +98 -0
  9. django_overseer-0.1.0/src/overseer/__init__.py +15 -0
  10. django_overseer-0.1.0/src/overseer/adapters/__init__.py +0 -0
  11. django_overseer-0.1.0/src/overseer/adapters/base.py +54 -0
  12. django_overseer-0.1.0/src/overseer/adapters/django_tasks_db.py +49 -0
  13. django_overseer-0.1.0/src/overseer/alerts.py +182 -0
  14. django_overseer-0.1.0/src/overseer/api.py +210 -0
  15. django_overseer-0.1.0/src/overseer/apps.py +21 -0
  16. django_overseer-0.1.0/src/overseer/checks.py +49 -0
  17. django_overseer-0.1.0/src/overseer/conf.py +48 -0
  18. django_overseer-0.1.0/src/overseer/decorators.py +192 -0
  19. django_overseer-0.1.0/src/overseer/exceptions.py +10 -0
  20. django_overseer-0.1.0/src/overseer/management/__init__.py +0 -0
  21. django_overseer-0.1.0/src/overseer/management/commands/__init__.py +0 -0
  22. django_overseer-0.1.0/src/overseer/management/commands/overseer_alerts.py +13 -0
  23. django_overseer-0.1.0/src/overseer/management/commands/overseer_prune.py +19 -0
  24. django_overseer-0.1.0/src/overseer/management/commands/overseer_rescue.py +11 -0
  25. django_overseer-0.1.0/src/overseer/management/commands/overseer_rollup.py +16 -0
  26. django_overseer-0.1.0/src/overseer/management/commands/overseer_scheduler.py +24 -0
  27. django_overseer-0.1.0/src/overseer/management/commands/overseer_sync_schedules.py +14 -0
  28. django_overseer-0.1.0/src/overseer/management/commands/overseer_worker.py +67 -0
  29. django_overseer-0.1.0/src/overseer/metrics.py +155 -0
  30. django_overseer-0.1.0/src/overseer/migrations/0001_initial.py +378 -0
  31. django_overseer-0.1.0/src/overseer/migrations/__init__.py +0 -0
  32. django_overseer-0.1.0/src/overseer/models.py +266 -0
  33. django_overseer-0.1.0/src/overseer/prune.py +31 -0
  34. django_overseer-0.1.0/src/overseer/recorders.py +231 -0
  35. django_overseer-0.1.0/src/overseer/registry.py +61 -0
  36. django_overseer-0.1.0/src/overseer/rescue.py +71 -0
  37. django_overseer-0.1.0/src/overseer/retry.py +115 -0
  38. django_overseer-0.1.0/src/overseer/scheduling/__init__.py +0 -0
  39. django_overseer-0.1.0/src/overseer/scheduling/cron.py +150 -0
  40. django_overseer-0.1.0/src/overseer/scheduling/registry.py +40 -0
  41. django_overseer-0.1.0/src/overseer/scheduling/scheduler.py +179 -0
  42. django_overseer-0.1.0/src/overseer/scheduling/sync.py +77 -0
  43. django_overseer-0.1.0/src/overseer/signals.py +10 -0
  44. django_overseer-0.1.0/src/overseer/static/overseer/overseer.css +66 -0
  45. django_overseer-0.1.0/src/overseer/static/overseer/overseer.js +45 -0
  46. django_overseer-0.1.0/src/overseer/stats.py +246 -0
  47. django_overseer-0.1.0/src/overseer/templates/overseer/_queues_table.html +15 -0
  48. django_overseer-0.1.0/src/overseer/templates/overseer/_window.html +6 -0
  49. django_overseer-0.1.0/src/overseer/templates/overseer/alerts.html +15 -0
  50. django_overseer-0.1.0/src/overseer/templates/overseer/base.html +36 -0
  51. django_overseer-0.1.0/src/overseer/templates/overseer/failed.html +37 -0
  52. django_overseer-0.1.0/src/overseer/templates/overseer/job_detail.html +52 -0
  53. django_overseer-0.1.0/src/overseer/templates/overseer/jobs.html +37 -0
  54. django_overseer-0.1.0/src/overseer/templates/overseer/metrics.html +34 -0
  55. django_overseer-0.1.0/src/overseer/templates/overseer/overview.html +50 -0
  56. django_overseer-0.1.0/src/overseer/templates/overseer/queues.html +7 -0
  57. django_overseer-0.1.0/src/overseer/templates/overseer/schedules.html +28 -0
  58. django_overseer-0.1.0/src/overseer/templates/overseer/tasks.html +21 -0
  59. django_overseer-0.1.0/src/overseer/templates/overseer/workers.html +23 -0
  60. django_overseer-0.1.0/src/overseer/templatetags/__init__.py +0 -0
  61. django_overseer-0.1.0/src/overseer/templatetags/overseer_extras.py +64 -0
  62. django_overseer-0.1.0/src/overseer/urls.py +37 -0
  63. django_overseer-0.1.0/src/overseer/views.py +342 -0
  64. django_overseer-0.1.0/src/overseer/worker.py +97 -0
  65. django_overseer-0.1.0/tests/__init__.py +0 -0
  66. django_overseer-0.1.0/tests/conftest.py +72 -0
  67. django_overseer-0.1.0/tests/fresh_project.sh +132 -0
  68. django_overseer-0.1.0/tests/procs.py +115 -0
  69. django_overseer-0.1.0/tests/settings.py +82 -0
  70. django_overseer-0.1.0/tests/settings_postgres.py +19 -0
  71. django_overseer-0.1.0/tests/tasks.py +81 -0
  72. django_overseer-0.1.0/tests/test_alerts.py +198 -0
  73. django_overseer-0.1.0/tests/test_concurrency.py +120 -0
  74. django_overseer-0.1.0/tests/test_cron.py +74 -0
  75. django_overseer-0.1.0/tests/test_dashboard.py +329 -0
  76. django_overseer-0.1.0/tests/test_decorators.py +73 -0
  77. django_overseer-0.1.0/tests/test_e2e.py +218 -0
  78. django_overseer-0.1.0/tests/test_integration.py +219 -0
  79. django_overseer-0.1.0/tests/test_metrics.py +174 -0
  80. django_overseer-0.1.0/tests/test_perf.py +183 -0
  81. django_overseer-0.1.0/tests/test_recorders.py +119 -0
  82. django_overseer-0.1.0/tests/test_rescue.py +128 -0
  83. django_overseer-0.1.0/tests/test_retry.py +144 -0
  84. django_overseer-0.1.0/tests/test_scheduler.py +281 -0
  85. django_overseer-0.1.0/tests/test_worker.py +104 -0
  86. django_overseer-0.1.0/tests/urls.py +7 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[co]
3
+ *.egg-info/
4
+ /dist/
5
+ /build/
6
+ /.coverage*
7
+ /htmlcov/
8
+ /.pytest_cache/
9
+ /.ruff_cache/
10
+ /site/
11
+ .DS_Store
12
+ /docs/_build/
@@ -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