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.
Files changed (52) hide show
  1. django_database_task-0.4.0/CHANGELOG.md +65 -0
  2. {django_database_task-0.3.0 → django_database_task-0.4.0}/MANIFEST.in +1 -0
  3. {django_database_task-0.3.0/django_database_task.egg-info → django_database_task-0.4.0}/PKG-INFO +384 -2
  4. {django_database_task-0.3.0 → django_database_task-0.4.0}/README.md +381 -1
  5. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/__init__.py +1 -1
  6. django_database_task-0.4.0/django_database_task/auth.py +314 -0
  7. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/backends.py +179 -2
  8. django_database_task-0.4.0/django_database_task/brokers/__init__.py +10 -0
  9. django_database_task-0.4.0/django_database_task/brokers/base.py +172 -0
  10. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/__init__.py +9 -5
  11. django_database_task-0.4.0/django_database_task/cloudtasks/backend.py +75 -0
  12. django_database_task-0.4.0/django_database_task/cloudtasks/broker.py +180 -0
  13. django_database_task-0.4.0/django_database_task/management/commands/run_database_tasks.py +419 -0
  14. django_database_task-0.4.0/django_database_task/sqs/__init__.py +58 -0
  15. django_database_task-0.4.0/django_database_task/sqs/backend.py +40 -0
  16. django_database_task-0.4.0/django_database_task/sqs/broker.py +253 -0
  17. django_database_task-0.4.0/django_database_task/sqs/detection.py +35 -0
  18. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/views.py +111 -37
  19. {django_database_task-0.3.0 → django_database_task-0.4.0/django_database_task.egg-info}/PKG-INFO +384 -2
  20. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/SOURCES.txt +11 -0
  21. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/requires.txt +3 -0
  22. {django_database_task-0.3.0 → django_database_task-0.4.0}/pyproject.toml +9 -2
  23. django_database_task-0.4.0/tests/test_auth.py +380 -0
  24. django_database_task-0.4.0/tests/test_brokers.py +237 -0
  25. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_commands.py +381 -0
  26. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_views.py +355 -0
  27. django_database_task-0.3.0/django_database_task/cloudtasks/backend.py +0 -239
  28. django_database_task-0.3.0/django_database_task/management/commands/run_database_tasks.py +0 -197
  29. {django_database_task-0.3.0 → django_database_task-0.4.0}/LICENSE +0 -0
  30. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/admin.py +0 -0
  31. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/apps.py +0 -0
  32. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/auth.py +0 -0
  33. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/cloudtasks/detection.py +0 -0
  34. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/executor.py +0 -0
  35. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/locale/ja/LC_MESSAGES/django.mo +0 -0
  36. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/locale/ja/LC_MESSAGES/django.po +0 -0
  37. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/__init__.py +0 -0
  38. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/commands/__init__.py +0 -0
  39. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/management/commands/purge_completed_database_tasks.py +0 -0
  40. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/migrations/0001_initial.py +0 -0
  41. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/migrations/__init__.py +0 -0
  42. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/models.py +0 -0
  43. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/shutdown.py +0 -0
  44. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task/urls.py +0 -0
  45. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/dependency_links.txt +0 -0
  46. {django_database_task-0.3.0 → django_database_task-0.4.0}/django_database_task.egg-info/top_level.txt +0 -0
  47. {django_database_task-0.3.0 → django_database_task-0.4.0}/setup.cfg +0 -0
  48. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_admin.py +0 -0
  49. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_backend.py +0 -0
  50. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_executor.py +0 -0
  51. {django_database_task-0.3.0 → django_database_task-0.4.0}/tests/test_models.py +0 -0
  52. {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.
@@ -1,3 +1,4 @@
1
1
  include LICENSE
2
2
  include README.md
3
+ include CHANGELOG.md
3
4
  recursive-include django_database_task/locale *.po *.mo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-database-task
3
- Version: 0.3.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 the `/tasks/execute/` and `/tasks/purge/` endpoints.
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.