django-lambda-tasks 0.4.11__tar.gz → 0.6.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.
Potentially problematic release.
This version of django-lambda-tasks might be problematic. Click here for more details.
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/steering/product.md +39 -10
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/steering/structure.md +10 -10
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/PKG-INFO +95 -19
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/README.md +94 -18
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/admin.py +18 -2
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/decorators.py +125 -59
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/environment_loader.py +17 -25
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/handler.py +34 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/models.py +14 -6
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/secret_loader.py +30 -25
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/settings.py +32 -0
- django_lambda_tasks-0.6.0/lambda_tasks/tasks.py +74 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/pyproject.toml +1 -1
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_admin.py +5 -0
- django_lambda_tasks-0.6.0/tests/test_batch.py +210 -0
- django_lambda_tasks-0.6.0/tests/test_batch_decorators.py +158 -0
- django_lambda_tasks-0.6.0/tests/test_batch_worker.py +66 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_environment_loader.py +11 -20
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_local_executor.py +6 -6
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_models.py +21 -25
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_secret_loader.py +15 -20
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_settings.py +78 -0
- django_lambda_tasks-0.4.11/lambda_tasks/tasks.py +0 -23
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.gitignore +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/README.md +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.11 → django_lambda_tasks-0.6.0}/tests/test_timeouts.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
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
View → @batch_task.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` —
|
|
17
|
-
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish or local pool submit)
|
|
30
|
+
- `decorators.py` — `BaseTaskWrapper`, `LambdaTaskWrapper`, `BatchTaskWrapper`, `@lambda_task`, `@batch_task`
|
|
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` — `TaskBackend` enum, lazy `LambdaTasksSettings` reading from Django settings
|
|
22
36
|
|
|
23
37
|
## Task Definition
|
|
24
38
|
|
|
25
39
|
```python
|
|
26
|
-
#
|
|
40
|
+
# Lambda task — max timeout 900s
|
|
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 — max timeout 3600s, for long-running or storage-heavy workloads
|
|
48
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500, queue="default",
|
|
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
|
+
- Both decorators share `BaseTaskWrapper` — same validation, kwargs building, retry, and singleton logic
|
|
38
59
|
|
|
39
60
|
## Enqueuing
|
|
40
61
|
|
|
@@ -132,18 +153,24 @@ 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()`.
|
|
138
165
|
|
|
139
166
|
When the environment variable `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is set, the loader parses the reference, fetches the named Secrets Manager secret, parses its JSON content as a flat key-value mapping, and sets the resulting pairs as environment variables.
|
|
140
167
|
|
|
141
|
-
Required format: `<arn>:<version-
|
|
168
|
+
Required format: `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
142
169
|
|
|
143
170
|
Behaviour:
|
|
144
171
|
- If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
|
|
145
172
|
- Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
|
|
146
|
-
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
173
|
+
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
147
174
|
- Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
148
175
|
- Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
|
|
149
176
|
- Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
|
|
@@ -155,12 +182,12 @@ Behaviour:
|
|
|
155
182
|
|
|
156
183
|
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
157
184
|
|
|
158
|
-
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments
|
|
185
|
+
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments). The version-stage segment must be empty. The json-key and version-id must be non-empty.
|
|
159
186
|
|
|
160
187
|
Behaviour:
|
|
161
188
|
- All references are validated before any AWS call — malformed references raise `ValueError` immediately
|
|
162
189
|
- Setting both `LAMBDA_TASKS_SECRET_FOO` and `FOO` is a configuration error and raises `ValueError`
|
|
163
|
-
- Calls are batched by `(ARN, version-
|
|
190
|
+
- Calls are batched by `(ARN, version-id)` — one `GetSecretValue` per unique combination
|
|
164
191
|
- Fetched secrets are cached in-process; warm invocations pay no extra cost
|
|
165
192
|
|
|
166
193
|
## Models
|
|
@@ -184,6 +211,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
|
184
211
|
| Setting | Default | Description |
|
|
185
212
|
|---|---|---|
|
|
186
213
|
| `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name: sqs_url}`, must include `"default"` |
|
|
214
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `{}` | Dict of `{queue_name: {"job_queue": ..., "job_definition": ...}}`, must include `"default"` when set |
|
|
187
215
|
| `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `270` | Soft timeout in seconds |
|
|
188
216
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `300` | Hard timeout in seconds |
|
|
189
217
|
| `LAMBDA_TASKS_EAGER` | `False` | Run tasks synchronously in-process (no SQS) |
|
|
@@ -261,6 +289,7 @@ def my_task(*, user_id: int) -> None:
|
|
|
261
289
|
`lambda_tasks.tasks` provides maintenance tasks that ship with the library:
|
|
262
290
|
|
|
263
291
|
- `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).
|
|
292
|
+
- `submit_batch_job(*, message_json: str, batch_queue: str) -> str` — submits a batch task to AWS Batch. Reads `job_queue` and `job_definition` from `LAMBDA_TASKS_BATCH_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 `backend == TaskBackend.BATCH` — users do not call it directly.
|
|
264
293
|
|
|
265
294
|
## Conventions
|
|
266
295
|
|
|
@@ -12,15 +12,15 @@ 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 # BaseTaskWrapper, LambdaTaskWrapper, BatchTaskWrapper, @lambda_task, @batch_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, TaskBackend enum, constants
|
|
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
|
|
@@ -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()` publishes to SQS, executes eagerly, or submits to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
42
|
+
- `decorators.py` — defines `BaseTaskWrapper` (shared validation, kwargs building, execution logic), `LambdaTaskWrapper` and `BatchTaskWrapper` (subclasses that set `_max_timeout` and `_backend`), plus `@lambda_task` and `@batch_task` decorator factories; enforces kwargs-only at decoration time
|
|
43
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing + `backend` field; `_execute()` publishes to SQS, submits to Batch via `submit_batch_job`, executes eagerly, or submits 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` — `TaskBackend` StrEnum (`LAMBDA`, `BATCH`); constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `LambdaTasksSettings` instantiated fresh per use (reads live Django settings including `BATCH_QUEUES`)
|
|
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.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -110,12 +110,20 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
110
110
|
| Setting | Type | Description |
|
|
111
111
|
|---|---|---|
|
|
112
112
|
| `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
|
|
113
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `dict[str, dict]` | Map of queue name → `{"job_queue": ..., "job_definition": ...}`. Must include a `"default"` key when set. Default `{}`. |
|
|
113
114
|
|
|
114
115
|
```python
|
|
115
116
|
LAMBDA_TASKS_QUEUES = {
|
|
116
117
|
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
117
118
|
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
122
|
+
"default": {
|
|
123
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
124
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
125
|
+
},
|
|
126
|
+
}
|
|
119
127
|
```
|
|
120
128
|
|
|
121
129
|
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
@@ -150,7 +158,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
150
158
|
|
|
151
159
|
| Setting | Type | Description |
|
|
152
160
|
|---|---|---|
|
|
153
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
161
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
154
162
|
| `LAMBDA_TASKS_SECRET_*` | env var(s) | Secrets Manager references resolved into env vars at Lambda cold start. The unprefixed name becomes the target env var. |
|
|
155
163
|
|
|
156
164
|
These are environment variables set on the Lambda function, not Django settings. See [Loading environment variables from Secrets Manager](#loading-environment-variables-from-secrets-manager) and [Resolving individual secrets from AWS Secrets Manager](#resolving-individual-secrets-from-aws-secrets-manager) for full details.
|
|
@@ -203,7 +211,6 @@ Key characteristics:
|
|
|
203
211
|
|
|
204
212
|
```python
|
|
205
213
|
@lambda_task(
|
|
206
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
207
214
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
208
215
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
209
216
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -493,20 +500,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
493
500
|
| Environment Variable | Required | Description |
|
|
494
501
|
|---|---|---|
|
|
495
502
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
496
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
503
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
497
504
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
498
505
|
|
|
499
506
|
### Loading environment variables from Secrets Manager
|
|
500
507
|
|
|
501
508
|
The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
|
|
502
509
|
|
|
503
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
510
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
504
511
|
|
|
505
512
|
```
|
|
506
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
513
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
507
514
|
```
|
|
508
515
|
|
|
509
|
-
The format is `<arn>:<version-
|
|
516
|
+
The format is `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
510
517
|
|
|
511
518
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
512
519
|
|
|
@@ -521,8 +528,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
521
528
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
522
529
|
|
|
523
530
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
524
|
-
2. Parses and validates the reference format (
|
|
525
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
531
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
532
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
526
533
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
527
534
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
528
535
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -533,7 +540,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
533
540
|
|
|
534
541
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
535
542
|
|
|
536
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
543
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
537
544
|
- Secret value is not valid JSON
|
|
538
545
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
539
546
|
- JSON contains an empty string key
|
|
@@ -547,14 +554,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
547
554
|
Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
|
|
548
555
|
|
|
549
556
|
```
|
|
550
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
557
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
551
558
|
```
|
|
552
559
|
|
|
553
560
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
554
561
|
|
|
555
562
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
556
563
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
557
|
-
3. Groups references by `(ARN, version-
|
|
564
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
558
565
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
559
566
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
560
567
|
|
|
@@ -566,18 +573,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
566
573
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
567
574
|
```
|
|
568
575
|
|
|
569
|
-
|
|
576
|
+
The version-stage segment must be empty. The json-key and version-id fields are required and must be non-empty. The secret value must be a JSON object; `json-key` names the field to extract.
|
|
570
577
|
|
|
571
578
|
```
|
|
572
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
573
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
579
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
580
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
574
581
|
```
|
|
575
582
|
|
|
576
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
583
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
577
584
|
|
|
578
585
|
```
|
|
579
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
580
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
586
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
587
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
581
588
|
```
|
|
582
589
|
|
|
583
590
|
#### Validation errors
|
|
@@ -585,7 +592,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
585
592
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
586
593
|
|
|
587
594
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
588
|
-
- Empty `json-key
|
|
595
|
+
- Empty `json-key` or `version-id`
|
|
596
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
589
597
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
590
598
|
- The named JSON key does not exist in the fetched secret
|
|
591
599
|
- The secret value is not valid JSON
|
|
@@ -623,3 +631,71 @@ result = send_welcome_email(user_id=1, template="welcome")
|
|
|
623
631
|
```
|
|
624
632
|
|
|
625
633
|
This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## AWS Batch tasks
|
|
638
|
+
|
|
639
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, use `@batch_task` to route execution to AWS Batch containers.
|
|
640
|
+
|
|
641
|
+
### 1. Define a batch task
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
# myapp/tasks.py
|
|
645
|
+
from lambda_tasks.decorators import batch_task
|
|
646
|
+
|
|
647
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500)
|
|
648
|
+
def process_large_file(*, file_id: int) -> str:
|
|
649
|
+
# long-running work with large disk usage
|
|
650
|
+
return "done"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 2. Configure AWS Batch queues
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
# settings.py
|
|
657
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
658
|
+
"default": {
|
|
659
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
660
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 3. Enqueue from a view
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
process_large_file.execute_on_commit(file_id=42)
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### How it works
|
|
672
|
+
|
|
673
|
+
1. `execute_on_commit()` enqueues a message to SQS (same as Lambda tasks)
|
|
674
|
+
2. The Lambda handler picks up the message and runs the built-in `submit_batch_job` task
|
|
675
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
676
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
677
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
678
|
+
|
|
679
|
+
### Batch task timeouts
|
|
680
|
+
|
|
681
|
+
Batch tasks support the same two-phase timeout mechanism as Lambda tasks (soft + hard), but with a maximum of **3600 seconds** (1 hour) instead of 900.
|
|
682
|
+
|
|
683
|
+
### Retries
|
|
684
|
+
|
|
685
|
+
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.
|
|
686
|
+
|
|
687
|
+
### Batch job definition setup
|
|
688
|
+
|
|
689
|
+
Your Batch job definition should:
|
|
690
|
+
- Use the same Docker image as your application
|
|
691
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
692
|
+
- 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)
|
|
693
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
694
|
+
|
|
695
|
+
### Container overrides limit
|
|
696
|
+
|
|
697
|
+
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.
|
|
698
|
+
|
|
699
|
+
### Eager and local worker modes
|
|
700
|
+
|
|
701
|
+
Batch tasks respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like Lambda tasks, without requiring AWS infrastructure.
|
|
@@ -98,12 +98,20 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
98
98
|
| Setting | Type | Description |
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
|
|
101
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `dict[str, dict]` | Map of queue name → `{"job_queue": ..., "job_definition": ...}`. Must include a `"default"` key when set. Default `{}`. |
|
|
101
102
|
|
|
102
103
|
```python
|
|
103
104
|
LAMBDA_TASKS_QUEUES = {
|
|
104
105
|
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
105
106
|
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
106
107
|
}
|
|
108
|
+
|
|
109
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
110
|
+
"default": {
|
|
111
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
112
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
113
|
+
},
|
|
114
|
+
}
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
@@ -138,7 +146,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
138
146
|
|
|
139
147
|
| Setting | Type | Description |
|
|
140
148
|
|---|---|---|
|
|
141
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
149
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
142
150
|
| `LAMBDA_TASKS_SECRET_*` | env var(s) | Secrets Manager references resolved into env vars at Lambda cold start. The unprefixed name becomes the target env var. |
|
|
143
151
|
|
|
144
152
|
These are environment variables set on the Lambda function, not Django settings. See [Loading environment variables from Secrets Manager](#loading-environment-variables-from-secrets-manager) and [Resolving individual secrets from AWS Secrets Manager](#resolving-individual-secrets-from-aws-secrets-manager) for full details.
|
|
@@ -191,7 +199,6 @@ Key characteristics:
|
|
|
191
199
|
|
|
192
200
|
```python
|
|
193
201
|
@lambda_task(
|
|
194
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
195
202
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
196
203
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
197
204
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -481,20 +488,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
481
488
|
| Environment Variable | Required | Description |
|
|
482
489
|
|---|---|---|
|
|
483
490
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
484
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
491
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
485
492
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
486
493
|
|
|
487
494
|
### Loading environment variables from Secrets Manager
|
|
488
495
|
|
|
489
496
|
The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
|
|
490
497
|
|
|
491
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
498
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
492
499
|
|
|
493
500
|
```
|
|
494
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
501
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
495
502
|
```
|
|
496
503
|
|
|
497
|
-
The format is `<arn>:<version-
|
|
504
|
+
The format is `<arn>:<version-id>` (8 colon-separated segments — the ARN is 7 segments, plus the version-id). The version-id must be non-empty.
|
|
498
505
|
|
|
499
506
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
500
507
|
|
|
@@ -509,8 +516,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
509
516
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
510
517
|
|
|
511
518
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
512
|
-
2. Parses and validates the reference format (
|
|
513
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
519
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
520
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
514
521
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
515
522
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
516
523
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -521,7 +528,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
521
528
|
|
|
522
529
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
523
530
|
|
|
524
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
531
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
525
532
|
- Secret value is not valid JSON
|
|
526
533
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
527
534
|
- JSON contains an empty string key
|
|
@@ -535,14 +542,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
535
542
|
Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager dynamic reference. The unprefixed name becomes the target env var:
|
|
536
543
|
|
|
537
544
|
```
|
|
538
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
545
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
539
546
|
```
|
|
540
547
|
|
|
541
548
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
542
549
|
|
|
543
550
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
544
551
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
545
|
-
3. Groups references by `(ARN, version-
|
|
552
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
546
553
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
547
554
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
548
555
|
|
|
@@ -554,18 +561,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
554
561
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
555
562
|
```
|
|
556
563
|
|
|
557
|
-
|
|
564
|
+
The version-stage segment must be empty. The json-key and version-id fields are required and must be non-empty. The secret value must be a JSON object; `json-key` names the field to extract.
|
|
558
565
|
|
|
559
566
|
```
|
|
560
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
561
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
567
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
568
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
562
569
|
```
|
|
563
570
|
|
|
564
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
571
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
565
572
|
|
|
566
573
|
```
|
|
567
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
568
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
574
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
575
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
569
576
|
```
|
|
570
577
|
|
|
571
578
|
#### Validation errors
|
|
@@ -573,7 +580,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
573
580
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
574
581
|
|
|
575
582
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
576
|
-
- Empty `json-key
|
|
583
|
+
- Empty `json-key` or `version-id`
|
|
584
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
577
585
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
578
586
|
- The named JSON key does not exist in the fetched secret
|
|
579
587
|
- The secret value is not valid JSON
|
|
@@ -611,3 +619,71 @@ result = send_welcome_email(user_id=1, template="welcome")
|
|
|
611
619
|
```
|
|
612
620
|
|
|
613
621
|
This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## AWS Batch tasks
|
|
626
|
+
|
|
627
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, use `@batch_task` to route execution to AWS Batch containers.
|
|
628
|
+
|
|
629
|
+
### 1. Define a batch task
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
# myapp/tasks.py
|
|
633
|
+
from lambda_tasks.decorators import batch_task
|
|
634
|
+
|
|
635
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500)
|
|
636
|
+
def process_large_file(*, file_id: int) -> str:
|
|
637
|
+
# long-running work with large disk usage
|
|
638
|
+
return "done"
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 2. Configure AWS Batch queues
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
# settings.py
|
|
645
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
646
|
+
"default": {
|
|
647
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
648
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
649
|
+
},
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 3. Enqueue from a view
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
process_large_file.execute_on_commit(file_id=42)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### How it works
|
|
660
|
+
|
|
661
|
+
1. `execute_on_commit()` enqueues a message to SQS (same as Lambda tasks)
|
|
662
|
+
2. The Lambda handler picks up the message and runs the built-in `submit_batch_job` task
|
|
663
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
664
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
665
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
666
|
+
|
|
667
|
+
### Batch task timeouts
|
|
668
|
+
|
|
669
|
+
Batch tasks support the same two-phase timeout mechanism as Lambda tasks (soft + hard), but with a maximum of **3600 seconds** (1 hour) instead of 900.
|
|
670
|
+
|
|
671
|
+
### Retries
|
|
672
|
+
|
|
673
|
+
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.
|
|
674
|
+
|
|
675
|
+
### Batch job definition setup
|
|
676
|
+
|
|
677
|
+
Your Batch job definition should:
|
|
678
|
+
- Use the same Docker image as your application
|
|
679
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
680
|
+
- 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)
|
|
681
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
682
|
+
|
|
683
|
+
### Container overrides limit
|
|
684
|
+
|
|
685
|
+
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.
|
|
686
|
+
|
|
687
|
+
### Eager and local worker modes
|
|
688
|
+
|
|
689
|
+
Batch tasks respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like Lambda tasks, without requiring AWS infrastructure.
|
|
@@ -17,6 +17,8 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
17
17
|
list_display = (
|
|
18
18
|
"pk",
|
|
19
19
|
"task_name",
|
|
20
|
+
"backend",
|
|
21
|
+
"queue",
|
|
20
22
|
"status",
|
|
21
23
|
"start_time",
|
|
22
24
|
"end_time",
|
|
@@ -59,10 +61,24 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
59
61
|
total_seconds = d.total_seconds()
|
|
60
62
|
return f"{total_seconds:.3f}s"
|
|
61
63
|
|
|
64
|
+
@admin.display(description="Backend")
|
|
65
|
+
def backend(self, obj: TaskRecord) -> str:
|
|
66
|
+
try:
|
|
67
|
+
return import_string(obj.task_name).backend.value
|
|
68
|
+
except Exception:
|
|
69
|
+
return "?"
|
|
70
|
+
|
|
71
|
+
@admin.display(description="Queue")
|
|
72
|
+
def queue(self, obj: TaskRecord) -> str:
|
|
73
|
+
try:
|
|
74
|
+
return import_string(obj.task_name).queue
|
|
75
|
+
except Exception:
|
|
76
|
+
return "?"
|
|
77
|
+
|
|
62
78
|
@admin.action(description="Replay selected tasks", permissions=("change",))
|
|
63
79
|
def replay_tasks(self, request: HttpRequest, queryset: QuerySet) -> None:
|
|
64
80
|
for record in queryset:
|
|
65
|
-
|
|
81
|
+
wrapper = import_string(record.task_name)
|
|
66
82
|
task = SQSLambdaTask(
|
|
67
83
|
message=SQSLambdaTaskMessage(
|
|
68
84
|
task_name=record.task_name,
|
|
@@ -70,6 +86,6 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
70
86
|
n_retries=0,
|
|
71
87
|
),
|
|
72
88
|
delay=0,
|
|
73
|
-
queue=queue,
|
|
89
|
+
queue=wrapper.queue,
|
|
74
90
|
)
|
|
75
91
|
task.execute_on_commit()
|