django-lambda-tasks 0.2.2__tar.gz → 0.2.4__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.2.2 → django_lambda_tasks-0.2.4}/.kiro/steering/product.md +5 -4
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/steering/structure.md +1 -1
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/steering/tech.md +1 -1
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/PKG-INFO +81 -56
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/README.md +80 -55
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/handler.py +23 -9
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/logging.py +1 -1
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/pyproject.toml +1 -1
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_handler.py +90 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.gitignore +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/README.md +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/example/manage.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/settings.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/tests/test_timeouts.py +0 -0
|
@@ -62,7 +62,7 @@ def sync_user(*, user_id: int) -> None:
|
|
|
62
62
|
|
|
63
63
|
## retry_on
|
|
64
64
|
|
|
65
|
-
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor automatically re-enqueues the task
|
|
65
|
+
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor automatically re-enqueues the task with the same kwargs and an incremented `n_retries` counter on the `SQSLambdaTaskMessage`.
|
|
66
66
|
|
|
67
67
|
- `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
|
|
68
68
|
- The retry is a new invocation — the current record is terminal at `RETRYING`
|
|
@@ -125,7 +125,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
125
125
|
7. On ignored exception (type matches `ignore_errors`): rolls back task-side writes, commits record as `SUCCESS` with traceback and `end_time`
|
|
126
126
|
8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks, `n_retries < MAX_RETRIES`): rolls back task-side writes, enqueues retry via `execute_on_commit` with `n_retries + 1`, commits record as `RETRYING` with traceback and `end_time`
|
|
127
127
|
9. On retryable exception with `n_retries >= MAX_RETRIES`: commits record as `FAILED` with traceback, raises `MaxRetriesExceededError`
|
|
128
|
-
10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback
|
|
128
|
+
10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback and `end_time`
|
|
129
129
|
|
|
130
130
|
## Lambda Handler
|
|
131
131
|
|
|
@@ -135,9 +135,10 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
135
135
|
- Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
|
|
136
136
|
- Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
|
|
137
137
|
- Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
|
|
138
|
-
- Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
|
|
138
|
+
- Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: a temporary `StreamHandler` is attached to the `lambda_tasks` logger, then `resolve_environment()` → `resolve_secrets_into_env()` (handler removed) → conditional `django.setup()`
|
|
139
139
|
- A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
|
|
140
140
|
- Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
|
|
141
|
+
- 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
|
|
141
142
|
|
|
142
143
|
## Environment Loader
|
|
143
144
|
|
|
@@ -205,7 +206,7 @@ Set `LAMBDA_TASKS_EAGER = True` to run tasks synchronously in-process (no SQS).
|
|
|
205
206
|
|
|
206
207
|
In eager mode a random UUID4 is generated as the `message_id` passed to `execute_immediately()`.
|
|
207
208
|
|
|
208
|
-
**Timeouts are not enforced in eager mode.** `TimeoutContext` is
|
|
209
|
+
**Timeouts are not enforced in eager mode.** `TimeoutContext` is still entered but becomes a no-op — it checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. `SIGALRM`-based timeouts require a Lambda worker process, not a Django dev server thread. Timeout values are still validated at decoration time.
|
|
209
210
|
|
|
210
211
|
## Logging
|
|
211
212
|
|
|
@@ -40,7 +40,7 @@ django-lambda-tasks/
|
|
|
40
40
|
|
|
41
41
|
- `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
|
|
42
42
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
43
|
-
- `handler.py` — Lambda entry point; cold-start init (`resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
|
|
43
|
+
- `handler.py` — Lambda entry point; cold-start init (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`
|
|
44
44
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
45
45
|
- `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
|
|
46
46
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
@@ -42,7 +42,7 @@ uv run pytest tests/test_foo.py # single file
|
|
|
42
42
|
uv run pytest -x # stop on first failure
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
- Tests live in `tests/` — one file per source module (`
|
|
45
|
+
- Tests live in `tests/` — one file per source module (`models.py` → `test_models.py`)
|
|
46
46
|
- Django settings for tests are in `tests/settings.py`
|
|
47
47
|
- Use `hypothesis` for property-based tests where correctness properties can be expressed
|
|
48
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -127,13 +127,32 @@ LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
|
|
|
127
127
|
LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Retry settings
|
|
131
|
+
|
|
132
|
+
| Setting | Type | Default | Description |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (default is 60 × 24 × 2). |
|
|
135
|
+
|
|
136
|
+
### Singleton settings
|
|
137
|
+
|
|
138
|
+
| Setting | Type | Default | Description |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
|
|
141
|
+
|
|
142
|
+
### Environment and secrets
|
|
143
|
+
|
|
144
|
+
| Setting | Type | Description |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
147
|
+
| `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. |
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
130
151
|
### Eager execution (development / testing)
|
|
131
152
|
|
|
132
153
|
| Setting | Type | Default | Description |
|
|
133
154
|
|---|---|---|---|
|
|
134
155
|
| `LAMBDA_TASKS_EAGER` | `bool` | `False` | When `True`, tasks run synchronously in-process instead of being sent to SQS. |
|
|
135
|
-
| `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised. |
|
|
136
|
-
| `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
|
|
137
156
|
|
|
138
157
|
```python
|
|
139
158
|
# settings/local.py
|
|
@@ -142,7 +161,7 @@ LAMBDA_TASKS_EAGER = True
|
|
|
142
161
|
|
|
143
162
|
With eager mode enabled, `.execute_on_commit()` executes the task immediately without touching SQS. Useful for local development and test suites where you don't want to mock AWS infrastructure.
|
|
144
163
|
|
|
145
|
-
> **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext`
|
|
164
|
+
> **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` becomes a no-op — it checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
|
|
146
165
|
|
|
147
166
|
---
|
|
148
167
|
|
|
@@ -150,6 +169,7 @@ With eager mode enabled, `.execute_on_commit()` executes the task immediately wi
|
|
|
150
169
|
|
|
151
170
|
```python
|
|
152
171
|
@lambda_task(
|
|
172
|
+
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
153
173
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
154
174
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
155
175
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -164,6 +184,7 @@ def my_task(*, arg: str) -> None:
|
|
|
164
184
|
|
|
165
185
|
| Parameter | Type | Default | Description |
|
|
166
186
|
|---|---|---|---|
|
|
187
|
+
| `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
|
|
167
188
|
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
|
|
168
189
|
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
|
|
169
190
|
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
@@ -190,7 +211,7 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
|
|
|
190
211
|
# }
|
|
191
212
|
```
|
|
192
213
|
|
|
193
|
-
The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
|
|
214
|
+
The returned dict matches the `SQSLambdaTask` schema (`message`, `delay`, `queue`). The `delay` value comes from the decorator's `delay` parameter. To reconstruct and enqueue it later:
|
|
194
215
|
|
|
195
216
|
```python
|
|
196
217
|
from lambda_tasks.models import SQSLambdaTask
|
|
@@ -270,8 +291,10 @@ TaskRecord.objects.get(pk="<uuid>")
|
|
|
270
291
|
|
|
271
292
|
| Field | Type | Description |
|
|
272
293
|
|---|---|---|
|
|
294
|
+
| `id` | `UUID` | Primary key — set to the SQS `messageId` for deduplication. |
|
|
273
295
|
| `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
|
|
274
296
|
| `kwargs` | `dict` | Serialized task arguments. |
|
|
297
|
+
| `n_retries` | `int` | Number of retries attempted so far (starts at 0). |
|
|
275
298
|
| `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`. |
|
|
276
299
|
| `start_time` | `datetime \| None` | When the worker began executing the task. |
|
|
277
300
|
| `end_time` | `datetime \| None` | When the task completed or failed. |
|
|
@@ -297,6 +320,8 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
|
|
|
297
320
|
|
|
298
321
|
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
|
|
299
322
|
|
|
323
|
+
The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can control the level by configuring the `lambda_tasks` logger in your Django `LOGGING` setting (e.g. set it to `DEBUG` or `WARNING`). If your `LOGGING` dictConfig sets a root logger with a handler (as most Django projects do), `lambda_tasks` will inherit from it via propagation.
|
|
324
|
+
|
|
300
325
|
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
|
|
301
326
|
|
|
302
327
|
To filter by invocation in CloudWatch Logs Insights:
|
|
@@ -435,19 +460,63 @@ Point your Lambda function's handler at:
|
|
|
435
460
|
lambda_tasks.handler.handler
|
|
436
461
|
```
|
|
437
462
|
|
|
438
|
-
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
463
|
+
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
439
464
|
|
|
440
465
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
441
466
|
|
|
442
467
|
| Environment Variable | Required | Description |
|
|
443
468
|
|---|---|---|
|
|
444
469
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
445
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
446
|
-
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (
|
|
470
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
471
|
+
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
472
|
+
|
|
473
|
+
### Loading environment variables from Secrets Manager
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
484
|
+
|
|
485
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"DATABASE_URL": "postgres://user:pass@host:5432/db",
|
|
490
|
+
"REDIS_URL": "redis://host:6379/0",
|
|
491
|
+
"DJANGO_SETTINGS_MODULE": "myapp.settings.production"
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
496
|
+
|
|
497
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
498
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
499
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
500
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
501
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
502
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
503
|
+
|
|
504
|
+
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
505
|
+
|
|
506
|
+
#### Validation errors
|
|
507
|
+
|
|
508
|
+
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
447
509
|
|
|
448
|
-
|
|
510
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
511
|
+
- Secret value is not valid JSON
|
|
512
|
+
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
513
|
+
- JSON contains an empty string key
|
|
514
|
+
|
|
515
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
449
516
|
|
|
450
|
-
|
|
517
|
+
### Resolving individual secrets from AWS Secrets Manager
|
|
518
|
+
|
|
519
|
+
The Lambda handler supports loading individual secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
|
|
451
520
|
|
|
452
521
|
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:
|
|
453
522
|
|
|
@@ -455,7 +524,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
455
524
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
456
525
|
```
|
|
457
526
|
|
|
458
|
-
At cold start (on the first handler invocation), before `django.setup()
|
|
527
|
+
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
459
528
|
|
|
460
529
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
461
530
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -495,50 +564,6 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
495
564
|
- The named JSON key does not exist in the fetched secret
|
|
496
565
|
- The secret value is not valid JSON
|
|
497
566
|
|
|
498
|
-
### Loading environment variables from Secrets Manager
|
|
499
|
-
|
|
500
|
-
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.
|
|
501
|
-
|
|
502
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
503
|
-
|
|
504
|
-
```
|
|
505
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
509
|
-
|
|
510
|
-
The secret value must be a flat JSON object where all keys and values are strings:
|
|
511
|
-
|
|
512
|
-
```json
|
|
513
|
-
{
|
|
514
|
-
"DATABASE_URL": "postgres://user:pass@host:5432/db",
|
|
515
|
-
"REDIS_URL": "redis://host:6379/0",
|
|
516
|
-
"DJANGO_SETTINGS_MODULE": "myapp.settings.production"
|
|
517
|
-
}
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
521
|
-
|
|
522
|
-
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
523
|
-
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
524
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
525
|
-
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
526
|
-
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
527
|
-
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
528
|
-
|
|
529
|
-
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
530
|
-
|
|
531
|
-
#### Validation errors
|
|
532
|
-
|
|
533
|
-
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
534
|
-
|
|
535
|
-
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
536
|
-
- Secret value is not valid JSON
|
|
537
|
-
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
538
|
-
- JSON contains an empty string key
|
|
539
|
-
|
|
540
|
-
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
541
|
-
|
|
542
567
|
---
|
|
543
568
|
|
|
544
569
|
## Built-in tasks
|
|
@@ -571,4 +596,4 @@ You can call a decorated task directly like a normal function — useful in test
|
|
|
571
596
|
result = send_welcome_email(user_id=1, template="welcome")
|
|
572
597
|
```
|
|
573
598
|
|
|
574
|
-
This bypasses the queue entirely and runs the function in the current process
|
|
599
|
+
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.
|
|
@@ -115,13 +115,32 @@ LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT = 240
|
|
|
115
115
|
LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
### Retry settings
|
|
119
|
+
|
|
120
|
+
| Setting | Type | Default | Description |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (default is 60 × 24 × 2). |
|
|
123
|
+
|
|
124
|
+
### Singleton settings
|
|
125
|
+
|
|
126
|
+
| Setting | Type | Default | Description |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
|
|
129
|
+
|
|
130
|
+
### Environment and secrets
|
|
131
|
+
|
|
132
|
+
| Setting | Type | Description |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
135
|
+
| `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. |
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
118
139
|
### Eager execution (development / testing)
|
|
119
140
|
|
|
120
141
|
| Setting | Type | Default | Description |
|
|
121
142
|
|---|---|---|---|
|
|
122
143
|
| `LAMBDA_TASKS_EAGER` | `bool` | `False` | When `True`, tasks run synchronously in-process instead of being sent to SQS. |
|
|
123
|
-
| `LAMBDA_TASKS_MAX_RETRIES` | `int` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised. |
|
|
124
|
-
| `LAMBDA_TASKS_SINGLETON_CACHE` | `str` | `"default"` | Django cache backend used for singleton task locks. |
|
|
125
144
|
|
|
126
145
|
```python
|
|
127
146
|
# settings/local.py
|
|
@@ -130,7 +149,7 @@ LAMBDA_TASKS_EAGER = True
|
|
|
130
149
|
|
|
131
150
|
With eager mode enabled, `.execute_on_commit()` executes the task immediately without touching SQS. Useful for local development and test suites where you don't want to mock AWS infrastructure.
|
|
132
151
|
|
|
133
|
-
> **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext`
|
|
152
|
+
> **Note:** Timeouts are not enforced in eager mode. `soft_timeout` and `hard_timeout` values are accepted and stored but `TimeoutContext` becomes a no-op — it checks `LAMBDA_TASKS_EAGER` internally and skips `SIGALRM` setup. This is intentional: `SIGALRM`-based timeouts require a Lambda/Unix worker process, not a Django dev server thread.
|
|
134
153
|
|
|
135
154
|
---
|
|
136
155
|
|
|
@@ -138,6 +157,7 @@ With eager mode enabled, `.execute_on_commit()` executes the task immediately wi
|
|
|
138
157
|
|
|
139
158
|
```python
|
|
140
159
|
@lambda_task(
|
|
160
|
+
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
141
161
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
142
162
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
143
163
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
@@ -152,6 +172,7 @@ def my_task(*, arg: str) -> None:
|
|
|
152
172
|
|
|
153
173
|
| Parameter | Type | Default | Description |
|
|
154
174
|
|---|---|---|---|
|
|
175
|
+
| `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
|
|
155
176
|
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
|
|
156
177
|
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
|
|
157
178
|
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
@@ -178,7 +199,7 @@ payload = send_welcome_email.serialize(user_id=42, template="welcome")
|
|
|
178
199
|
# }
|
|
179
200
|
```
|
|
180
201
|
|
|
181
|
-
The returned dict matches the `SQSLambdaTask` schema. To reconstruct and enqueue it later:
|
|
202
|
+
The returned dict matches the `SQSLambdaTask` schema (`message`, `delay`, `queue`). The `delay` value comes from the decorator's `delay` parameter. To reconstruct and enqueue it later:
|
|
182
203
|
|
|
183
204
|
```python
|
|
184
205
|
from lambda_tasks.models import SQSLambdaTask
|
|
@@ -258,8 +279,10 @@ TaskRecord.objects.get(pk="<uuid>")
|
|
|
258
279
|
|
|
259
280
|
| Field | Type | Description |
|
|
260
281
|
|---|---|---|
|
|
282
|
+
| `id` | `UUID` | Primary key — set to the SQS `messageId` for deduplication. |
|
|
261
283
|
| `task_name` | `str` | Fully-qualified function name (e.g. `myapp.tasks.send_welcome_email`). |
|
|
262
284
|
| `kwargs` | `dict` | Serialized task arguments. |
|
|
285
|
+
| `n_retries` | `int` | Number of retries attempted so far (starts at 0). |
|
|
263
286
|
| `status` | `str` | One of `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`. |
|
|
264
287
|
| `start_time` | `datetime \| None` | When the worker began executing the task. |
|
|
265
288
|
| `end_time` | `datetime \| None` | When the task completed or failed. |
|
|
@@ -285,6 +308,8 @@ def send_welcome_email(*, user_id: int, template: str) -> str:
|
|
|
285
308
|
|
|
286
309
|
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. `SQSLambdaTaskMessage.execute_immediately()` sets the `message_id` before each task runs and clears it afterwards — you don't need to manage it yourself.
|
|
287
310
|
|
|
311
|
+
The Lambda handler configures the `lambda_tasks` logger hierarchy to `INFO` at cold start so that `task_logger` lines appear in CloudWatch. You can control the level by configuring the `lambda_tasks` logger in your Django `LOGGING` setting (e.g. set it to `DEBUG` or `WARNING`). If your `LOGGING` dictConfig sets a root logger with a handler (as most Django projects do), `lambda_tasks` will inherit from it via propagation.
|
|
312
|
+
|
|
288
313
|
Using your own `logging.getLogger(__name__)` is fine too; those records just won't carry the `message_id` prefix.
|
|
289
314
|
|
|
290
315
|
To filter by invocation in CloudWatch Logs Insights:
|
|
@@ -423,19 +448,63 @@ Point your Lambda function's handler at:
|
|
|
423
448
|
lambda_tasks.handler.handler
|
|
424
449
|
```
|
|
425
450
|
|
|
426
|
-
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
451
|
+
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
427
452
|
|
|
428
453
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
429
454
|
|
|
430
455
|
| Environment Variable | Required | Description |
|
|
431
456
|
|---|---|---|
|
|
432
457
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
433
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
434
|
-
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (
|
|
458
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
459
|
+
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
460
|
+
|
|
461
|
+
### Loading environment variables from Secrets Manager
|
|
462
|
+
|
|
463
|
+
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.
|
|
464
|
+
|
|
465
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
472
|
+
|
|
473
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{
|
|
477
|
+
"DATABASE_URL": "postgres://user:pass@host:5432/db",
|
|
478
|
+
"REDIS_URL": "redis://host:6379/0",
|
|
479
|
+
"DJANGO_SETTINGS_MODULE": "myapp.settings.production"
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
484
|
+
|
|
485
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
486
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
487
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
488
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
489
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
490
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
491
|
+
|
|
492
|
+
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
493
|
+
|
|
494
|
+
#### Validation errors
|
|
495
|
+
|
|
496
|
+
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
435
497
|
|
|
436
|
-
|
|
498
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
499
|
+
- Secret value is not valid JSON
|
|
500
|
+
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
501
|
+
- JSON contains an empty string key
|
|
502
|
+
|
|
503
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
437
504
|
|
|
438
|
-
|
|
505
|
+
### Resolving individual secrets from AWS Secrets Manager
|
|
506
|
+
|
|
507
|
+
The Lambda handler supports loading individual secret values from AWS Secrets Manager into the environment before Django starts. This lets your Django settings file read from `os.environ` as normal while keeping secrets out of plaintext environment variables.
|
|
439
508
|
|
|
440
509
|
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:
|
|
441
510
|
|
|
@@ -443,7 +512,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
443
512
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
444
513
|
```
|
|
445
514
|
|
|
446
|
-
At cold start (on the first handler invocation), before `django.setup()
|
|
515
|
+
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
447
516
|
|
|
448
517
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
449
518
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -483,50 +552,6 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
483
552
|
- The named JSON key does not exist in the fetched secret
|
|
484
553
|
- The secret value is not valid JSON
|
|
485
554
|
|
|
486
|
-
### Loading environment variables from Secrets Manager
|
|
487
|
-
|
|
488
|
-
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.
|
|
489
|
-
|
|
490
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
491
|
-
|
|
492
|
-
```
|
|
493
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
497
|
-
|
|
498
|
-
The secret value must be a flat JSON object where all keys and values are strings:
|
|
499
|
-
|
|
500
|
-
```json
|
|
501
|
-
{
|
|
502
|
-
"DATABASE_URL": "postgres://user:pass@host:5432/db",
|
|
503
|
-
"REDIS_URL": "redis://host:6379/0",
|
|
504
|
-
"DJANGO_SETTINGS_MODULE": "myapp.settings.production"
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
509
|
-
|
|
510
|
-
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
511
|
-
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
512
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
513
|
-
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
514
|
-
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
515
|
-
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
516
|
-
|
|
517
|
-
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
518
|
-
|
|
519
|
-
#### Validation errors
|
|
520
|
-
|
|
521
|
-
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
522
|
-
|
|
523
|
-
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
524
|
-
- Secret value is not valid JSON
|
|
525
|
-
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
526
|
-
- JSON contains an empty string key
|
|
527
|
-
|
|
528
|
-
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
529
|
-
|
|
530
555
|
---
|
|
531
556
|
|
|
532
557
|
## Built-in tasks
|
|
@@ -559,4 +584,4 @@ You can call a decorated task directly like a normal function — useful in test
|
|
|
559
584
|
result = send_welcome_email(user_id=1, template="welcome")
|
|
560
585
|
```
|
|
561
586
|
|
|
562
|
-
This bypasses the queue entirely and runs the function in the current process
|
|
587
|
+
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.
|
|
@@ -15,6 +15,9 @@ warm invocations skip it.
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
|
|
18
|
+
import django
|
|
19
|
+
from django.apps import apps
|
|
20
|
+
|
|
18
21
|
from lambda_tasks.environment_loader import resolve_environment
|
|
19
22
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
20
23
|
|
|
@@ -29,21 +32,32 @@ def _perform_cold_start() -> None:
|
|
|
29
32
|
Both loaders are idempotent and run unconditionally — the environment
|
|
30
33
|
secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
|
|
31
34
|
depend on environment-loaded vars.
|
|
35
|
+
|
|
36
|
+
A temporary StreamHandler is attached to the ``lambda_tasks`` logger for
|
|
37
|
+
the duration of the loaders so their log output is visible before Django's
|
|
38
|
+
LOGGING dictConfig has run. It is removed immediately after so that
|
|
39
|
+
Django's configuration is the sole authority on logging from that point on.
|
|
32
40
|
"""
|
|
33
41
|
global _cold_start_done
|
|
34
42
|
|
|
35
43
|
if _cold_start_done:
|
|
36
44
|
return
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
lambda_tasks_logger = logging.getLogger(__package__)
|
|
47
|
+
boot_handler = logging.StreamHandler()
|
|
48
|
+
boot_handler.setLevel(logging.DEBUG)
|
|
49
|
+
lambda_tasks_logger.addHandler(boot_handler)
|
|
50
|
+
lambda_tasks_logger.setLevel(logging.DEBUG)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
resolve_environment()
|
|
54
|
+
resolve_secrets_into_env()
|
|
55
|
+
finally:
|
|
56
|
+
lambda_tasks_logger.removeHandler(boot_handler)
|
|
57
|
+
lambda_tasks_logger.setLevel(logging.NOTSET)
|
|
58
|
+
|
|
59
|
+
if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
|
|
60
|
+
django.setup()
|
|
47
61
|
|
|
48
62
|
_cold_start_done = True
|
|
49
63
|
|
|
@@ -21,7 +21,7 @@ class _TaskLogger(logging.LoggerAdapter):
|
|
|
21
21
|
"""LoggerAdapter that prepends [message_id] to every message."""
|
|
22
22
|
|
|
23
23
|
def __init__(self) -> None:
|
|
24
|
-
super().__init__(logging.getLogger("
|
|
24
|
+
super().__init__(logging.getLogger(f"{__package__}.task"), extra={})
|
|
25
25
|
self.message_id: str | None = None
|
|
26
26
|
|
|
27
27
|
def process(
|
|
@@ -16,6 +16,7 @@ Validates: Requirements 4.2, 4.3, 4.5
|
|
|
16
16
|
|
|
17
17
|
import inspect
|
|
18
18
|
import json
|
|
19
|
+
import logging
|
|
19
20
|
import uuid
|
|
20
21
|
from unittest.mock import MagicMock, patch
|
|
21
22
|
|
|
@@ -367,3 +368,92 @@ def test_property_4_cold_start_runs_only_once(monkeypatch):
|
|
|
367
368
|
)
|
|
368
369
|
|
|
369
370
|
assert call_count["setup"] == 1
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
# Cold-start logging tests
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class TestColdStartLogging:
|
|
379
|
+
def test_loader_logs_are_emitted_during_cold_start(self, monkeypatch, capfd):
|
|
380
|
+
"""Loader log output is visible during cold start before Django setup."""
|
|
381
|
+
import lambda_tasks.handler as handler_module
|
|
382
|
+
|
|
383
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
384
|
+
|
|
385
|
+
logged_messages: list[str] = []
|
|
386
|
+
|
|
387
|
+
def spy_resolve_environment():
|
|
388
|
+
logging.getLogger("lambda_tasks.environment_loader").info(
|
|
389
|
+
"env loader message"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def spy_resolve_secrets():
|
|
393
|
+
logging.getLogger("lambda_tasks.secret_loader").info(
|
|
394
|
+
"secret loader message"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
monkeypatch.setattr(
|
|
398
|
+
"lambda_tasks.handler.resolve_environment", spy_resolve_environment
|
|
399
|
+
)
|
|
400
|
+
monkeypatch.setattr(
|
|
401
|
+
"lambda_tasks.handler.resolve_secrets_into_env", spy_resolve_secrets
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
lambda_tasks_logger = logging.getLogger("lambda_tasks")
|
|
405
|
+
lambda_tasks_logger.handlers.clear()
|
|
406
|
+
lambda_tasks_logger.setLevel(logging.NOTSET)
|
|
407
|
+
|
|
408
|
+
handler_module._cold_start_done = False
|
|
409
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
410
|
+
|
|
411
|
+
captured = capfd.readouterr()
|
|
412
|
+
assert "env loader message" in captured.err
|
|
413
|
+
assert "secret loader message" in captured.err
|
|
414
|
+
|
|
415
|
+
def test_boot_handler_removed_after_loaders(self, monkeypatch):
|
|
416
|
+
"""The temporary handler is removed after the loaders run, leaving
|
|
417
|
+
the logger clean for Django's LOGGING dictConfig."""
|
|
418
|
+
import lambda_tasks.handler as handler_module
|
|
419
|
+
|
|
420
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
421
|
+
monkeypatch.setattr("lambda_tasks.handler.resolve_environment", lambda: None)
|
|
422
|
+
monkeypatch.setattr(
|
|
423
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
lambda_tasks_logger = logging.getLogger("lambda_tasks")
|
|
427
|
+
lambda_tasks_logger.handlers.clear()
|
|
428
|
+
lambda_tasks_logger.setLevel(logging.NOTSET)
|
|
429
|
+
|
|
430
|
+
handler_module._cold_start_done = False
|
|
431
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
432
|
+
|
|
433
|
+
assert lambda_tasks_logger.handlers == []
|
|
434
|
+
assert lambda_tasks_logger.level == logging.NOTSET
|
|
435
|
+
|
|
436
|
+
def test_boot_handler_removed_even_on_loader_error(self, monkeypatch):
|
|
437
|
+
"""The temporary handler is cleaned up even if a loader raises."""
|
|
438
|
+
import lambda_tasks.handler as handler_module
|
|
439
|
+
|
|
440
|
+
monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False)
|
|
441
|
+
monkeypatch.setattr(
|
|
442
|
+
"lambda_tasks.handler.resolve_environment",
|
|
443
|
+
lambda: (_ for _ in ()).throw(ValueError("bad ref")),
|
|
444
|
+
)
|
|
445
|
+
monkeypatch.setattr(
|
|
446
|
+
"lambda_tasks.handler.resolve_secrets_into_env", lambda: None
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
lambda_tasks_logger = logging.getLogger("lambda_tasks")
|
|
450
|
+
lambda_tasks_logger.handlers.clear()
|
|
451
|
+
lambda_tasks_logger.setLevel(logging.NOTSET)
|
|
452
|
+
|
|
453
|
+
handler_module._cold_start_done = False
|
|
454
|
+
|
|
455
|
+
with pytest.raises(ValueError, match="bad ref"):
|
|
456
|
+
handler_module.handler(event={"Records": []}, context=None)
|
|
457
|
+
|
|
458
|
+
assert lambda_tasks_logger.handlers == []
|
|
459
|
+
assert lambda_tasks_logger.level == logging.NOTSET
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/eager-mode-example-app/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/rse-background-tasks/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/.kiro/specs/task-retry/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.2.2 → django_lambda_tasks-0.2.4}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|