django-lambda-tasks 0.5.0__tar.gz → 0.6.1__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_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/steering/product.md +38 -10
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/steering/structure.md +11 -11
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/PKG-INFO +80 -17
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/README.md +79 -16
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_project/settings.py +3 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/admin.py +10 -2
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/decorators.py +25 -60
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/handler.py +34 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/models.py +26 -9
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/settings.py +55 -4
- django_lambda_tasks-0.6.1/lambda_tasks/tasks.py +78 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/pyproject.toml +1 -1
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/settings.py +4 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_admin.py +1 -0
- django_lambda_tasks-0.6.1/tests/test_batch.py +216 -0
- django_lambda_tasks-0.6.1/tests/test_batch_worker.py +68 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_decorator.py +46 -70
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_local_executor.py +7 -7
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_models.py +40 -40
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_settings.py +111 -7
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_timeout_validation.py +68 -9
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_timeouts.py +1 -0
- django_lambda_tasks-0.5.0/lambda_tasks/tasks.py +0 -23
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.gitignore +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/README.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/example/manage.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.1}/tests/test_tasks.py +0 -0
|
@@ -4,37 +4,58 @@ inclusion: always
|
|
|
4
4
|
|
|
5
5
|
# django-lambda-tasks
|
|
6
6
|
|
|
7
|
-
A Django library for offloading tasks to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda worker.
|
|
7
|
+
A Django library for offloading tasks to AWS Lambda or AWS Batch outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda worker or Batch container.
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
11
|
+
### Lambda tasks
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLambdaTaskMessage.execute_immediately() → TaskRecord
|
|
13
15
|
```
|
|
14
16
|
|
|
17
|
+
### Batch tasks (via queue config)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
View → @lambda_task(queue="heavy").execute_on_commit() → SQS → Lambda handler → submit_batch_job → batch.submit_job()
|
|
21
|
+
→ Container → python -m lambda_tasks.handler → handler() → execute_immediately() → TaskRecord
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Retry path (Batch):
|
|
25
|
+
```
|
|
26
|
+
Container (retry_on exception) → execute_on_commit() → SQS → Lambda → submit_batch_job → new Batch job
|
|
27
|
+
```
|
|
28
|
+
|
|
15
29
|
Key modules:
|
|
16
|
-
- `decorators.py` — `@lambda_task` decorator
|
|
17
|
-
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish or local pool submit)
|
|
30
|
+
- `decorators.py` — `LambdaTaskWrapper` and `@lambda_task` decorator factory
|
|
31
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish, Batch submit, or local pool submit)
|
|
18
32
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution for development
|
|
19
|
-
- `handler.py` — AWS Lambda entry point
|
|
33
|
+
- `handler.py` — AWS Lambda entry point + AWS Batch container entry point (`main()`)
|
|
20
34
|
- `logging.py` — `task_logger` for invocation-scoped log output
|
|
21
|
-
- `settings.py` — lazy `LambdaTasksSettings` reading from Django settings
|
|
35
|
+
- `settings.py` — lazy `LambdaTasksSettings` reading from Django settings, queue type helpers
|
|
22
36
|
|
|
23
37
|
## Task Definition
|
|
24
38
|
|
|
25
39
|
```python
|
|
26
|
-
#
|
|
40
|
+
# Lambda task — max timeout determined by queue type (900s for SQS, 3600s for Batch)
|
|
27
41
|
@lambda_task(retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
|
|
28
42
|
retry_on=(TransientError,),
|
|
29
43
|
singleton=True, retry_singleton=True)
|
|
30
44
|
def my_task(*, user_id: int, action: str) -> None:
|
|
31
45
|
...
|
|
46
|
+
|
|
47
|
+
# Batch task — same decorator, just routed to a Batch queue
|
|
48
|
+
@lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500,
|
|
49
|
+
retry_on=(TransientError,))
|
|
50
|
+
def heavy_task(*, file_id: int) -> None:
|
|
51
|
+
...
|
|
32
52
|
```
|
|
33
53
|
|
|
34
54
|
- Tasks are resolved at execution time via `import_string` on the fully-qualified task name
|
|
35
55
|
- Task files should be named `tasks.py` within the Django app
|
|
36
56
|
- Direct call `my_task(user_id=1, action="x")` runs synchronously
|
|
37
57
|
- `my_task.execute_on_commit(user_id=1, action="x")` enqueues after transaction commit
|
|
58
|
+
- The execution backend (Lambda vs Batch) is determined by the queue configuration, not the decorator
|
|
38
59
|
|
|
39
60
|
## Enqueuing
|
|
40
61
|
|
|
@@ -109,7 +130,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
109
130
|
`SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py`:
|
|
110
131
|
1. Checks for an existing `TaskRecord` with the same `pk` (`message_id`) via `get_or_create`
|
|
111
132
|
2. If a record already exists (any status), logs and returns immediately — duplicate deliveries are silently skipped
|
|
112
|
-
3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s)
|
|
133
|
+
3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s), validated against queue max (900 for SQS, 3600 for Batch)
|
|
113
134
|
4. If `wrapper.singleton` is `True`, acquires a Redis lock via `caches[SINGLETON_CACHE].lock(lock_key)` wrapping the atomic block; if `False`, no lock is acquired
|
|
114
135
|
5. Runs task inside `transaction.atomic()` + `TimeoutContext`
|
|
115
136
|
6. On success: updates record to `SUCCESS` with result and `end_time`
|
|
@@ -132,6 +153,12 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
132
153
|
- A temporary `StreamHandler` is attached to the `lambda_tasks` logger for the duration of the loaders so their log output is visible before Django's `LOGGING` dictConfig has run; it is removed immediately after so that Django's configuration is the sole authority on logging from that point on
|
|
133
154
|
- `resource.setrlimit(RLIMIT_DATA)` is set from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` so that excessive allocation raises `MemoryError` instead of triggering the OOM killer
|
|
134
155
|
|
|
156
|
+
`main()` in `handler.py` — AWS Batch container entry point (`python -m lambda_tasks.handler`):
|
|
157
|
+
- Reads `LAMBDA_TASKS_MESSAGE` env var (the serialized task JSON, set by `submit_batch_job` via container overrides)
|
|
158
|
+
- Uses `AWS_BATCH_JOB_ID` as the `message_id` (falls back to UUID4 if not set)
|
|
159
|
+
- Constructs a synthetic SQS event and delegates to `handler()` — reuses the full cold-start and execution path
|
|
160
|
+
- Returns exit code 0 on success, 1 on failure
|
|
161
|
+
|
|
135
162
|
## Environment Loader
|
|
136
163
|
|
|
137
164
|
`resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
|
|
@@ -169,7 +196,7 @@ Behaviour:
|
|
|
169
196
|
|
|
170
197
|
- `TaskRecord` — Django ORM model; persists task execution state with statuses `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
171
198
|
- `SQSLambdaTaskMessage` — Pydantic model; the SQS message schema; `execute_immediately()` runs the task inside `transaction.atomic()` + `TimeoutContext`
|
|
172
|
-
- `SQSLambdaTask` — Pydantic model; holds a `SQSLambdaTaskMessage` plus routing fields (`delay`, `queue`); `_execute()`
|
|
199
|
+
- `SQSLambdaTask` — Pydantic model; holds a `SQSLambdaTaskMessage` plus routing fields (`delay`, `queue`); `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, or execute eagerly/locally; `execute_on_commit()` registers `_execute` with `transaction.on_commit`
|
|
173
200
|
|
|
174
201
|
## TaskRecord Model
|
|
175
202
|
|
|
@@ -183,7 +210,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
|
183
210
|
|
|
184
211
|
| Setting | Default | Description |
|
|
185
212
|
|---|---|---|
|
|
186
|
-
| `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name:
|
|
213
|
+
| `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name: queue_config}`. Each value is a dict with either `queue_url` (SQS) or `job_queue_arn`+`job_definition_arn` (Batch). Must include `"default"` which must be SQS. |
|
|
187
214
|
| `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `270` | Soft timeout in seconds |
|
|
188
215
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `300` | Hard timeout in seconds |
|
|
189
216
|
| `LAMBDA_TASKS_EAGER` | `False` | Run tasks synchronously in-process (no SQS) |
|
|
@@ -191,7 +218,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
|
191
218
|
| `LAMBDA_TASKS_MAX_RETRIES` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (60 × 24 × 2) |
|
|
192
219
|
| `LAMBDA_TASKS_SINGLETON_CACHE` | `"default"` | Django cache backend used for singleton task locks |
|
|
193
220
|
|
|
194
|
-
`LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. Both timeout values must be greater than zero and
|
|
221
|
+
`LAMBDA_TASKS_QUEUES` must be set and include a `"default"` key. The `"default"` queue must be an SQS queue. Both timeout values must be greater than zero and `soft_timeout` must always be strictly less than `hard_timeout`. The maximum allowed timeout depends on the queue type: 900 for SQS, 3600 for Batch.
|
|
195
222
|
|
|
196
223
|
## Eager Mode
|
|
197
224
|
|
|
@@ -261,6 +288,7 @@ def my_task(*, user_id: int) -> None:
|
|
|
261
288
|
`lambda_tasks.tasks` provides maintenance tasks that ship with the library:
|
|
262
289
|
|
|
263
290
|
- `cleanup_task_records(*, retention_days: int = 7) -> int` — deletes `TaskRecord` rows whose `start_time` is strictly older than `retention_days`. Returns the number of deleted rows. Decorated with `@lambda_task` so it can be enqueued via `cleanup_task_records.execute_on_commit()` or called directly. Users are responsible for scheduling (e.g. EventBridge rule, cron, management command).
|
|
291
|
+
- `submit_batch_job(*, message_json: str, batch_queue: str) -> str` — submits a batch task to AWS Batch. Reads `job_queue_arn` and `job_definition_arn` from `LAMBDA_TASKS_QUEUES[batch_queue]`, sanitizes the task name for use as `jobName`, and calls `batch.submit_job()` with the message passed as a `LAMBDA_TASKS_MESSAGE` environment variable override. Returns the Batch job ID. This task is enqueued automatically by `SQSLambdaTask._execute()` when the queue is a Batch queue — users do not call it directly.
|
|
264
292
|
|
|
265
293
|
## Conventions
|
|
266
294
|
|
|
@@ -12,22 +12,22 @@ django-lambda-tasks/
|
|
|
12
12
|
│ ├── __init__.py
|
|
13
13
|
│ ├── admin.py # Django admin for TaskRecord
|
|
14
14
|
│ ├── apps.py # Django AppConfig
|
|
15
|
-
│ ├── decorators.py # @lambda_task
|
|
16
|
-
│ ├── handler.py # AWS Lambda entry point
|
|
15
|
+
│ ├── decorators.py # LambdaTaskWrapper, @lambda_task
|
|
16
|
+
│ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
|
|
17
17
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
18
18
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
19
|
-
│ ├── settings.py # LambdaTasksSettings
|
|
19
|
+
│ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
|
|
20
20
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
21
21
|
│ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
|
|
22
22
|
│ ├── local_executor.py # ProcessPoolExecutor for async local task execution
|
|
23
|
-
│ ├── tasks.py # Built-in
|
|
23
|
+
│ ├── tasks.py # Built-in tasks (cleanup_task_records, submit_batch_job)
|
|
24
24
|
│ ├── timeouts.py # TimeoutContext implementation
|
|
25
25
|
│ └── migrations/ # Django migrations for TaskRecord
|
|
26
26
|
├── tests/ # pytest test suite
|
|
27
27
|
│ ├── conftest.py
|
|
28
28
|
│ ├── settings.py # Django settings for test environment
|
|
29
29
|
│ └── test_*.py # One test file per module
|
|
30
|
-
├── example/ # Runnable Django project (
|
|
30
|
+
├── example/ # Runnable Django project (LOCAL_WORKERS mode, no AWS needed)
|
|
31
31
|
│ ├── example_app/ # Sample app with a task definition and trigger view
|
|
32
32
|
│ ├── example_project/ # Django settings, URLs, WSGI
|
|
33
33
|
│ ├── manage.py
|
|
@@ -39,23 +39,23 @@ django-lambda-tasks/
|
|
|
39
39
|
|
|
40
40
|
## Module Responsibilities
|
|
41
41
|
|
|
42
|
-
- `decorators.py` — defines `@lambda_task
|
|
43
|
-
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()`
|
|
42
|
+
- `decorators.py` — defines `LambdaTaskWrapper` and `@lambda_task` decorator factory; enforces kwargs-only at decoration time; validates timeouts (> 0, soft < hard) at decoration time; upper-bound timeout validation deferred to execution time based on queue type
|
|
43
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, execute eagerly, or submit to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
44
44
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
|
|
45
|
-
- `handler.py` — Lambda entry point; cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
|
|
45
|
+
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
|
|
46
46
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
47
47
|
- `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
|
|
48
48
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
49
|
-
- `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
|
|
49
|
+
- `settings.py` — constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `_is_sqs_queue()` and `_is_batch_queue()` helpers; `LambdaTasksSettings` instantiated fresh per use (reads live Django settings); `queue_max_timeout()` method returns 900 or 3600 based on queue type
|
|
50
50
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
51
51
|
- `apps.py` — Django `AppConfig`; `ready()` installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
52
|
-
- `tasks.py` — built-in
|
|
52
|
+
- `tasks.py` — built-in tasks; `cleanup_task_records` deletes old `TaskRecord` rows; `submit_batch_job` receives a serialized task message and submits it to AWS Batch via `batch.submit_job()`
|
|
53
53
|
|
|
54
54
|
## Conventions
|
|
55
55
|
|
|
56
56
|
- Task definitions belong in `tasks.py` within each Django app
|
|
57
57
|
- All task functions must use keyword-only arguments — positional args raise `TypeError` at decoration time
|
|
58
|
-
- Task logic must be self-contained; no Django request context is available in Lambda
|
|
58
|
+
- Task logic must be self-contained; no Django request context is available in Lambda or Batch
|
|
59
59
|
- New modules go inside `lambda_tasks/`; add a corresponding `tests/test_<module>.py`
|
|
60
60
|
- Tests for `models.py` (including `SQSLambdaTask._execute` and `SQSLambdaTask.execute_on_commit`) live in `tests/test_models.py`
|
|
61
61
|
- Do not add application-level code to `main.py` — it is a placeholder only
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -12,7 +12,7 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
|
|
13
13
|
# Django Lambda Tasks
|
|
14
14
|
|
|
15
|
-
A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler
|
|
15
|
+
A Django library for offloading work to AWS Lambda or AWS Batch outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler or Batch container. Task results, status, and metadata are persisted in the Django database.
|
|
16
16
|
|
|
17
17
|
> **Platform note:** Unix-based systems only (Linux, macOS). Timeout enforcement relies on `SIGALRM`.
|
|
18
18
|
|
|
@@ -86,7 +86,7 @@ See [AWS Lambda and SQS setup](#aws-lambda-and-sqs-setup) for queue and Lambda c
|
|
|
86
86
|
|
|
87
87
|
## Example app
|
|
88
88
|
|
|
89
|
-
A runnable Django project is included in the [`example/`](example/) directory. It uses `
|
|
89
|
+
A runnable Django project is included in the [`example/`](example/) directory. It uses `LAMBDA_TASKS_LOCAL_WORKERS = 2` so no AWS infrastructure is needed.
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
cd example
|
|
@@ -109,16 +109,25 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
109
109
|
|
|
110
110
|
| Setting | Type | Description |
|
|
111
111
|
|---|---|---|
|
|
112
|
-
| `LAMBDA_TASKS_QUEUES` | `dict[str,
|
|
112
|
+
| `LAMBDA_TASKS_QUEUES` | `dict[str, dict]` | Map of queue name → queue configuration. Must include a `"default"` key. The `"default"` queue must be an SQS queue. |
|
|
113
|
+
|
|
114
|
+
Each queue value is a dict. The queue type is determined by the keys present:
|
|
115
|
+
|
|
116
|
+
- **SQS queue:** `{"queue_url": "https://sqs.../queue-name"}` — tasks execute on Lambda (max timeout 900s)
|
|
117
|
+
- **Batch queue:** `{"job_queue_arn": "arn:...", "job_definition_arn": "arn:..."}` — tasks execute on AWS Batch (max timeout 3600s)
|
|
113
118
|
|
|
114
119
|
```python
|
|
115
120
|
LAMBDA_TASKS_QUEUES = {
|
|
116
|
-
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
117
|
-
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
121
|
+
"default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
|
|
122
|
+
"high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
|
|
123
|
+
"heavy": {
|
|
124
|
+
"job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
125
|
+
"job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
126
|
+
},
|
|
118
127
|
}
|
|
119
128
|
```
|
|
120
129
|
|
|
121
|
-
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
130
|
+
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use. The `"default"` queue must be an SQS queue.
|
|
122
131
|
|
|
123
132
|
### Timeout settings
|
|
124
133
|
|
|
@@ -127,7 +136,7 @@ LAMBDA_TASKS_QUEUES = {
|
|
|
127
136
|
| `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `int` | `270` | Seconds before `SoftTimeLimitExceeded` is raised inside the task. |
|
|
128
137
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `int` | `300` | Seconds before the task is forcibly terminated. |
|
|
129
138
|
|
|
130
|
-
Both values must be
|
|
139
|
+
Both values must be greater than zero and `soft_timeout` must always be strictly less than `hard_timeout`. The maximum allowed value depends on the queue type: 900 seconds for SQS queues (Lambda's 15-minute limit), 3600 seconds for Batch queues (1 hour). Upper bound validation happens at execution time based on the task's queue.
|
|
131
140
|
|
|
132
141
|
```python
|
|
133
142
|
LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
|
|
@@ -217,9 +226,9 @@ def my_task(*, arg: str) -> None:
|
|
|
217
226
|
|
|
218
227
|
| Parameter | Type | Default | Description |
|
|
219
228
|
|---|---|---|---|
|
|
220
|
-
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (
|
|
221
|
-
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (
|
|
222
|
-
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
229
|
+
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
|
|
230
|
+
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
|
|
231
|
+
| `queue` | `str` | `"default"` | Named queue to route this task to. Determines both execution backend and timeout limits. |
|
|
223
232
|
| `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
|
|
224
233
|
| `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
|
|
225
234
|
| `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
|
|
@@ -298,26 +307,46 @@ Timeout resolution order (first non-`None` value wins):
|
|
|
298
307
|
1. Per-task decorator default (`soft_timeout` / `hard_timeout` on `@lambda_task`)
|
|
299
308
|
2. Global settings (`LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` / `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT`)
|
|
300
309
|
|
|
310
|
+
The maximum allowed timeout is determined by the queue type at execution time: 900 seconds for SQS queues, 3600 seconds for Batch queues.
|
|
311
|
+
|
|
301
312
|
---
|
|
302
313
|
|
|
303
314
|
## Named queues
|
|
304
315
|
|
|
305
|
-
Route tasks to
|
|
316
|
+
Route tasks to different execution backends by assigning them to named queues:
|
|
306
317
|
|
|
307
318
|
```python
|
|
308
319
|
# settings.py
|
|
309
320
|
LAMBDA_TASKS_QUEUES = {
|
|
310
|
-
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
311
|
-
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
321
|
+
"default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
|
|
322
|
+
"high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
|
|
323
|
+
"heavy": {
|
|
324
|
+
"job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
325
|
+
"job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
326
|
+
},
|
|
312
327
|
}
|
|
313
328
|
```
|
|
314
329
|
|
|
315
330
|
```python
|
|
331
|
+
# Runs on Lambda with more memory
|
|
316
332
|
@lambda_task(queue="high_memory")
|
|
317
|
-
def
|
|
333
|
+
def process_images(*, batch_id: int) -> None:
|
|
334
|
+
...
|
|
335
|
+
|
|
336
|
+
# Runs on AWS Batch (up to 1 hour, large disk)
|
|
337
|
+
@lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
|
|
338
|
+
def process_large_file(*, file_id: int) -> str:
|
|
318
339
|
...
|
|
319
340
|
```
|
|
320
341
|
|
|
342
|
+
Tasks routed to a Batch queue are automatically submitted to AWS Batch via the built-in `submit_batch_job` task. The flow is:
|
|
343
|
+
|
|
344
|
+
1. `execute_on_commit()` enqueues a message to the default SQS queue
|
|
345
|
+
2. The Lambda handler picks up the message and runs `submit_batch_job`
|
|
346
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
347
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
348
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
349
|
+
|
|
321
350
|
---
|
|
322
351
|
|
|
323
352
|
## Task results
|
|
@@ -453,10 +482,11 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
|
|
|
453
482
|
|---|---|---|
|
|
454
483
|
| Task function has positional parameters | `TypeError` | At decoration time |
|
|
455
484
|
| `soft_timeout >= hard_timeout` | `ValueError` | At decoration time |
|
|
456
|
-
| Timeout value exceeds 900
|
|
485
|
+
| Timeout value exceeds queue max (900 or 3600) | `ValueError` | At execution time (timeout resolution) |
|
|
457
486
|
| Unknown queue name | `ImproperlyConfigured` | At `.execute_on_commit()` |
|
|
458
487
|
| `LAMBDA_TASKS_QUEUES` not set | `ImproperlyConfigured` | On first settings access |
|
|
459
488
|
| `LAMBDA_TASKS_QUEUES` missing `"default"` | `ImproperlyConfigured` | On first settings access |
|
|
489
|
+
| `"default"` queue is not SQS | `ImproperlyConfigured` | On first settings access |
|
|
460
490
|
| SQS `send_message` failure | boto3 exception (propagated) | At `.execute_on_commit()` |
|
|
461
491
|
|
|
462
492
|
---
|
|
@@ -465,7 +495,7 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
|
|
|
465
495
|
|
|
466
496
|
### SQS queue configuration
|
|
467
497
|
|
|
468
|
-
Each queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
|
|
498
|
+
Each SQS queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
|
|
469
499
|
|
|
470
500
|
- **Visibility timeout** — set this higher than your Lambda function's `hard_timeout`. If the Lambda times out before SQS receives a deletion confirmation, the message becomes visible again and will be redelivered.
|
|
471
501
|
- **Dead letter queue (DLQ)** — configure a DLQ to capture messages that cannot be processed. Without one, messages that exhaust retries are silently deleted.
|
|
@@ -592,6 +622,39 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
592
622
|
|
|
593
623
|
---
|
|
594
624
|
|
|
625
|
+
## AWS Batch setup
|
|
626
|
+
|
|
627
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, route them to a Batch queue:
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
@lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
|
|
631
|
+
def process_large_file(*, file_id: int) -> str:
|
|
632
|
+
# long-running work with large disk usage
|
|
633
|
+
return "done"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Batch job definition setup
|
|
637
|
+
|
|
638
|
+
Your Batch job definition should:
|
|
639
|
+
- Use the same Docker image as your application
|
|
640
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
641
|
+
- Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
|
|
642
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
643
|
+
|
|
644
|
+
### Retries
|
|
645
|
+
|
|
646
|
+
Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
|
|
647
|
+
|
|
648
|
+
### Container overrides limit
|
|
649
|
+
|
|
650
|
+
The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
|
|
651
|
+
|
|
652
|
+
### Eager and local worker modes
|
|
653
|
+
|
|
654
|
+
Tasks on Batch queues respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like SQS-backed tasks, without requiring AWS infrastructure.
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
595
658
|
## Built-in tasks
|
|
596
659
|
|
|
597
660
|
### `cleanup_task_records`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Django Lambda Tasks
|
|
2
2
|
|
|
3
|
-
A Django library for offloading work to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler
|
|
3
|
+
A Django library for offloading work to AWS Lambda or AWS Batch outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda handler or Batch container. Task results, status, and metadata are persisted in the Django database.
|
|
4
4
|
|
|
5
5
|
> **Platform note:** Unix-based systems only (Linux, macOS). Timeout enforcement relies on `SIGALRM`.
|
|
6
6
|
|
|
@@ -74,7 +74,7 @@ See [AWS Lambda and SQS setup](#aws-lambda-and-sqs-setup) for queue and Lambda c
|
|
|
74
74
|
|
|
75
75
|
## Example app
|
|
76
76
|
|
|
77
|
-
A runnable Django project is included in the [`example/`](example/) directory. It uses `
|
|
77
|
+
A runnable Django project is included in the [`example/`](example/) directory. It uses `LAMBDA_TASKS_LOCAL_WORKERS = 2` so no AWS infrastructure is needed.
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
cd example
|
|
@@ -97,16 +97,25 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
97
97
|
|
|
98
98
|
| Setting | Type | Description |
|
|
99
99
|
|---|---|---|
|
|
100
|
-
| `LAMBDA_TASKS_QUEUES` | `dict[str,
|
|
100
|
+
| `LAMBDA_TASKS_QUEUES` | `dict[str, dict]` | Map of queue name → queue configuration. Must include a `"default"` key. The `"default"` queue must be an SQS queue. |
|
|
101
|
+
|
|
102
|
+
Each queue value is a dict. The queue type is determined by the keys present:
|
|
103
|
+
|
|
104
|
+
- **SQS queue:** `{"queue_url": "https://sqs.../queue-name"}` — tasks execute on Lambda (max timeout 900s)
|
|
105
|
+
- **Batch queue:** `{"job_queue_arn": "arn:...", "job_definition_arn": "arn:..."}` — tasks execute on AWS Batch (max timeout 3600s)
|
|
101
106
|
|
|
102
107
|
```python
|
|
103
108
|
LAMBDA_TASKS_QUEUES = {
|
|
104
|
-
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
105
|
-
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
109
|
+
"default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
|
|
110
|
+
"high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
|
|
111
|
+
"heavy": {
|
|
112
|
+
"job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
113
|
+
"job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
114
|
+
},
|
|
106
115
|
}
|
|
107
116
|
```
|
|
108
117
|
|
|
109
|
-
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
118
|
+
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use. The `"default"` queue must be an SQS queue.
|
|
110
119
|
|
|
111
120
|
### Timeout settings
|
|
112
121
|
|
|
@@ -115,7 +124,7 @@ LAMBDA_TASKS_QUEUES = {
|
|
|
115
124
|
| `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `int` | `270` | Seconds before `SoftTimeLimitExceeded` is raised inside the task. |
|
|
116
125
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `int` | `300` | Seconds before the task is forcibly terminated. |
|
|
117
126
|
|
|
118
|
-
Both values must be
|
|
127
|
+
Both values must be greater than zero and `soft_timeout` must always be strictly less than `hard_timeout`. The maximum allowed value depends on the queue type: 900 seconds for SQS queues (Lambda's 15-minute limit), 3600 seconds for Batch queues (1 hour). Upper bound validation happens at execution time based on the task's queue.
|
|
119
128
|
|
|
120
129
|
```python
|
|
121
130
|
LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
|
|
@@ -205,9 +214,9 @@ def my_task(*, arg: str) -> None:
|
|
|
205
214
|
|
|
206
215
|
| Parameter | Type | Default | Description |
|
|
207
216
|
|---|---|---|---|
|
|
208
|
-
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (
|
|
209
|
-
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (
|
|
210
|
-
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
217
|
+
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
|
|
218
|
+
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds. Max depends on queue type (900 for SQS, 3600 for Batch). |
|
|
219
|
+
| `queue` | `str` | `"default"` | Named queue to route this task to. Determines both execution backend and timeout limits. |
|
|
211
220
|
| `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
|
|
212
221
|
| `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
|
|
213
222
|
| `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
|
|
@@ -286,26 +295,46 @@ Timeout resolution order (first non-`None` value wins):
|
|
|
286
295
|
1. Per-task decorator default (`soft_timeout` / `hard_timeout` on `@lambda_task`)
|
|
287
296
|
2. Global settings (`LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` / `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT`)
|
|
288
297
|
|
|
298
|
+
The maximum allowed timeout is determined by the queue type at execution time: 900 seconds for SQS queues, 3600 seconds for Batch queues.
|
|
299
|
+
|
|
289
300
|
---
|
|
290
301
|
|
|
291
302
|
## Named queues
|
|
292
303
|
|
|
293
|
-
Route tasks to
|
|
304
|
+
Route tasks to different execution backends by assigning them to named queues:
|
|
294
305
|
|
|
295
306
|
```python
|
|
296
307
|
# settings.py
|
|
297
308
|
LAMBDA_TASKS_QUEUES = {
|
|
298
|
-
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
299
|
-
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
309
|
+
"default": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/default"},
|
|
310
|
+
"high_memory": {"queue_url": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory"},
|
|
311
|
+
"heavy": {
|
|
312
|
+
"job_queue_arn": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
313
|
+
"job_definition_arn": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
314
|
+
},
|
|
300
315
|
}
|
|
301
316
|
```
|
|
302
317
|
|
|
303
318
|
```python
|
|
319
|
+
# Runs on Lambda with more memory
|
|
304
320
|
@lambda_task(queue="high_memory")
|
|
305
|
-
def
|
|
321
|
+
def process_images(*, batch_id: int) -> None:
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
# Runs on AWS Batch (up to 1 hour, large disk)
|
|
325
|
+
@lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
|
|
326
|
+
def process_large_file(*, file_id: int) -> str:
|
|
306
327
|
...
|
|
307
328
|
```
|
|
308
329
|
|
|
330
|
+
Tasks routed to a Batch queue are automatically submitted to AWS Batch via the built-in `submit_batch_job` task. The flow is:
|
|
331
|
+
|
|
332
|
+
1. `execute_on_commit()` enqueues a message to the default SQS queue
|
|
333
|
+
2. The Lambda handler picks up the message and runs `submit_batch_job`
|
|
334
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
335
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
336
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
337
|
+
|
|
309
338
|
---
|
|
310
339
|
|
|
311
340
|
## Task results
|
|
@@ -441,10 +470,11 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
|
|
|
441
470
|
|---|---|---|
|
|
442
471
|
| Task function has positional parameters | `TypeError` | At decoration time |
|
|
443
472
|
| `soft_timeout >= hard_timeout` | `ValueError` | At decoration time |
|
|
444
|
-
| Timeout value exceeds 900
|
|
473
|
+
| Timeout value exceeds queue max (900 or 3600) | `ValueError` | At execution time (timeout resolution) |
|
|
445
474
|
| Unknown queue name | `ImproperlyConfigured` | At `.execute_on_commit()` |
|
|
446
475
|
| `LAMBDA_TASKS_QUEUES` not set | `ImproperlyConfigured` | On first settings access |
|
|
447
476
|
| `LAMBDA_TASKS_QUEUES` missing `"default"` | `ImproperlyConfigured` | On first settings access |
|
|
477
|
+
| `"default"` queue is not SQS | `ImproperlyConfigured` | On first settings access |
|
|
448
478
|
| SQS `send_message` failure | boto3 exception (propagated) | At `.execute_on_commit()` |
|
|
449
479
|
|
|
450
480
|
---
|
|
@@ -453,7 +483,7 @@ Each task runs inside a `django.db.transaction.atomic` block. If the task raises
|
|
|
453
483
|
|
|
454
484
|
### SQS queue configuration
|
|
455
485
|
|
|
456
|
-
Each queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
|
|
486
|
+
Each SQS queue in `LAMBDA_TASKS_QUEUES` should be a standard SQS queue (not FIFO) with the following recommended settings:
|
|
457
487
|
|
|
458
488
|
- **Visibility timeout** — set this higher than your Lambda function's `hard_timeout`. If the Lambda times out before SQS receives a deletion confirmation, the message becomes visible again and will be redelivered.
|
|
459
489
|
- **Dead letter queue (DLQ)** — configure a DLQ to capture messages that cannot be processed. Without one, messages that exhaust retries are silently deleted.
|
|
@@ -580,6 +610,39 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
580
610
|
|
|
581
611
|
---
|
|
582
612
|
|
|
613
|
+
## AWS Batch setup
|
|
614
|
+
|
|
615
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, route them to a Batch queue:
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
@lambda_task(queue="heavy", soft_timeout=1800, hard_timeout=3500)
|
|
619
|
+
def process_large_file(*, file_id: int) -> str:
|
|
620
|
+
# long-running work with large disk usage
|
|
621
|
+
return "done"
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Batch job definition setup
|
|
625
|
+
|
|
626
|
+
Your Batch job definition should:
|
|
627
|
+
- Use the same Docker image as your application
|
|
628
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
629
|
+
- Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
|
|
630
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
631
|
+
|
|
632
|
+
### Retries
|
|
633
|
+
|
|
634
|
+
Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
|
|
635
|
+
|
|
636
|
+
### Container overrides limit
|
|
637
|
+
|
|
638
|
+
The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
|
|
639
|
+
|
|
640
|
+
### Eager and local worker modes
|
|
641
|
+
|
|
642
|
+
Tasks on Batch queues respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like SQS-backed tasks, without requiring AWS infrastructure.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
583
646
|
## Built-in tasks
|
|
584
647
|
|
|
585
648
|
### `cleanup_task_records`
|
|
@@ -17,6 +17,7 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
17
17
|
list_display = (
|
|
18
18
|
"pk",
|
|
19
19
|
"task_name",
|
|
20
|
+
"queue",
|
|
20
21
|
"status",
|
|
21
22
|
"start_time",
|
|
22
23
|
"end_time",
|
|
@@ -59,10 +60,17 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
59
60
|
total_seconds = d.total_seconds()
|
|
60
61
|
return f"{total_seconds:.3f}s"
|
|
61
62
|
|
|
63
|
+
@admin.display(description="Queue")
|
|
64
|
+
def queue(self, obj: TaskRecord) -> str:
|
|
65
|
+
try:
|
|
66
|
+
return import_string(obj.task_name).queue
|
|
67
|
+
except Exception:
|
|
68
|
+
return "?"
|
|
69
|
+
|
|
62
70
|
@admin.action(description="Replay selected tasks", permissions=("change",))
|
|
63
71
|
def replay_tasks(self, request: HttpRequest, queryset: QuerySet) -> None:
|
|
64
72
|
for record in queryset:
|
|
65
|
-
|
|
73
|
+
wrapper = import_string(record.task_name)
|
|
66
74
|
task = SQSLambdaTask(
|
|
67
75
|
message=SQSLambdaTaskMessage(
|
|
68
76
|
task_name=record.task_name,
|
|
@@ -70,6 +78,6 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
70
78
|
n_retries=0,
|
|
71
79
|
),
|
|
72
80
|
delay=0,
|
|
73
|
-
queue=queue,
|
|
81
|
+
queue=wrapper.queue,
|
|
74
82
|
)
|
|
75
83
|
task.execute_on_commit()
|