threadmill 0.2.0__tar.gz → 0.4.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.
- {threadmill-0.2.0 → threadmill-0.4.0}/PKG-INFO +109 -29
- threadmill-0.4.0/README.md +206 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/pyproject.toml +6 -5
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/_version.py +3 -3
- threadmill-0.4.0/threadmill/backends/base.py +248 -0
- threadmill-0.4.0/threadmill/backends/lua/acknowledge.lua +34 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/backends/lua/reaper.lua +6 -2
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/backends/redis.py +197 -25
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/exceptions.py +0 -2
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/executor.py +17 -4
- threadmill-0.4.0/threadmill/inspector/__init__.py +5 -0
- threadmill-0.4.0/threadmill/inspector/app.py +461 -0
- threadmill-0.4.0/threadmill/inspector/inspector.scss +106 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/management/commands/threadmill.py +57 -7
- threadmill-0.4.0/threadmill/retry.py +71 -0
- threadmill-0.2.0/README.md +0 -126
- threadmill-0.2.0/threadmill/backends/base.py +0 -145
- threadmill-0.2.0/threadmill/backends/lua/acknowledge.lua +0 -24
- {threadmill-0.2.0 → threadmill-0.4.0}/LICENSE +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/__init__.py +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/backends/__init__.py +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/backends/lua/acquire.lua +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/backends/lua/mover.lua +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/management/__init__.py +0 -0
- {threadmill-0.2.0 → threadmill-0.4.0}/threadmill/management/commands/__init__.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threadmill
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The most reliable backend for Django's task framework.
|
|
5
5
|
Keywords: Django,tasks,worker
|
|
6
6
|
Author-email: Johannes Maron <johannes@maron.family>
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
@@ -20,13 +20,12 @@ Classifier: Topic :: Software Development
|
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
21
|
Classifier: Programming Language :: Python :: 3
|
|
22
22
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
25
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
24
|
Classifier: Framework :: Django
|
|
27
25
|
Classifier: Framework :: Django :: 6.1
|
|
28
26
|
License-File: LICENSE
|
|
29
|
-
Requires-Dist: django>=6.
|
|
27
|
+
Requires-Dist: django>=6.0
|
|
28
|
+
Requires-Dist: textual>=8.2.7 ; extra == "inspector"
|
|
30
29
|
Requires-Dist: redis>=5.0 ; extra == "redis"
|
|
31
30
|
Project-URL: Changelog, https://github.com/codingjoe/threadmill/releases
|
|
32
31
|
Project-URL: Documentation, https://github.com/codingjoe/threadmill/
|
|
@@ -35,15 +34,14 @@ Project-URL: Homepage, https://github.com/codingjoe/threadmill
|
|
|
35
34
|
Project-URL: Issues, https://github.com/codingjoe/threadmill/issues
|
|
36
35
|
Project-URL: Releasenotes, https://github.com/codingjoe/threadmill/releases/latest
|
|
37
36
|
Project-URL: Source, https://github.com/codingjoe/threadmill
|
|
37
|
+
Provides-Extra: inspector
|
|
38
38
|
Provides-Extra: redis
|
|
39
39
|
|
|
40
|
-
# Threadmill
|
|
41
|
-
|
|
42
40
|
<p align="center">
|
|
43
41
|
<picture>
|
|
44
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/images/logo-dark.svg">
|
|
45
|
-
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/images/logo-light.svg">
|
|
46
|
-
<img alt="Threadmill:
|
|
42
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-dark.svg">
|
|
43
|
+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
|
|
44
|
+
<img alt="Threadmill: Durable high-performance backend for Django's task framework." src="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
|
|
47
45
|
</picture>
|
|
48
46
|
<br>
|
|
49
47
|
<a href="https://github.com/codingjoe/threadmill/">Documentation</a> |
|
|
@@ -52,24 +50,15 @@ Provides-Extra: redis
|
|
|
52
50
|
<a href="https://github.com/sponsors/codingjoe">Funding</a> 💚
|
|
53
51
|
</p>
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## Design Principles
|
|
58
|
-
|
|
59
|
-
- **Durability** – We recover from any failures, even poorly written tasks.
|
|
60
|
-
- **Consistency** – We never lose data, even if someone unplugs the power or network.
|
|
61
|
-
- **Utilization** – We keep the CPU saturated with tasks, not with idle time or waiting for locks.
|
|
62
|
-
|
|
63
|
-
> [!WARNING]
|
|
64
|
-
> Threadmill requires a development version of Django and is in a preview stage.
|
|
53
|
+
# Threadmill [](https://pypi.python.org/pypi/threadmill/) [](https://codecov.io/gh/codingjoe/threadmill) [](https://raw.githubusercontent.com/codingjoe/threadmill/master/LICENSE)
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
[](https://codecov.io/gh/codingjoe/threadmill)
|
|
68
|
-
[](https://raw.githubusercontent.com/codingjoe/threadmill/master/LICENSE)
|
|
55
|
+
**Durable high-performance backend for Django's task framework.**
|
|
69
56
|
|
|
70
|
-
##
|
|
57
|
+
## Design Principles
|
|
71
58
|
|
|
72
|
-
|
|
59
|
+
- **Durability** – Recover from any failures, even poorly written tasks.
|
|
60
|
+
- **Consistency** – Never lose data, even if someone unplugs the power or network.
|
|
61
|
+
- **Utilization** – Keep the CPU saturated with tasks, not with idle time or waiting for locks.
|
|
73
62
|
|
|
74
63
|
## Setup
|
|
75
64
|
|
|
@@ -100,10 +89,16 @@ TASKS = {
|
|
|
100
89
|
}
|
|
101
90
|
```
|
|
102
91
|
|
|
103
|
-
|
|
92
|
+
Optionally, install the inspector dependency if you want the TUI:
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
uv add threadmill[inspector]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then launch the worker pool:
|
|
104
99
|
|
|
105
100
|
```console
|
|
106
|
-
uv run manage.py threadmill
|
|
101
|
+
uv run manage.py threadmill worker
|
|
107
102
|
```
|
|
108
103
|
|
|
109
104
|
## Usage
|
|
@@ -118,7 +113,7 @@ Depending on your workload, you can tweak the number of processes and threads.
|
|
|
118
113
|
Processes allow for parallel compute (no GIL) while threads are great for low-memory concurrent IO.
|
|
119
114
|
|
|
120
115
|
```console
|
|
121
|
-
uv run manage.py threadmill --processes 4 --threads 2
|
|
116
|
+
uv run manage.py threadmill worker --processes 4 --threads 2
|
|
122
117
|
```
|
|
123
118
|
|
|
124
119
|
#### Health
|
|
@@ -126,7 +121,7 @@ uv run manage.py threadmill --processes 4 --threads 2
|
|
|
126
121
|
If your tasks leak memory, you can recycle (restart) the workers after a certain number of tasks have been processed:
|
|
127
122
|
|
|
128
123
|
```console
|
|
129
|
-
uv run manage.py threadmill --max-tasks 1000 --max-tasks-jitter 100
|
|
124
|
+
uv run manage.py threadmill worker --max-tasks 1000 --max-tasks-jitter 100
|
|
130
125
|
```
|
|
131
126
|
|
|
132
127
|
This will restart the workers after 1000 tasks have been processed, with a random jitter of up to 100 tasks to avoid all workers restarting at the same time.
|
|
@@ -141,6 +136,18 @@ All workers will finish the tasks they acquired and acknowledge them.
|
|
|
141
136
|
You can use `--exit-empty` to exit immediately after all tasks have been processed,
|
|
142
137
|
which might be useful for draining a one-off queue.
|
|
143
138
|
|
|
139
|
+
### Inspector
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
The optional TUI inspector lets you watch queues, tasks, and task details in real-time.
|
|
144
|
+
Install it with the `inspector` extra and launch it from a separate terminal:
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
uv add threadmill[inspector]
|
|
148
|
+
uv run manage.py threadmill inspector
|
|
149
|
+
```
|
|
150
|
+
|
|
144
151
|
### Redis Backend Options
|
|
145
152
|
|
|
146
153
|
The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
|
|
@@ -162,5 +169,78 @@ every multi-key operation — including the cross-queue acquire — runs on a si
|
|
|
162
169
|
shard. Scale horizontally by running additional backend aliases, not by relying
|
|
163
170
|
on cross-slot operations.
|
|
164
171
|
|
|
172
|
+
### Retrying failed tasks
|
|
173
|
+
|
|
174
|
+
Pass a `retry` callback to `@task()` to retry failed tasks with a delay.
|
|
175
|
+
The callback receives a `TaskContext` — use `context.attempt` for the current
|
|
176
|
+
attempt count and `context.task_result.errors[-1]` for the latest error.
|
|
177
|
+
Return a `timedelta` to schedule the next attempt, or `None` to stop retrying.
|
|
178
|
+
|
|
179
|
+
The worker re-queues the failed task, preserving its ID and error history;
|
|
180
|
+
the broker promotes it back to the ready queue once the delay elapses.
|
|
181
|
+
|
|
182
|
+
#### Built-in `ExponentialBackoff`
|
|
183
|
+
|
|
184
|
+
`threadmill.retry.ExponentialBackoff` provides a serializable exponential
|
|
185
|
+
backoff strategy out of the box. It caps the delay at `max_delay`, stops
|
|
186
|
+
after `max_retries` attempts, and only retries exceptions listed in
|
|
187
|
+
`expected_exceptions`.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import datetime
|
|
191
|
+
|
|
192
|
+
from django.tasks import task
|
|
193
|
+
from requests import HTTPError
|
|
194
|
+
|
|
195
|
+
from threadmill.retry import ExponentialBackoff
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@task(
|
|
199
|
+
retry=ExponentialBackoff(
|
|
200
|
+
base_delay=datetime.timedelta(seconds=1),
|
|
201
|
+
max_delay=datetime.timedelta(minutes=5),
|
|
202
|
+
factor=2.0,
|
|
203
|
+
max_retries=5,
|
|
204
|
+
expected_exceptions=(HTTPError,),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
def fetch_github_api(url: str): ...
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Custom retry callbacks
|
|
211
|
+
|
|
212
|
+
For cases that need logic beyond what `ExponentialBackoff` supports,
|
|
213
|
+
write a callable that accepts a `TaskContext` and returns a `timedelta`
|
|
214
|
+
or `None`. Use `TaskError.exception_class` to filter by exception type:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import datetime
|
|
218
|
+
|
|
219
|
+
from django.tasks import task
|
|
220
|
+
from django.tasks.base import TaskContext
|
|
221
|
+
from requests import HTTPError
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def retry_on_rate_limit(context: TaskContext) -> datetime.timedelta | None:
|
|
225
|
+
"""Retry HTTP 429 responses with exponential backoff, up to 5 attempts."""
|
|
226
|
+
if context.attempt >= 5:
|
|
227
|
+
return None
|
|
228
|
+
error = context.task_result.errors[-1]
|
|
229
|
+
if not issubclass(error.exception_class, HTTPError):
|
|
230
|
+
return None
|
|
231
|
+
return min(
|
|
232
|
+
datetime.timedelta(seconds=2**context.attempt),
|
|
233
|
+
datetime.timedelta(seconds=60),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@task(retry=retry_on_rate_limit)
|
|
238
|
+
def fetch_github_api(url: str): ...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Sponsors
|
|
242
|
+
|
|
243
|
+
[](https://github.com/sponsors/codingjoe)
|
|
244
|
+
|
|
165
245
|
[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/
|
|
166
246
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
|
|
5
|
+
<img alt="Threadmill: Durable high-performance backend for Django's task framework." src="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
|
|
6
|
+
</picture>
|
|
7
|
+
<br>
|
|
8
|
+
<a href="https://github.com/codingjoe/threadmill/">Documentation</a> |
|
|
9
|
+
<a href="https://github.com/codingjoe/threadmill/issues/new/choose">Issues</a> |
|
|
10
|
+
<a href="https://github.com/codingjoe/threadmill/releases">Changelog</a> |
|
|
11
|
+
<a href="https://github.com/sponsors/codingjoe">Funding</a> 💚
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
# Threadmill [](https://pypi.python.org/pypi/threadmill/) [](https://codecov.io/gh/codingjoe/threadmill) [](https://raw.githubusercontent.com/codingjoe/threadmill/master/LICENSE)
|
|
15
|
+
|
|
16
|
+
**Durable high-performance backend for Django's task framework.**
|
|
17
|
+
|
|
18
|
+
## Design Principles
|
|
19
|
+
|
|
20
|
+
- **Durability** – Recover from any failures, even poorly written tasks.
|
|
21
|
+
- **Consistency** – Never lose data, even if someone unplugs the power or network.
|
|
22
|
+
- **Utilization** – Keep the CPU saturated with tasks, not with idle time or waiting for locks.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
You need to have [Django's Task framework][django-tasks] set up properly.
|
|
27
|
+
|
|
28
|
+
```console
|
|
29
|
+
uv add threadmill[redis]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Add `threadmill` to your `INSTALLED_APPS` in `settings.py`
|
|
33
|
+
and configure the task backend:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# settings.py
|
|
37
|
+
import os
|
|
38
|
+
|
|
39
|
+
INSTALLED_APPS = [
|
|
40
|
+
"threadmill",
|
|
41
|
+
# ...
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
TASKS = {
|
|
45
|
+
"default": {
|
|
46
|
+
"BACKEND": "threadmill.backends.redis.RedisTaskBackend",
|
|
47
|
+
"REDIS_URL": os.getenv("REDIS_URL", "redis://localhost:6379/0"),
|
|
48
|
+
},
|
|
49
|
+
# ...
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Optionally, install the inspector dependency if you want the TUI:
|
|
54
|
+
|
|
55
|
+
```console
|
|
56
|
+
uv add threadmill[inspector]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then launch the worker pool:
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
uv run manage.py threadmill worker
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Workers
|
|
68
|
+
|
|
69
|
+
The workers are inspired by Gunicorn, and the CLI is very similar.
|
|
70
|
+
|
|
71
|
+
#### Utilization
|
|
72
|
+
|
|
73
|
+
Depending on your workload, you can tweak the number of processes and threads.
|
|
74
|
+
Processes allow for parallel compute (no GIL) while threads are great for low-memory concurrent IO.
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
uv run manage.py threadmill worker --processes 4 --threads 2
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Health
|
|
81
|
+
|
|
82
|
+
If your tasks leak memory, you can recycle (restart) the workers after a certain number of tasks have been processed:
|
|
83
|
+
|
|
84
|
+
```console
|
|
85
|
+
uv run manage.py threadmill worker --max-tasks 1000 --max-tasks-jitter 100
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This will restart the workers after 1000 tasks have been processed, with a random jitter of up to 100 tasks to avoid all workers restarting at the same time.
|
|
89
|
+
|
|
90
|
+
Should a worker crash or be killed, the pool will automatically restart it.
|
|
91
|
+
|
|
92
|
+
#### Shutdown
|
|
93
|
+
|
|
94
|
+
A graceful shutdown is possible with the `SIGTERM` or a keyboard interrupt.
|
|
95
|
+
All workers will finish the tasks they acquired and acknowledge them.
|
|
96
|
+
|
|
97
|
+
You can use `--exit-empty` to exit immediately after all tasks have been processed,
|
|
98
|
+
which might be useful for draining a one-off queue.
|
|
99
|
+
|
|
100
|
+
### Inspector
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
The optional TUI inspector lets you watch queues, tasks, and task details in real-time.
|
|
105
|
+
Install it with the `inspector` extra and launch it from a separate terminal:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
uv add threadmill[inspector]
|
|
109
|
+
uv run manage.py threadmill inspector
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Redis Backend Options
|
|
113
|
+
|
|
114
|
+
The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
|
|
115
|
+
`TASKS` configuration:
|
|
116
|
+
|
|
117
|
+
| Option | Default | Description |
|
|
118
|
+
| ----------------- | ---------------------- | ------------------------------------------------------------ |
|
|
119
|
+
| `lease_ttl` | `timedelta(hours=1)` | Max processing time before a started task is marked FAILED. |
|
|
120
|
+
| `result_ttl` | `timedelta(days=1)` | How long task results are retained before automatic removal. |
|
|
121
|
+
| `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
|
|
122
|
+
| `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
|
|
123
|
+
|
|
124
|
+
A task that is started but never acknowledged (lease expired) is marked FAILED
|
|
125
|
+
with an `AcknowledgementTimeout` error. Set `lease_ttl` comfortably above your
|
|
126
|
+
worst-case task runtime.
|
|
127
|
+
|
|
128
|
+
All keys for one backend alias share a Redis Cluster hash tag (`{alias}`), so
|
|
129
|
+
every multi-key operation — including the cross-queue acquire — runs on a single
|
|
130
|
+
shard. Scale horizontally by running additional backend aliases, not by relying
|
|
131
|
+
on cross-slot operations.
|
|
132
|
+
|
|
133
|
+
### Retrying failed tasks
|
|
134
|
+
|
|
135
|
+
Pass a `retry` callback to `@task()` to retry failed tasks with a delay.
|
|
136
|
+
The callback receives a `TaskContext` — use `context.attempt` for the current
|
|
137
|
+
attempt count and `context.task_result.errors[-1]` for the latest error.
|
|
138
|
+
Return a `timedelta` to schedule the next attempt, or `None` to stop retrying.
|
|
139
|
+
|
|
140
|
+
The worker re-queues the failed task, preserving its ID and error history;
|
|
141
|
+
the broker promotes it back to the ready queue once the delay elapses.
|
|
142
|
+
|
|
143
|
+
#### Built-in `ExponentialBackoff`
|
|
144
|
+
|
|
145
|
+
`threadmill.retry.ExponentialBackoff` provides a serializable exponential
|
|
146
|
+
backoff strategy out of the box. It caps the delay at `max_delay`, stops
|
|
147
|
+
after `max_retries` attempts, and only retries exceptions listed in
|
|
148
|
+
`expected_exceptions`.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import datetime
|
|
152
|
+
|
|
153
|
+
from django.tasks import task
|
|
154
|
+
from requests import HTTPError
|
|
155
|
+
|
|
156
|
+
from threadmill.retry import ExponentialBackoff
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@task(
|
|
160
|
+
retry=ExponentialBackoff(
|
|
161
|
+
base_delay=datetime.timedelta(seconds=1),
|
|
162
|
+
max_delay=datetime.timedelta(minutes=5),
|
|
163
|
+
factor=2.0,
|
|
164
|
+
max_retries=5,
|
|
165
|
+
expected_exceptions=(HTTPError,),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
def fetch_github_api(url: str): ...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Custom retry callbacks
|
|
172
|
+
|
|
173
|
+
For cases that need logic beyond what `ExponentialBackoff` supports,
|
|
174
|
+
write a callable that accepts a `TaskContext` and returns a `timedelta`
|
|
175
|
+
or `None`. Use `TaskError.exception_class` to filter by exception type:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import datetime
|
|
179
|
+
|
|
180
|
+
from django.tasks import task
|
|
181
|
+
from django.tasks.base import TaskContext
|
|
182
|
+
from requests import HTTPError
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def retry_on_rate_limit(context: TaskContext) -> datetime.timedelta | None:
|
|
186
|
+
"""Retry HTTP 429 responses with exponential backoff, up to 5 attempts."""
|
|
187
|
+
if context.attempt >= 5:
|
|
188
|
+
return None
|
|
189
|
+
error = context.task_result.errors[-1]
|
|
190
|
+
if not issubclass(error.exception_class, HTTPError):
|
|
191
|
+
return None
|
|
192
|
+
return min(
|
|
193
|
+
datetime.timedelta(seconds=2**context.attempt),
|
|
194
|
+
datetime.timedelta(seconds=60),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@task(retry=retry_on_rate_limit)
|
|
199
|
+
def fetch_github_api(url: str): ...
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Sponsors
|
|
203
|
+
|
|
204
|
+
[](https://github.com/sponsors/codingjoe)
|
|
205
|
+
|
|
206
|
+
[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/
|
|
@@ -26,17 +26,18 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python",
|
|
27
27
|
"Programming Language :: Python :: 3",
|
|
28
28
|
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
-
"Programming Language :: Python :: 3.12",
|
|
30
|
-
"Programming Language :: Python :: 3.13",
|
|
31
29
|
"Programming Language :: Python :: 3.14",
|
|
32
30
|
"Framework :: Django",
|
|
33
31
|
"Framework :: Django :: 6.1",
|
|
34
32
|
]
|
|
35
|
-
requires-python = ">=3.
|
|
36
|
-
dependencies = ["django>=6.
|
|
33
|
+
requires-python = ">=3.14"
|
|
34
|
+
dependencies = ["django>=6.0"]
|
|
37
35
|
|
|
38
36
|
[project.optional-dependencies]
|
|
39
37
|
redis = ["redis>=5.0"]
|
|
38
|
+
inspector = [
|
|
39
|
+
"textual>=8.2.7",
|
|
40
|
+
]
|
|
40
41
|
|
|
41
42
|
[project.urls]
|
|
42
43
|
# https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels
|
|
@@ -55,7 +56,7 @@ name = "threadmill"
|
|
|
55
56
|
write_to = "threadmill/_version.py"
|
|
56
57
|
|
|
57
58
|
[tool.pytest.ini_options]
|
|
58
|
-
minversion = "6.
|
|
59
|
+
minversion = "6.1a1"
|
|
59
60
|
addopts = "--cov --cov-report=xml --cov-report=term --tb=short -rxs --benchmark-autosave --benchmark-group-by=fullname --benchmark-min-rounds=10"
|
|
60
61
|
testpaths = ["tests"]
|
|
61
62
|
DJANGO_SETTINGS_MODULE = "tests.testapp.settings"
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gf9749f870'
|