django-maintenance-tasks 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_maintenance_tasks-0.1.0/.github/workflows/publish.yml +44 -0
- django_maintenance_tasks-0.1.0/.gitignore +7 -0
- django_maintenance_tasks-0.1.0/LICENSE +21 -0
- django_maintenance_tasks-0.1.0/PKG-INFO +176 -0
- django_maintenance_tasks-0.1.0/README.md +158 -0
- django_maintenance_tasks-0.1.0/example/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/example/blog/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/example/blog/maintenance_tasks.py +35 -0
- django_maintenance_tasks-0.1.0/example/blog/migrations/0001_initial.py +22 -0
- django_maintenance_tasks-0.1.0/example/blog/migrations/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/example/blog/models.py +6 -0
- django_maintenance_tasks-0.1.0/example/e2e.py +270 -0
- django_maintenance_tasks-0.1.0/example/manage.py +11 -0
- django_maintenance_tasks-0.1.0/example/settings.py +66 -0
- django_maintenance_tasks-0.1.0/example/urls.py +4 -0
- django_maintenance_tasks-0.1.0/pyproject.toml +32 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/__init__.py +3 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/admin.py +125 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/apps.py +13 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/management/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/management/commands/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/management/commands/maintenance_tasks.py +50 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/migrations/0001_initial.py +39 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/migrations/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/models.py +78 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/runner.py +266 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/task.py +72 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/templates/admin/maintenance_tasks/run/change_list.html +6 -0
- django_maintenance_tasks-0.1.0/src/maintenance_tasks/templates/admin/maintenance_tasks/run/start.html +20 -0
- django_maintenance_tasks-0.1.0/tests/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/tests/settings.py +38 -0
- django_maintenance_tasks-0.1.0/tests/test_runner.py +265 -0
- django_maintenance_tasks-0.1.0/tests/testapp/__init__.py +0 -0
- django_maintenance_tasks-0.1.0/tests/testapp/maintenance_tasks.py +73 -0
- django_maintenance_tasks-0.1.0/tests/testapp/models.py +6 -0
- django_maintenance_tasks-0.1.0/tests/urls.py +4 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.13"
|
|
15
|
+
- run: pip install -e ".[dev]"
|
|
16
|
+
- run: pytest
|
|
17
|
+
|
|
18
|
+
build:
|
|
19
|
+
needs: test
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.13"
|
|
26
|
+
- run: pip install build
|
|
27
|
+
- run: python -m build
|
|
28
|
+
- uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write # trusted publishing, no API token needed
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ajinkya Pisal
|
|
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,176 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-maintenance-tasks
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pausable, resumable, throttled data backfills for Django, built on django.tasks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ajinkyapisal/django-maintenance-tasks
|
|
6
|
+
Project-URL: Issues, https://github.com/ajinkyapisal/django-maintenance-tasks/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Django :: 6.0
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: django>=6.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-django; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# django-maintenance-tasks
|
|
20
|
+
|
|
21
|
+
Pausable, resumable, throttled data backfills for Django, inspired by Shopify's
|
|
22
|
+
[`maintenance_tasks`](https://github.com/Shopify/maintenance_tasks) for Rails.
|
|
23
|
+
|
|
24
|
+
Backfilling millions of rows from a management command or a `RunPython`
|
|
25
|
+
migration is risky: it can lock tables, slow the site down, and if it crashes
|
|
26
|
+
halfway it starts again from zero. This package turns each backfill into a small
|
|
27
|
+
class that runs in the background in batches, saves its position as it goes, and
|
|
28
|
+
can be started, paused, resumed and cancelled from the Django admin.
|
|
29
|
+
|
|
30
|
+
It runs on Django's built-in [Tasks framework](https://docs.djangoproject.com/en/stable/topics/tasks/),
|
|
31
|
+
so it works with any task backend: the database backend from `django-tasks`, RQ,
|
|
32
|
+
Celery and others.
|
|
33
|
+
|
|
34
|
+
> Status: early scaffold. The API may change.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires Python 3.12+ and Django 6.0+. Not on PyPI yet, so install from GitHub:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install git+https://github.com/ajinkyapisal/django-maintenance-tasks
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
INSTALLED_APPS = [
|
|
46
|
+
# ...
|
|
47
|
+
"maintenance_tasks",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Any django.tasks backend that runs tasks in a worker.
|
|
51
|
+
TASKS = {"default": {"BACKEND": "..."}}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python manage.py migrate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Write a task
|
|
59
|
+
|
|
60
|
+
Put tasks in `<app>/maintenance_tasks.py`. They are discovered automatically.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from django.utils.text import slugify
|
|
64
|
+
|
|
65
|
+
from maintenance_tasks import MaintenanceTask
|
|
66
|
+
from blog.models import Post
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BackfillSlugs(MaintenanceTask):
|
|
70
|
+
description = "Fill in missing post slugs."
|
|
71
|
+
batch_size = 500
|
|
72
|
+
|
|
73
|
+
def collection(self):
|
|
74
|
+
return Post.objects.filter(slug="")
|
|
75
|
+
|
|
76
|
+
def process(self, post):
|
|
77
|
+
post.slug = slugify(post.title)
|
|
78
|
+
post.save(update_fields=["slug"])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `collection()` returns a QuerySet, a list or another iterable. QuerySets are
|
|
82
|
+
walked in primary key order. Other iterables are walked by position, so they
|
|
83
|
+
must return the same items in the same order every time.
|
|
84
|
+
- `process(item)` handles one item. It must be safe to run twice on the same
|
|
85
|
+
item: after a crash, up to one batch can be processed again.
|
|
86
|
+
- `self.arguments` holds the JSON arguments the run was started with.
|
|
87
|
+
- `count()` gives the total for the progress bar. Override it if counting is
|
|
88
|
+
slow.
|
|
89
|
+
- `throttle()` returns `True` to back off for `throttle_backoff` seconds, for
|
|
90
|
+
example when database replicas are lagging.
|
|
91
|
+
|
|
92
|
+
## Run it
|
|
93
|
+
|
|
94
|
+
From the admin: **Maintenance tasks → Runs → Start a run**. Select runs to
|
|
95
|
+
pause, resume or cancel them. Starting and controlling runs needs the
|
|
96
|
+
`maintenance_tasks.add_run` permission.
|
|
97
|
+
|
|
98
|
+
From the command line:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python manage.py maintenance_tasks list
|
|
102
|
+
python manage.py maintenance_tasks run blog.maintenance_tasks.BackfillSlugs --arguments '{"batch": "2024-q1"}'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
From code:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from maintenance_tasks import runner
|
|
109
|
+
|
|
110
|
+
run = runner.start("blog.maintenance_tasks.BackfillSlugs")
|
|
111
|
+
runner.pause(run)
|
|
112
|
+
runner.resume(run)
|
|
113
|
+
runner.cancel(run)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
- Each run is a `Run` row with a status, a cursor and progress counts. Only one
|
|
119
|
+
active run per task is allowed.
|
|
120
|
+
- A run is processed by a `django.tasks` job. After every batch it saves the
|
|
121
|
+
cursor and checks whether a pause or cancel was requested.
|
|
122
|
+
- A job gives up its worker after `MAX_RUNTIME` seconds (default 120) and
|
|
123
|
+
enqueues the next job, so long backfills never block a worker for hours:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
MAINTENANCE_TASKS = {"MAX_RUNTIME": 120, "STALE_AFTER": 600}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- If a worker dies mid-run (killed, out of memory, machine lost), the run stays
|
|
130
|
+
`running` with an old heartbeat. `recover_stalled()` puts it back on the
|
|
131
|
+
queue from its last checkpoint. Run it from cron, or use the admin action:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python manage.py maintenance_tasks recover
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A run counts as stalled after `STALE_AFTER` seconds without a heartbeat
|
|
138
|
+
(default 600). Keep it well above the time one batch takes.
|
|
139
|
+
- Every job carries a generation number. Recovering a run starts a new
|
|
140
|
+
generation, so if the old worker was only frozen and comes back, it stops at
|
|
141
|
+
its next checkpoint instead of running alongside the new one. Duplicate
|
|
142
|
+
deliveries of the same job are ignored too.
|
|
143
|
+
- If `process()` raises, the run is marked `errored` with the exception and
|
|
144
|
+
traceback, and the cursor points at the last item that succeeded.
|
|
145
|
+
- Throttle backoff uses `run_after` when the backend supports deferred tasks.
|
|
146
|
+
Otherwise the worker sleeps for the backoff time.
|
|
147
|
+
|
|
148
|
+
## Roadmap
|
|
149
|
+
|
|
150
|
+
- CSV upload as a collection
|
|
151
|
+
- Typed task parameters with a generated admin form
|
|
152
|
+
- Dry runs
|
|
153
|
+
- Retrying errored runs from the cursor
|
|
154
|
+
- Live progress in the admin
|
|
155
|
+
- Run history: who started what, with which arguments
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python3.13 -m venv .venv
|
|
161
|
+
.venv/bin/pip install -e ".[dev]"
|
|
162
|
+
.venv/bin/pytest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`example/` is a demo project on Postgres with the `django-tasks-db` backend.
|
|
166
|
+
`example/e2e.py` runs real `db_worker` processes and checks pausing,
|
|
167
|
+
cancelling, throttling, errors, graceful shutdown, killed workers and frozen
|
|
168
|
+
workers:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
docker run -d --name dmt-pg -e POSTGRES_HOST_AUTH_METHOD=trust \
|
|
172
|
+
-e POSTGRES_DB=dmt_demo -p 55432:5432 postgres:17-alpine
|
|
173
|
+
.venv/bin/pip install django-tasks-db "psycopg[binary]"
|
|
174
|
+
.venv/bin/python example/manage.py migrate
|
|
175
|
+
.venv/bin/python example/e2e.py
|
|
176
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# django-maintenance-tasks
|
|
2
|
+
|
|
3
|
+
Pausable, resumable, throttled data backfills for Django, inspired by Shopify's
|
|
4
|
+
[`maintenance_tasks`](https://github.com/Shopify/maintenance_tasks) for Rails.
|
|
5
|
+
|
|
6
|
+
Backfilling millions of rows from a management command or a `RunPython`
|
|
7
|
+
migration is risky: it can lock tables, slow the site down, and if it crashes
|
|
8
|
+
halfway it starts again from zero. This package turns each backfill into a small
|
|
9
|
+
class that runs in the background in batches, saves its position as it goes, and
|
|
10
|
+
can be started, paused, resumed and cancelled from the Django admin.
|
|
11
|
+
|
|
12
|
+
It runs on Django's built-in [Tasks framework](https://docs.djangoproject.com/en/stable/topics/tasks/),
|
|
13
|
+
so it works with any task backend: the database backend from `django-tasks`, RQ,
|
|
14
|
+
Celery and others.
|
|
15
|
+
|
|
16
|
+
> Status: early scaffold. The API may change.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
Requires Python 3.12+ and Django 6.0+. Not on PyPI yet, so install from GitHub:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install git+https://github.com/ajinkyapisal/django-maintenance-tasks
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
INSTALLED_APPS = [
|
|
28
|
+
# ...
|
|
29
|
+
"maintenance_tasks",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Any django.tasks backend that runs tasks in a worker.
|
|
33
|
+
TASKS = {"default": {"BACKEND": "..."}}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python manage.py migrate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Write a task
|
|
41
|
+
|
|
42
|
+
Put tasks in `<app>/maintenance_tasks.py`. They are discovered automatically.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from django.utils.text import slugify
|
|
46
|
+
|
|
47
|
+
from maintenance_tasks import MaintenanceTask
|
|
48
|
+
from blog.models import Post
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BackfillSlugs(MaintenanceTask):
|
|
52
|
+
description = "Fill in missing post slugs."
|
|
53
|
+
batch_size = 500
|
|
54
|
+
|
|
55
|
+
def collection(self):
|
|
56
|
+
return Post.objects.filter(slug="")
|
|
57
|
+
|
|
58
|
+
def process(self, post):
|
|
59
|
+
post.slug = slugify(post.title)
|
|
60
|
+
post.save(update_fields=["slug"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `collection()` returns a QuerySet, a list or another iterable. QuerySets are
|
|
64
|
+
walked in primary key order. Other iterables are walked by position, so they
|
|
65
|
+
must return the same items in the same order every time.
|
|
66
|
+
- `process(item)` handles one item. It must be safe to run twice on the same
|
|
67
|
+
item: after a crash, up to one batch can be processed again.
|
|
68
|
+
- `self.arguments` holds the JSON arguments the run was started with.
|
|
69
|
+
- `count()` gives the total for the progress bar. Override it if counting is
|
|
70
|
+
slow.
|
|
71
|
+
- `throttle()` returns `True` to back off for `throttle_backoff` seconds, for
|
|
72
|
+
example when database replicas are lagging.
|
|
73
|
+
|
|
74
|
+
## Run it
|
|
75
|
+
|
|
76
|
+
From the admin: **Maintenance tasks → Runs → Start a run**. Select runs to
|
|
77
|
+
pause, resume or cancel them. Starting and controlling runs needs the
|
|
78
|
+
`maintenance_tasks.add_run` permission.
|
|
79
|
+
|
|
80
|
+
From the command line:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python manage.py maintenance_tasks list
|
|
84
|
+
python manage.py maintenance_tasks run blog.maintenance_tasks.BackfillSlugs --arguments '{"batch": "2024-q1"}'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
From code:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from maintenance_tasks import runner
|
|
91
|
+
|
|
92
|
+
run = runner.start("blog.maintenance_tasks.BackfillSlugs")
|
|
93
|
+
runner.pause(run)
|
|
94
|
+
runner.resume(run)
|
|
95
|
+
runner.cancel(run)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## How it works
|
|
99
|
+
|
|
100
|
+
- Each run is a `Run` row with a status, a cursor and progress counts. Only one
|
|
101
|
+
active run per task is allowed.
|
|
102
|
+
- A run is processed by a `django.tasks` job. After every batch it saves the
|
|
103
|
+
cursor and checks whether a pause or cancel was requested.
|
|
104
|
+
- A job gives up its worker after `MAX_RUNTIME` seconds (default 120) and
|
|
105
|
+
enqueues the next job, so long backfills never block a worker for hours:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
MAINTENANCE_TASKS = {"MAX_RUNTIME": 120, "STALE_AFTER": 600}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- If a worker dies mid-run (killed, out of memory, machine lost), the run stays
|
|
112
|
+
`running` with an old heartbeat. `recover_stalled()` puts it back on the
|
|
113
|
+
queue from its last checkpoint. Run it from cron, or use the admin action:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python manage.py maintenance_tasks recover
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A run counts as stalled after `STALE_AFTER` seconds without a heartbeat
|
|
120
|
+
(default 600). Keep it well above the time one batch takes.
|
|
121
|
+
- Every job carries a generation number. Recovering a run starts a new
|
|
122
|
+
generation, so if the old worker was only frozen and comes back, it stops at
|
|
123
|
+
its next checkpoint instead of running alongside the new one. Duplicate
|
|
124
|
+
deliveries of the same job are ignored too.
|
|
125
|
+
- If `process()` raises, the run is marked `errored` with the exception and
|
|
126
|
+
traceback, and the cursor points at the last item that succeeded.
|
|
127
|
+
- Throttle backoff uses `run_after` when the backend supports deferred tasks.
|
|
128
|
+
Otherwise the worker sleeps for the backoff time.
|
|
129
|
+
|
|
130
|
+
## Roadmap
|
|
131
|
+
|
|
132
|
+
- CSV upload as a collection
|
|
133
|
+
- Typed task parameters with a generated admin form
|
|
134
|
+
- Dry runs
|
|
135
|
+
- Retrying errored runs from the cursor
|
|
136
|
+
- Live progress in the admin
|
|
137
|
+
- Run history: who started what, with which arguments
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3.13 -m venv .venv
|
|
143
|
+
.venv/bin/pip install -e ".[dev]"
|
|
144
|
+
.venv/bin/pytest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`example/` is a demo project on Postgres with the `django-tasks-db` backend.
|
|
148
|
+
`example/e2e.py` runs real `db_worker` processes and checks pausing,
|
|
149
|
+
cancelling, throttling, errors, graceful shutdown, killed workers and frozen
|
|
150
|
+
workers:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
docker run -d --name dmt-pg -e POSTGRES_HOST_AUTH_METHOD=trust \
|
|
154
|
+
-e POSTGRES_DB=dmt_demo -p 55432:5432 postgres:17-alpine
|
|
155
|
+
.venv/bin/pip install django-tasks-db "psycopg[binary]"
|
|
156
|
+
.venv/bin/python example/manage.py migrate
|
|
157
|
+
.venv/bin/python example/e2e.py
|
|
158
|
+
```
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from django.utils.text import slugify
|
|
5
|
+
|
|
6
|
+
from example.blog.models import Post
|
|
7
|
+
from maintenance_tasks import MaintenanceTask
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BackfillSlugs(MaintenanceTask):
|
|
11
|
+
"""Fills in missing slugs.
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
delay: seconds to sleep per post, to make the run slow enough to watch.
|
|
15
|
+
throttle_file: while this file exists, the task backs off.
|
|
16
|
+
fail_at: raise when processing the post with this title.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
description = "Fill in missing post slugs."
|
|
20
|
+
batch_size = 100
|
|
21
|
+
throttle_backoff = 2
|
|
22
|
+
|
|
23
|
+
def collection(self):
|
|
24
|
+
return Post.objects.filter(slug="")
|
|
25
|
+
|
|
26
|
+
def process(self, post):
|
|
27
|
+
if post.title == self.arguments.get("fail_at"):
|
|
28
|
+
raise ValueError(f"cannot slugify {post.title!r}")
|
|
29
|
+
time.sleep(self.arguments.get("delay", 0))
|
|
30
|
+
post.slug = slugify(post.title)
|
|
31
|
+
post.save(update_fields=["slug"])
|
|
32
|
+
|
|
33
|
+
def throttle(self):
|
|
34
|
+
path = self.arguments.get("throttle_file")
|
|
35
|
+
return bool(path) and Path(path).exists()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 6.1.1 on 2026-10-03 08:21
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='Post',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('title', models.CharField(max_length=200)),
|
|
19
|
+
('slug', models.SlugField(blank=True)),
|
|
20
|
+
],
|
|
21
|
+
),
|
|
22
|
+
]
|
|
File without changes
|