django-database-task 0.3.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.
- django_database_task-0.4.0/CHANGELOG.md +65 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/MANIFEST.in +1 -0
- {django_database_task-0.3.0/django_database_task.egg-info → django_database_task-0.4.0}/PKG-INFO +384 -2
- {django_database_task-0.3.0 → django_database_task-0.4.0}/README.md +381 -1
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/__init__.py +1 -1
- django_database_task-0.4.0/django_database_task/auth.py +314 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/backends.py +179 -2
- django_database_task-0.4.0/django_database_task/brokers/__init__.py +10 -0
- django_database_task-0.4.0/django_database_task/brokers/base.py +172 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/__init__.py +9 -5
- django_database_task-0.4.0/django_database_task/cloudtasks/backend.py +75 -0
- django_database_task-0.4.0/django_database_task/cloudtasks/broker.py +180 -0
- django_database_task-0.4.0/django_database_task/management/commands/run_database_tasks.py +419 -0
- django_database_task-0.4.0/django_database_task/sqs/__init__.py +58 -0
- django_database_task-0.4.0/django_database_task/sqs/backend.py +40 -0
- django_database_task-0.4.0/django_database_task/sqs/broker.py +253 -0
- django_database_task-0.4.0/django_database_task/sqs/detection.py +35 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/views.py +111 -37
- {django_database_task-0.3.0 → django_database_task-0.4.0/django_database_task.egg-info}/PKG-INFO +384 -2
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/SOURCES.txt +11 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/requires.txt +3 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/pyproject.toml +9 -2
- django_database_task-0.4.0/tests/test_auth.py +380 -0
- django_database_task-0.4.0/tests/test_brokers.py +237 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_commands.py +381 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_views.py +355 -0
- django_database_task-0.3.0/django_database_task/cloudtasks/backend.py +0 -239
- django_database_task-0.3.0/django_database_task/management/commands/run_database_tasks.py +0 -197
- {django_database_task-0.3.0 → django_database_task-0.4.0}/LICENSE +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/admin.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/apps.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/auth.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/detection.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/executor.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/__init__.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/commands/__init__.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/migrations/0001_initial.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/migrations/__init__.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/models.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/shutdown.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/urls.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/dependency_links.txt +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/top_level.txt +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/setup.cfg +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_admin.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_backend.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_executor.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_models.py +0 -0
- {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_shutdown.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Brokers — the services that trigger execution of a saved task — are now
|
|
6
|
+
separate from the task backend, and Amazon SQS joins Cloud Tasks as one of
|
|
7
|
+
them.
|
|
8
|
+
|
|
9
|
+
**Existing projects need no changes.** The settings, the URLs, the management
|
|
10
|
+
commands and their defaults all behave as they did in 0.3.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Amazon SQS broker** (`django_database_task.sqs.SQSDatabaseBackend`,
|
|
15
|
+
`pip install django-database-task[sqs]`). Sends a message naming the task,
|
|
16
|
+
and `run_database_tasks` receives it. A task deferred beyond the 15 minute
|
|
17
|
+
SQS delay limit stays in the database for the worker's database sweep.
|
|
18
|
+
- **`--source` for `run_database_tasks`**: `auto` (default), `db`, `broker` or
|
|
19
|
+
`both`. `auto` means `both` when the backend has a broker a worker can
|
|
20
|
+
receive from, and `db` otherwise, so the command is run the same way as
|
|
21
|
+
before either way. `--wait-time` and `--max-messages` go with it.
|
|
22
|
+
- **The broker abstraction** (`django_database_task.brokers`): `TaskBroker`,
|
|
23
|
+
`HTTPPushBroker` and `PullBroker`. A project attaches its own with the
|
|
24
|
+
`BROKER` option.
|
|
25
|
+
- **Several authentication handlers per backend**, through
|
|
26
|
+
`get_auth_handlers()`. A request is accepted as soon as one handler accepts
|
|
27
|
+
it, so the service that calls the endpoints and an external cron job can use
|
|
28
|
+
different credentials. Configure them with the `AUTH_HANDLERS` option, and
|
|
29
|
+
limit one to some endpoints with `ENDPOINTS`.
|
|
30
|
+
- **Bundled authentication handlers** in `django_database_task.auth`:
|
|
31
|
+
`SharedSecretAuth`, `HMACAuth` and `StaffOnlyAuth`, with `build_signature()`
|
|
32
|
+
for callers that have to sign a request for `HMACAuth`.
|
|
33
|
+
- **AWS environment detection** in `django_database_task.sqs`:
|
|
34
|
+
`detect_aws_region()`, `is_lambda()` and `is_ecs()`, alongside the existing
|
|
35
|
+
Cloud Tasks ones.
|
|
36
|
+
- The brokers themselves are importable, for a project that wants one on a
|
|
37
|
+
backend of its own: `django_database_task.sqs.SQSBroker` and
|
|
38
|
+
`django_database_task.cloudtasks.CloudTasksBroker`.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Enabling Cloud Tasks OIDC no longer locks every other caller out of the task
|
|
43
|
+
endpoints. Since 0.3.1 the OIDC handler was applied to all of them, so an
|
|
44
|
+
external cron job calling `/tasks/run/` or `/tasks/purge/` was rejected.
|
|
45
|
+
- The Cloud Tasks tests were skipped on every run, in CI included, because the
|
|
46
|
+
`cloudtasks` extra was never installed. Four of them had gone stale
|
|
47
|
+
unnoticed.
|
|
48
|
+
|
|
49
|
+
### Deprecated
|
|
50
|
+
|
|
51
|
+
- `get_auth_handler()` (singular). It still works, with a
|
|
52
|
+
`DeprecationWarning`, and is removed in 0.5. Override `get_auth_handlers()`
|
|
53
|
+
instead.
|
|
54
|
+
|
|
55
|
+
### Documentation
|
|
56
|
+
|
|
57
|
+
- A *Task Brokers* section in the README, and an Amazon SQS one with a
|
|
58
|
+
sequence diagram beside the ones the database backend and Cloud Tasks
|
|
59
|
+
already had.
|
|
60
|
+
- An SQS walkthrough in `examples/`, run against a local mock: set
|
|
61
|
+
`DEMO_BROKER=sqs` to point the demo project at it.
|
|
62
|
+
- `CONTRIBUTING.md`, covering the development setup, the tests, and how to add
|
|
63
|
+
a broker.
|
|
64
|
+
- This file. Releases before 0.4.0 are in the git history and the GitHub
|
|
65
|
+
releases.
|
{django_database_task-0.3.0/django_database_task.egg-info → django_database_task-0.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-database-task
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A database-backed task queue backend for Django's built-in task framework
|
|
5
5
|
Author-email: Shinya Okano <tokibito@gmail.com>
|
|
6
6
|
Maintainer-email: Shinya Okano <tokibito@gmail.com>
|
|
@@ -29,6 +29,8 @@ Requires-Dist: Django>=6.0
|
|
|
29
29
|
Provides-Extra: cloudtasks
|
|
30
30
|
Requires-Dist: google-cloud-tasks>=2.0.0; extra == "cloudtasks"
|
|
31
31
|
Requires-Dist: google-auth>=2.0.0; extra == "cloudtasks"
|
|
32
|
+
Provides-Extra: sqs
|
|
33
|
+
Requires-Dist: boto3>=1.28; extra == "sqs"
|
|
32
34
|
Provides-Extra: dev
|
|
33
35
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
36
|
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
@@ -113,6 +115,10 @@ The minimum database versions are the ones Django itself requires, and Django
|
|
|
113
115
|
|
|
114
116
|
```bash
|
|
115
117
|
pip install django-database-task
|
|
118
|
+
|
|
119
|
+
# With a broker (see Task Brokers)
|
|
120
|
+
pip install django-database-task[cloudtasks]
|
|
121
|
+
pip install django-database-task[sqs]
|
|
116
122
|
```
|
|
117
123
|
|
|
118
124
|
## Quick Start
|
|
@@ -282,12 +288,46 @@ python manage.py run_database_tasks [options]
|
|
|
282
288
|
| `--continuous` | Keep polling even when no tasks |
|
|
283
289
|
| `--interval` | Polling interval in seconds (default: 5) |
|
|
284
290
|
| `--max-tasks` | Maximum number of tasks to process (0=unlimited) |
|
|
291
|
+
| `--source` | Where to look for tasks: `auto` (default), `db`, `broker` or `both`. See [Task sources](#task-sources) |
|
|
292
|
+
| `--wait-time` | Seconds to wait for a broker message before looking again (default: 20) |
|
|
293
|
+
| `--max-messages` | Maximum number of broker messages to receive at a time (default: 1) |
|
|
285
294
|
| `--shutdown-timeout` | Maximum seconds to wait for the running task after `SIGTERM`/`SIGINT` before forcing exit (0=wait indefinitely, default: 0) |
|
|
286
295
|
| `--no-graceful-shutdown` | Do not install signal handlers (terminate immediately, even while a task is running) |
|
|
287
296
|
| `--verbosity` | Output level: `0` silent (errors only), `1` normal (default), `2` also print an idle heartbeat dot per poll |
|
|
288
297
|
|
|
289
298
|
See [Graceful Shutdown](#graceful-shutdown) for details.
|
|
290
299
|
|
|
300
|
+
#### Task sources
|
|
301
|
+
|
|
302
|
+
By default the worker polls the database, which is what it has always done.
|
|
303
|
+
When the backend has a [broker](#task-brokers) a worker can receive from — a
|
|
304
|
+
`PullBroker` — the same command also receives from it, without any change to
|
|
305
|
+
how the command is run.
|
|
306
|
+
|
|
307
|
+
| `--source` | Behaviour |
|
|
308
|
+
|------------|-----------|
|
|
309
|
+
| `auto` | `both` when the backend has a `PullBroker`, `db` otherwise. The default |
|
|
310
|
+
| `db` | Poll the database only. What the command did before 0.4 |
|
|
311
|
+
| `broker` | Receive from the broker only |
|
|
312
|
+
| `both` | Receive from the broker, and fall back to the database when it is empty |
|
|
313
|
+
|
|
314
|
+
`both` is the useful combination for a broker that cannot hold a task
|
|
315
|
+
indefinitely. A broker with a delivery delay limit — SQS caps it at 15 minutes
|
|
316
|
+
— cannot carry a task deferred further out than that, so those stay in the
|
|
317
|
+
database until they are due, and the database sweep is what picks them up. It
|
|
318
|
+
is also what recovers tasks the broker never accepted, since a broker failure
|
|
319
|
+
during `enqueue()` is logged and swallowed.
|
|
320
|
+
|
|
321
|
+
While receiving from a broker, `--wait-time` replaces `--interval` as the idle
|
|
322
|
+
wait: the broker's own wait for a message is the pause, so a message wakes the
|
|
323
|
+
worker as soon as it arrives. `SIGTERM` is still honoured — see
|
|
324
|
+
[Graceful Shutdown](#graceful-shutdown).
|
|
325
|
+
|
|
326
|
+
A message is acknowledged whenever redelivering it would not help: the task
|
|
327
|
+
ran (whether it succeeded or failed), it no longer exists, or another worker
|
|
328
|
+
already holds it. If the worker itself cannot run the task, the message is
|
|
329
|
+
returned to the broker instead, to be delivered again.
|
|
330
|
+
|
|
291
331
|
#### Output verbosity
|
|
292
332
|
|
|
293
333
|
At the default verbosity the worker only prints the startup banner and one
|
|
@@ -815,6 +855,186 @@ urlpatterns = [
|
|
|
815
855
|
]
|
|
816
856
|
```
|
|
817
857
|
|
|
858
|
+
### Backend authentication handlers
|
|
859
|
+
|
|
860
|
+
Instead of wrapping each view, the backend can supply authentication handlers
|
|
861
|
+
that every endpoint applies automatically. Configure them with the
|
|
862
|
+
`AUTH_HANDLERS` option:
|
|
863
|
+
|
|
864
|
+
```python
|
|
865
|
+
# settings.py
|
|
866
|
+
TASKS = {
|
|
867
|
+
"default": {
|
|
868
|
+
"BACKEND": "django_database_task.backends.DatabaseTaskBackend",
|
|
869
|
+
"OPTIONS": {
|
|
870
|
+
"AUTH_HANDLERS": [
|
|
871
|
+
"django_database_task.auth.SharedSecretAuth",
|
|
872
|
+
],
|
|
873
|
+
"AUTH_HANDLER_OPTIONS": {
|
|
874
|
+
# Read the token from settings.TASK_API_TOKEN
|
|
875
|
+
"TOKEN_SETTING": "TASK_API_TOKEN",
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
```bash
|
|
883
|
+
curl -X POST https://example.com/tasks/run/ \
|
|
884
|
+
-H "Authorization: Bearer $TASK_API_TOKEN"
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
**A request is accepted as soon as one handler accepts it.** This lets the
|
|
888
|
+
service that calls the endpoints (Cloud Tasks, for example) and an external
|
|
889
|
+
cron job authenticate differently on the same endpoint:
|
|
890
|
+
|
|
891
|
+
```python
|
|
892
|
+
TASKS = {
|
|
893
|
+
"default": {
|
|
894
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
895
|
+
"OPTIONS": {
|
|
896
|
+
# Cloud Tasks calls /tasks/execute/<id>/ with an OIDC token
|
|
897
|
+
"OIDC_SERVICE_ACCOUNT_EMAIL": "sa@my-project.iam.gserviceaccount.com",
|
|
898
|
+
# An external cron job calls /tasks/run/ with a shared secret
|
|
899
|
+
"AUTH_HANDLERS": [
|
|
900
|
+
{
|
|
901
|
+
"HANDLER": "django_database_task.auth.SharedSecretAuth",
|
|
902
|
+
"OPTIONS": {"TOKEN_SETTING": "TASK_CRON_TOKEN"},
|
|
903
|
+
"ENDPOINTS": ["run", "run_one", "status", "purge"],
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
`ENDPOINTS` limits a handler to some of the endpoints; omit it to apply the
|
|
912
|
+
handler everywhere. The valid names are `run`, `run_one`, `status`, `execute`
|
|
913
|
+
and `purge`.
|
|
914
|
+
|
|
915
|
+
#### Bundled handlers
|
|
916
|
+
|
|
917
|
+
| Handler | Description |
|
|
918
|
+
|---------|-------------|
|
|
919
|
+
| `SharedSecretAuth` | Compares a token in a header. Options: `TOKEN` / `TOKEN_SETTING` / `TOKEN_ENV`, `HEADER` (default `Authorization`), `SCHEME` (default `Bearer`) |
|
|
920
|
+
| `HMACAuth` | Verifies a signature with a timestamp, rejecting replays. Options: `SECRET` / `SECRET_SETTING` / `SECRET_ENV`, `HEADER` (default `X-Task-Signature`), `TIMESTAMP_HEADER` (default `X-Task-Timestamp`), `MAX_AGE` (default `300`), `ALGORITHM` (default `sha256`) |
|
|
921
|
+
| `StaffOnlyAuth` | Accepts a logged in staff user. Requires `AuthenticationMiddleware` |
|
|
922
|
+
|
|
923
|
+
Prefer `TOKEN_SETTING` / `TOKEN_ENV` over writing the secret into `OPTIONS`.
|
|
924
|
+
|
|
925
|
+
Callers sign a request for `HMACAuth` with `build_signature()`:
|
|
926
|
+
|
|
927
|
+
```python
|
|
928
|
+
import time
|
|
929
|
+
import requests
|
|
930
|
+
from django_database_task.auth import build_signature
|
|
931
|
+
|
|
932
|
+
timestamp = str(int(time.time()))
|
|
933
|
+
body = b'{"max_tasks": 10}'
|
|
934
|
+
signature = build_signature(SECRET, timestamp, "POST", "/tasks/run/", body)
|
|
935
|
+
|
|
936
|
+
requests.post(
|
|
937
|
+
"https://example.com/tasks/run/",
|
|
938
|
+
data=body,
|
|
939
|
+
headers={
|
|
940
|
+
"Content-Type": "application/json",
|
|
941
|
+
"X-Task-Signature": signature,
|
|
942
|
+
"X-Task-Timestamp": timestamp,
|
|
943
|
+
},
|
|
944
|
+
)
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
#### Custom handlers
|
|
948
|
+
|
|
949
|
+
A handler is any callable that takes a request and returns `None` to accept it
|
|
950
|
+
or a response to reject it. Put one in `AUTH_HANDLERS`, or override
|
|
951
|
+
`get_auth_handlers()` on a backend subclass:
|
|
952
|
+
|
|
953
|
+
```python
|
|
954
|
+
from django.http import JsonResponse
|
|
955
|
+
from django_database_task.backends import DatabaseTaskBackend
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def allow_internal_network(request):
|
|
959
|
+
if request.META.get("REMOTE_ADDR", "").startswith("10."):
|
|
960
|
+
return None
|
|
961
|
+
return JsonResponse({"error": "Forbidden"}, status=403)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class MyBackend(DatabaseTaskBackend):
|
|
965
|
+
def get_auth_handlers(self, endpoint=None):
|
|
966
|
+
return [allow_internal_network, *super().get_auth_handlers(endpoint)]
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
> **Deprecated:** the single-handler `get_auth_handler()` still works in 0.4
|
|
970
|
+
> but is removed in 0.5. Override `get_auth_handlers()` instead.
|
|
971
|
+
|
|
972
|
+
## Task Brokers
|
|
973
|
+
|
|
974
|
+
A broker notifies an external service whenever a task is saved, so that
|
|
975
|
+
service can trigger its execution. The database stays the source of truth: a
|
|
976
|
+
broker only ever carries a task id, never the arguments or the state.
|
|
977
|
+
|
|
978
|
+
Without a broker — the default — tasks are picked up by `run_database_tasks`
|
|
979
|
+
or the [HTTP endpoints](#http-endpoints-optional). With one, the two are still
|
|
980
|
+
available and become the **fallback when the broker is down**: a broker
|
|
981
|
+
failure is logged and the task is left `READY` in the database, so the next
|
|
982
|
+
worker run or endpoint call picks it up.
|
|
983
|
+
|
|
984
|
+
Two brokers are bundled. Each has a backend that attaches it, so naming the
|
|
985
|
+
backend is all a project has to do:
|
|
986
|
+
|
|
987
|
+
| Broker | Backend | Shape |
|
|
988
|
+
|--------|---------|-------|
|
|
989
|
+
| [Cloud Tasks](#google-cloud-tasks-integration) | `django_database_task.cloudtasks.CloudTasksDatabaseBackend` | Push: calls an [HTTP endpoint](#http-endpoints-optional) of your app |
|
|
990
|
+
| [Amazon SQS](#amazon-sqs-integration) | `django_database_task.sqs.SQSDatabaseBackend` | Pull: a worker receives from the queue |
|
|
991
|
+
|
|
992
|
+
```python
|
|
993
|
+
TASKS = {
|
|
994
|
+
"default": {
|
|
995
|
+
"BACKEND": "django_database_task.cloudtasks.CloudTasksDatabaseBackend",
|
|
996
|
+
},
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### Custom brokers
|
|
1001
|
+
|
|
1002
|
+
A project can attach its own broker to the plain backend with the `BROKER`
|
|
1003
|
+
option:
|
|
1004
|
+
|
|
1005
|
+
```python
|
|
1006
|
+
TASKS = {
|
|
1007
|
+
"default": {
|
|
1008
|
+
"BACKEND": "django_database_task.backends.DatabaseTaskBackend",
|
|
1009
|
+
"OPTIONS": {"BROKER": "myproject.brokers.MyBroker"},
|
|
1010
|
+
},
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
The broker receives the backend and its whole `OPTIONS` dict, so it decides
|
|
1015
|
+
which options it reads:
|
|
1016
|
+
|
|
1017
|
+
```python
|
|
1018
|
+
from django_database_task.brokers import HTTPPushBroker
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
class MyBroker(HTTPPushBroker):
|
|
1022
|
+
def enqueue(self, task_result):
|
|
1023
|
+
url = self.get_handler_url(task_result.id)
|
|
1024
|
+
queue = self.resolve_queue(task_result.task.queue_name)
|
|
1025
|
+
my_service.publish(queue, url)
|
|
1026
|
+
|
|
1027
|
+
def get_auth_handlers(self, endpoint=None):
|
|
1028
|
+
# Verify the credentials my_service sends back to the endpoints.
|
|
1029
|
+
return [verify_my_service]
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
| Base class | Use for |
|
|
1033
|
+
|------------|---------|
|
|
1034
|
+
| `TaskBroker` | Anything else |
|
|
1035
|
+
| `HTTPPushBroker` | Services that call an HTTP endpoint of your app (Cloud Tasks). Provides `get_handler_url()`, `TASK_HANDLER_URL` and `TASK_HANDLER_PATH` |
|
|
1036
|
+
| `PullBroker` | Services a worker polls. Defines `receive()`, `ack()` and `nack()` |
|
|
1037
|
+
|
|
818
1038
|
## Google Cloud Tasks Integration
|
|
819
1039
|
|
|
820
1040
|
For serverless environments like Google App Engine or Cloud Run, you can use the Cloud Tasks backend to automatically create Cloud Tasks when tasks are enqueued.
|
|
@@ -935,7 +1155,9 @@ TASKS = {
|
|
|
935
1155
|
|
|
936
1156
|
### OIDC Authentication
|
|
937
1157
|
|
|
938
|
-
When `OIDC_SERVICE_ACCOUNT_EMAIL` is configured, Cloud Tasks will send OIDC tokens with each request. The backend automatically verifies these tokens on
|
|
1158
|
+
When `OIDC_SERVICE_ACCOUNT_EMAIL` is configured, Cloud Tasks will send OIDC tokens with each request. The backend automatically verifies these tokens on every task endpoint.
|
|
1159
|
+
|
|
1160
|
+
To let another caller — an external cron job, for example — reach the endpoints with its own credentials, add handlers with the `AUTH_HANDLERS` option. A request is accepted as soon as one handler accepts it. See [Backend authentication handlers](#backend-authentication-handlers).
|
|
939
1161
|
|
|
940
1162
|
#### Required IAM Roles
|
|
941
1163
|
|
|
@@ -1024,6 +1246,161 @@ elif is_app_engine():
|
|
|
1024
1246
|
print(f"Running on App Engine in project {detect_gcp_project()}")
|
|
1025
1247
|
```
|
|
1026
1248
|
|
|
1249
|
+
## Amazon SQS Integration
|
|
1250
|
+
|
|
1251
|
+
Send a message to SQS whenever a task is saved, and let a worker receive those
|
|
1252
|
+
messages. Unlike Cloud Tasks, SQS is a **pull** broker: nothing calls your
|
|
1253
|
+
application, so there is no HTTP endpoint to expose and nothing to
|
|
1254
|
+
authenticate.
|
|
1255
|
+
|
|
1256
|
+
### Installation
|
|
1257
|
+
|
|
1258
|
+
```bash
|
|
1259
|
+
pip install django-database-task[sqs]
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### Quick Setup
|
|
1263
|
+
|
|
1264
|
+
```python
|
|
1265
|
+
# settings.py
|
|
1266
|
+
TASKS = {
|
|
1267
|
+
"default": {
|
|
1268
|
+
"BACKEND": "django_database_task.sqs.SQSDatabaseBackend",
|
|
1269
|
+
"QUEUES": [], # Allow all queue names
|
|
1270
|
+
},
|
|
1271
|
+
}
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
```bash
|
|
1275
|
+
python manage.py run_database_tasks --continuous
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
That is the same worker command as always. With an SQS broker configured it
|
|
1279
|
+
receives from the queue and sweeps the database, because `--source` defaults
|
|
1280
|
+
to `auto`. See [Task sources](#task-sources).
|
|
1281
|
+
|
|
1282
|
+
Credentials come from the usual boto3 chain: the instance or task role, the
|
|
1283
|
+
environment, or `~/.aws/credentials`. The task needs `sqs:SendMessage`,
|
|
1284
|
+
`sqs:ReceiveMessage`, `sqs:DeleteMessage`, `sqs:ChangeMessageVisibility` and,
|
|
1285
|
+
unless you set `SQS_QUEUE_URL_TEMPLATE`, `sqs:GetQueueUrl`.
|
|
1286
|
+
|
|
1287
|
+
### How It Works
|
|
1288
|
+
|
|
1289
|
+
```mermaid
|
|
1290
|
+
sequenceDiagram
|
|
1291
|
+
participant App as Application
|
|
1292
|
+
participant Backend as SQSDatabaseBackend
|
|
1293
|
+
participant DB as Database
|
|
1294
|
+
participant SQS as Amazon SQS
|
|
1295
|
+
participant Worker as Worker Process
|
|
1296
|
+
|
|
1297
|
+
Note over App,Worker: Task Enqueue
|
|
1298
|
+
App->>Backend: task.enqueue(args, kwargs)
|
|
1299
|
+
Backend->>DB: INSERT task (status=READY)
|
|
1300
|
+
DB-->>Backend: Task ID
|
|
1301
|
+
alt No run_after, or within 15 minutes
|
|
1302
|
+
Backend->>SQS: SendMessage (task_id only,<br/>DelaySeconds)
|
|
1303
|
+
SQS-->>Backend: MessageId
|
|
1304
|
+
else Deferred beyond the SQS delay limit
|
|
1305
|
+
Note over Backend,SQS: Not sent. The task waits in the<br/>database for the sweep below
|
|
1306
|
+
end
|
|
1307
|
+
Backend-->>App: TaskResult (id, status=READY)
|
|
1308
|
+
|
|
1309
|
+
Note over App,Worker: Task Execution (the worker receives)
|
|
1310
|
+
loop run_database_tasks --continuous
|
|
1311
|
+
Worker->>SQS: ReceiveMessage (long poll)
|
|
1312
|
+
alt A message is waiting
|
|
1313
|
+
SQS-->>Worker: task_id + ReceiptHandle
|
|
1314
|
+
Worker->>DB: SELECT FOR UPDATE SKIP LOCKED<br/>(id=task_id, status=READY)
|
|
1315
|
+
Worker->>DB: UPDATE status=RUNNING
|
|
1316
|
+
Worker->>Worker: Execute task function
|
|
1317
|
+
Worker->>DB: UPDATE status=SUCCESSFUL / FAILED
|
|
1318
|
+
Worker->>SQS: DeleteMessage (only now)
|
|
1319
|
+
else The queue is empty
|
|
1320
|
+
Worker->>DB: SELECT FOR UPDATE SKIP LOCKED<br/>(status=READY, run_after <= now)
|
|
1321
|
+
Worker->>Worker: Execute task function
|
|
1322
|
+
Worker->>DB: UPDATE status=SUCCESSFUL / FAILED
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
The message carries only the task id, the same as with Cloud Tasks. What that
|
|
1328
|
+
buys here:
|
|
1329
|
+
|
|
1330
|
+
- **The message is deleted after the task finishes**, not when it is received.
|
|
1331
|
+
A worker that dies mid-task leaves the message to reappear once the
|
|
1332
|
+
visibility timeout expires, so nothing is lost. Should it be delivered twice
|
|
1333
|
+
anyway, the `READY` check and the row lock mean only one worker runs it
|
|
1334
|
+
- **The database sweep is the other half of the worker.** It runs tasks SQS
|
|
1335
|
+
could not carry — anything deferred past 15 minutes — along with anything the
|
|
1336
|
+
broker never accepted, since a `SendMessage` failure is logged and swallowed
|
|
1337
|
+
rather than losing the task
|
|
1338
|
+
- **Nothing calls the application**, so there is no endpoint to expose and no
|
|
1339
|
+
credentials for SQS to present, unlike the push model Cloud Tasks uses
|
|
1340
|
+
|
|
1341
|
+
### Options
|
|
1342
|
+
|
|
1343
|
+
| Option | Description |
|
|
1344
|
+
|--------|-------------|
|
|
1345
|
+
| `AWS_REGION` | Region. Detected from `AWS_REGION` or `AWS_DEFAULT_REGION` when unset |
|
|
1346
|
+
| `SQS_QUEUE_URL_TEMPLATE` | Queue URL with a `{queue_name}` placeholder. Set it to skip the `GetQueueUrl` call |
|
|
1347
|
+
| `SQS_ENDPOINT_URL` | Endpoint override, for LocalStack |
|
|
1348
|
+
| `VISIBILITY_TIMEOUT` | Seconds a received message stays hidden. Leave unset to use the queue's own setting |
|
|
1349
|
+
| `MAX_DELAY_SECONDS` | Largest delay to put on a message (default: 900, the SQS limit) |
|
|
1350
|
+
|
|
1351
|
+
### Queues
|
|
1352
|
+
|
|
1353
|
+
The SQS queue name is the task's `queue_name` attribute, the same as with
|
|
1354
|
+
Cloud Tasks:
|
|
1355
|
+
|
|
1356
|
+
```python
|
|
1357
|
+
@task(queue_name="ranking")
|
|
1358
|
+
def rebuild_ranking(tenant_id):
|
|
1359
|
+
...
|
|
1360
|
+
# → sent to the "ranking" SQS queue
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
Run one worker per queue with `--queue`:
|
|
1364
|
+
|
|
1365
|
+
```bash
|
|
1366
|
+
python manage.py run_database_tasks --queue ranking --continuous
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
Use standard queues, not FIFO ones. Duplicate delivery is already handled by
|
|
1370
|
+
the task status and `SELECT FOR UPDATE SKIP LOCKED`, and ordering does not
|
|
1371
|
+
apply to independent tasks.
|
|
1372
|
+
|
|
1373
|
+
Set the queue's visibility timeout to more than your longest task, or a second
|
|
1374
|
+
worker will start the same task before the first one finishes. Attach a
|
|
1375
|
+
dead letter queue with a `maxReceiveCount` to catch messages that keep coming
|
|
1376
|
+
back.
|
|
1377
|
+
|
|
1378
|
+
### Deferred tasks
|
|
1379
|
+
|
|
1380
|
+
**SQS cannot hold a message for longer than 15 minutes.** A task deferred
|
|
1381
|
+
further out than that is not sent to the queue at all:
|
|
1382
|
+
|
|
1383
|
+
```python
|
|
1384
|
+
send_report.using(run_after=timezone.now() + timedelta(hours=3)).enqueue()
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
It stays `READY` in the database, and the database sweep the worker already
|
|
1388
|
+
performs runs it once it is due. This is why `--source` resolves to `both`
|
|
1389
|
+
rather than `broker`, and why the worker should be left running with
|
|
1390
|
+
`--continuous`. The same sweep recovers tasks SQS never accepted, since a
|
|
1391
|
+
broker failure during `enqueue()` is logged and swallowed.
|
|
1392
|
+
|
|
1393
|
+
### Serverless
|
|
1394
|
+
|
|
1395
|
+
On Lambda or App Runner, where no worker process can be kept running, put an
|
|
1396
|
+
HTTP push in front of the existing
|
|
1397
|
+
[`/tasks/execute/<task_id>/` endpoint](#http-endpoints-optional) instead —
|
|
1398
|
+
EventBridge Pipes with an SQS source and an API destination target needs no
|
|
1399
|
+
code of its own. Authenticate it with the
|
|
1400
|
+
[bundled handlers](#backend-authentication-handlers): an EventBridge
|
|
1401
|
+
connection sends an API key or basic credentials, which `SharedSecretAuth`
|
|
1402
|
+
verifies.
|
|
1403
|
+
|
|
1027
1404
|
## Django Admin
|
|
1028
1405
|
|
|
1029
1406
|
The package includes a Django Admin integration to view and manage tasks:
|
|
@@ -1047,6 +1424,11 @@ These actions are useful for:
|
|
|
1047
1424
|
- Retrying failed tasks after fixing issues
|
|
1048
1425
|
- Testing task execution during development
|
|
1049
1426
|
|
|
1427
|
+
## Contributing
|
|
1428
|
+
|
|
1429
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
1430
|
+
for how to set up a development environment, run the tests and add a broker.
|
|
1431
|
+
|
|
1050
1432
|
## License
|
|
1051
1433
|
|
|
1052
1434
|
MIT License - see [LICENSE](LICENSE) for details.
|