django-lambda-tasks 0.4.10__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/steering/product.md +20 -37
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/PKG-INFO +39 -65
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/README.md +38 -64
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/decorators.py +13 -53
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/environment_loader.py +17 -25
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/models.py +11 -4
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/secret_loader.py +30 -25
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/pyproject.toml +1 -1
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_decorator.py +7 -98
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_decorators.py +76 -57
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_deferred_enqueue.py +5 -5
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_environment_loader.py +11 -20
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_kwargs_only.py +1 -1
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_models.py +51 -279
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_secret_loader.py +15 -20
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.gitignore +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/steering/structure.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/README.md +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.5.0}/tests/test_timeouts.py +0 -0
|
@@ -24,10 +24,9 @@ Key modules:
|
|
|
24
24
|
|
|
25
25
|
```python
|
|
26
26
|
# Always kwargs-only — positional args raise TypeError at decoration time
|
|
27
|
-
@lambda_task(
|
|
28
|
-
ignore_errors=(SomeExpectedException,),
|
|
27
|
+
@lambda_task(retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
|
|
29
28
|
retry_on=(TransientError,),
|
|
30
|
-
singleton=True)
|
|
29
|
+
singleton=True, retry_singleton=True)
|
|
31
30
|
def my_task(*, user_id: int, action: str) -> None:
|
|
32
31
|
...
|
|
33
32
|
```
|
|
@@ -39,34 +38,14 @@ def my_task(*, user_id: int, action: str) -> None:
|
|
|
39
38
|
|
|
40
39
|
## Enqueuing
|
|
41
40
|
|
|
42
|
-
`execute_on_commit()`
|
|
41
|
+
`execute_on_commit()` defaults to delay 0. Pass `_delay=<seconds>` at call time to set the SQS delay for that specific enqueue:
|
|
43
42
|
|
|
44
43
|
```python
|
|
45
|
-
my_task.execute_on_commit(user_id=1, action="x") #
|
|
46
|
-
my_task.execute_on_commit(user_id=1, action="x", _delay=60) #
|
|
44
|
+
my_task.execute_on_commit(user_id=1, action="x") # delay 0
|
|
45
|
+
my_task.execute_on_commit(user_id=1, action="x", _delay=60) # 60s delay
|
|
47
46
|
```
|
|
48
47
|
|
|
49
|
-
The `_delay` override is validated against the
|
|
50
|
-
|
|
51
|
-
## ignore_errors
|
|
52
|
-
|
|
53
|
-
Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor treats it as a non-fatal outcome:
|
|
54
|
-
|
|
55
|
-
- `TaskRecord.status` is set to `SUCCESS`
|
|
56
|
-
- The exception traceback is saved to `TaskRecord.traceback` for observability
|
|
57
|
-
- Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
|
|
58
|
-
- The `TaskRecord` update itself is committed outside the atomic block
|
|
59
|
-
|
|
60
|
-
Exceptions not listed in `ignore_errors` continue to produce `FAILED` with a rollback. Omitting `ignore_errors` (or passing `()`) preserves the existing behaviour.
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
@lambda_task(ignore_errors=(RecordNotFound,))
|
|
64
|
-
def sync_user(*, user_id: int) -> None:
|
|
65
|
-
# RecordNotFound → SUCCESS + traceback recorded; anything else → FAILED
|
|
66
|
-
...
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
`ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
|
|
48
|
+
The `_delay` override is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
|
|
70
49
|
|
|
71
50
|
## retry_on
|
|
72
51
|
|
|
@@ -75,8 +54,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
|
|
|
75
54
|
- `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
|
|
76
55
|
- The retry is a new invocation — the current record is terminal at `RETRYING`
|
|
77
56
|
- Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
78
|
-
- `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
|
|
79
|
-
- `retry_on` and `ignore_errors` must not overlap (exact match or subclass relationship); overlapping raises `TypeError` at decoration time
|
|
80
57
|
|
|
81
58
|
```python
|
|
82
59
|
@lambda_task(retry_on=(RateLimitError, ConnectionError))
|
|
@@ -97,7 +74,8 @@ Pass `singleton=True` on `@lambda_task` to prevent concurrent execution of the s
|
|
|
97
74
|
|
|
98
75
|
- Lock key format: `lambda_tasks.singleton_lock.{task_name}`
|
|
99
76
|
- The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
|
|
100
|
-
- If the lock cannot be acquired (`LockError`), the executor treats it as a retryable exception — same code path as `retry_on`. The `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
|
|
77
|
+
- If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (default), the executor treats it as a retryable exception — same code path as `retry_on`. The `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
|
|
78
|
+
- If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
|
|
101
79
|
- If `n_retries` has reached `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
102
80
|
- The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
|
|
103
81
|
|
|
@@ -106,9 +84,14 @@ Pass `singleton=True` on `@lambda_task` to prevent concurrent execution of the s
|
|
|
106
84
|
def sync_inventory(*, warehouse_id: int) -> None:
|
|
107
85
|
# Only one instance runs at a time; LockError → RETRYING + re-enqueued
|
|
108
86
|
...
|
|
87
|
+
|
|
88
|
+
@lambda_task(singleton=True, retry_singleton=False)
|
|
89
|
+
def sync_inventory(*, warehouse_id: int) -> None:
|
|
90
|
+
# Only one instance runs at a time; LockError → SUCCESS + traceback (no retry)
|
|
91
|
+
...
|
|
109
92
|
```
|
|
110
93
|
|
|
111
|
-
`singleton`
|
|
94
|
+
`singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
|
|
112
95
|
|
|
113
96
|
## SQS Message Schema (`SQSLambdaTaskMessage`)
|
|
114
97
|
|
|
@@ -130,8 +113,8 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
130
113
|
4. If `wrapper.singleton` is `True`, acquires a Redis lock via `caches[SINGLETON_CACHE].lock(lock_key)` wrapping the atomic block; if `False`, no lock is acquired
|
|
131
114
|
5. Runs task inside `transaction.atomic()` + `TimeoutContext`
|
|
132
115
|
6. On success: updates record to `SUCCESS` with result and `end_time`
|
|
133
|
-
7. On
|
|
134
|
-
8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks
|
|
116
|
+
7. On `LockError` when `singleton=True` and `retry_singleton=False`: rolls back task-side writes, commits record as `SUCCESS` with traceback and `end_time`
|
|
117
|
+
8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks with `retry_singleton=True`, `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`
|
|
135
118
|
9. On retryable exception with `n_retries >= MAX_RETRIES`: commits record as `FAILED` with traceback, raises `MaxRetriesExceededError`
|
|
136
119
|
10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback and `end_time`
|
|
137
120
|
|
|
@@ -155,12 +138,12 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
155
138
|
|
|
156
139
|
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.
|
|
157
140
|
|
|
158
|
-
Required format: `<arn>:<version-
|
|
141
|
+
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.
|
|
159
142
|
|
|
160
143
|
Behaviour:
|
|
161
144
|
- If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
|
|
162
145
|
- Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
|
|
163
|
-
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
146
|
+
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
164
147
|
- Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
165
148
|
- Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
|
|
166
149
|
- Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
|
|
@@ -172,12 +155,12 @@ Behaviour:
|
|
|
172
155
|
|
|
173
156
|
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
174
157
|
|
|
175
|
-
Required format: `<arn>:<json-key>:<version-stage>:<version-id>` (10 colon-separated segments
|
|
158
|
+
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.
|
|
176
159
|
|
|
177
160
|
Behaviour:
|
|
178
161
|
- All references are validated before any AWS call — malformed references raise `ValueError` immediately
|
|
179
162
|
- Setting both `LAMBDA_TASKS_SECRET_FOO` and `FOO` is a configuration error and raises `ValueError`
|
|
180
|
-
- Calls are batched by `(ARN, version-
|
|
163
|
+
- Calls are batched by `(ARN, version-id)` — one `GetSecretValue` per unique combination
|
|
181
164
|
- Fetched secrets are cached in-process; warm invocations pay no extra cost
|
|
182
165
|
|
|
183
166
|
## Models
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -74,7 +74,7 @@ def register(request):
|
|
|
74
74
|
You can override the delay for a specific enqueue by passing `_delay`:
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
|
-
# Delay this particular invocation by 60 seconds
|
|
77
|
+
# Delay this particular invocation by 60 seconds
|
|
78
78
|
send_welcome_email.execute_on_commit(user_id=user.id, template="welcome", _delay=60)
|
|
79
79
|
```
|
|
80
80
|
|
|
@@ -150,7 +150,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
150
150
|
|
|
151
151
|
| Setting | Type | Description |
|
|
152
152
|
|---|---|---|
|
|
153
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
153
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
154
154
|
| `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
155
|
|
|
156
156
|
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,14 +203,13 @@ Key characteristics:
|
|
|
203
203
|
|
|
204
204
|
```python
|
|
205
205
|
@lambda_task(
|
|
206
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
207
206
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
208
207
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
209
208
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
210
|
-
ignore_errors=(), # exception types treated as non-fatal
|
|
211
209
|
retry_on=(), # exception types that trigger automatic retry
|
|
212
210
|
retry_delay=0, # base retry delay in seconds (jitter always added, capped at 900)
|
|
213
211
|
singleton=False, # prevent concurrent execution via Redis lock
|
|
212
|
+
retry_singleton=True, # retry on LockError for singleton tasks (or treat as success if False)
|
|
214
213
|
)
|
|
215
214
|
def my_task(*, arg: str) -> None:
|
|
216
215
|
...
|
|
@@ -218,32 +217,31 @@ def my_task(*, arg: str) -> None:
|
|
|
218
217
|
|
|
219
218
|
| Parameter | Type | Default | Description |
|
|
220
219
|
|---|---|---|---|
|
|
221
|
-
| `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
|
|
222
220
|
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
|
|
223
221
|
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
|
|
224
222
|
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
225
|
-
| `ignore_errors` | `tuple[type[BaseException], ...]` | `()` | Exception types to treat as non-fatal (see [Ignored exceptions](#ignored-exceptions)). |
|
|
226
223
|
| `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
|
|
227
224
|
| `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
|
|
228
225
|
| `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
|
|
226
|
+
| `retry_singleton` | `bool` | `True` | When `True`, `LockError` on a singleton task triggers a retry. When `False`, lock contention is treated as a successful no-op (traceback recorded). |
|
|
229
227
|
|
|
230
228
|
### Per-call delay override
|
|
231
229
|
|
|
232
|
-
|
|
230
|
+
By default, `execute_on_commit()` uses a delay of 0 seconds. To set the SQS `DelaySeconds` for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
|
|
233
231
|
|
|
234
232
|
```python
|
|
235
|
-
@lambda_task
|
|
233
|
+
@lambda_task
|
|
236
234
|
def notify_user(*, user_id: int) -> None:
|
|
237
235
|
...
|
|
238
236
|
|
|
239
|
-
#
|
|
237
|
+
# Default (0 seconds delay)
|
|
240
238
|
notify_user.execute_on_commit(user_id=1)
|
|
241
239
|
|
|
242
|
-
#
|
|
240
|
+
# Delay this specific invocation by 120 seconds
|
|
243
241
|
notify_user.execute_on_commit(user_id=1, _delay=120)
|
|
244
242
|
```
|
|
245
243
|
|
|
246
|
-
`_delay` is validated against the
|
|
244
|
+
`_delay` is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
|
|
247
245
|
|
|
248
246
|
---
|
|
249
247
|
|
|
@@ -386,35 +384,6 @@ fields @timestamp, @message
|
|
|
386
384
|
|
|
387
385
|
---
|
|
388
386
|
|
|
389
|
-
## Ignored exceptions
|
|
390
|
-
|
|
391
|
-
Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor treats it as a non-fatal outcome:
|
|
392
|
-
|
|
393
|
-
- `TaskRecord.status` is set to `SUCCESS`
|
|
394
|
-
- The exception traceback is saved to `TaskRecord.traceback` for observability
|
|
395
|
-
- Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
|
|
396
|
-
- The `TaskRecord` update is committed outside the atomic block
|
|
397
|
-
|
|
398
|
-
Exceptions not in `ignore_errors` continue to produce `FAILED` with a rollback. The default (`()`) preserves existing behaviour.
|
|
399
|
-
|
|
400
|
-
```python
|
|
401
|
-
from lambda_tasks.decorators import lambda_task
|
|
402
|
-
|
|
403
|
-
class RecordNotFound(Exception):
|
|
404
|
-
pass
|
|
405
|
-
|
|
406
|
-
@lambda_task(ignore_errors=(RecordNotFound,))
|
|
407
|
-
def sync_user(*, user_id: int) -> None:
|
|
408
|
-
user = fetch_user(user_id) # raises RecordNotFound if already deleted
|
|
409
|
-
update_profile(user)
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
If `sync_user` raises `RecordNotFound`, the `TaskRecord` will have `status=SUCCESS` and the traceback recorded in `traceback`. Any ORM writes made before the exception are rolled back.
|
|
413
|
-
|
|
414
|
-
`ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
|
|
415
|
-
|
|
416
|
-
---
|
|
417
|
-
|
|
418
387
|
## Automatic retries
|
|
419
388
|
|
|
420
389
|
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor re-enqueues the task with an incremented retry counter:
|
|
@@ -422,8 +391,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
|
|
|
422
391
|
- `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
|
|
423
392
|
- The retry is a new invocation — the current record is terminal at `RETRYING`
|
|
424
393
|
- Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES` (default `2880`), at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
425
|
-
- `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
|
|
426
|
-
- `retry_on` and `ignore_errors` must not overlap; overlapping raises `TypeError` at decoration time
|
|
427
394
|
|
|
428
395
|
```python
|
|
429
396
|
from lambda_tasks.decorators import lambda_task
|
|
@@ -450,19 +417,27 @@ from lambda_tasks.decorators import lambda_task
|
|
|
450
417
|
|
|
451
418
|
@lambda_task(singleton=True)
|
|
452
419
|
def sync_inventory(*, warehouse_id: int) -> None:
|
|
453
|
-
# Only one instance runs at a time
|
|
420
|
+
# Only one instance runs at a time; LockError → RETRYING + re-enqueued
|
|
454
421
|
...
|
|
455
422
|
```
|
|
456
423
|
|
|
457
424
|
- Lock key format: `lambda_tasks.singleton_lock.{task_name}`
|
|
458
425
|
- The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
|
|
459
|
-
- If the lock cannot be acquired (`LockError`), the task is retried
|
|
426
|
+
- If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (the default), the task is retried — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
|
|
427
|
+
- If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
|
|
460
428
|
- If `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
461
429
|
- The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
|
|
462
430
|
|
|
463
|
-
`LockError` is
|
|
431
|
+
`LockError` is handled automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time).
|
|
432
|
+
|
|
433
|
+
`singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
|
|
464
434
|
|
|
465
|
-
|
|
435
|
+
```python
|
|
436
|
+
@lambda_task(singleton=True, retry_singleton=False)
|
|
437
|
+
def sync_inventory(*, warehouse_id: int) -> None:
|
|
438
|
+
# Lock contention → SUCCESS with traceback (no retry)
|
|
439
|
+
...
|
|
440
|
+
```
|
|
466
441
|
|
|
467
442
|
---
|
|
468
443
|
|
|
@@ -470,8 +445,6 @@ def sync_inventory(*, warehouse_id: int) -> None:
|
|
|
470
445
|
|
|
471
446
|
Each task runs inside a `django.db.transaction.atomic` block. If the task raises an unhandled exception, all ORM writes made inside the task are rolled back. The `TaskRecord` failure update is always written outside the atomic block so it survives the rollback.
|
|
472
447
|
|
|
473
|
-
When an exception matches `ignore_errors`, the same rollback applies to task-side writes — the atomic block still exits via exception. Only the `TaskRecord` update (written outside the block) is committed, recording the `SUCCESS` outcome and traceback.
|
|
474
|
-
|
|
475
448
|
---
|
|
476
449
|
|
|
477
450
|
## Error handling
|
|
@@ -519,20 +492,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
519
492
|
| Environment Variable | Required | Description |
|
|
520
493
|
|---|---|---|
|
|
521
494
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
522
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
495
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
523
496
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
524
497
|
|
|
525
498
|
### Loading environment variables from Secrets Manager
|
|
526
499
|
|
|
527
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.
|
|
528
501
|
|
|
529
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
502
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
530
503
|
|
|
531
504
|
```
|
|
532
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
505
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
533
506
|
```
|
|
534
507
|
|
|
535
|
-
The format is `<arn>:<version-
|
|
508
|
+
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.
|
|
536
509
|
|
|
537
510
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
538
511
|
|
|
@@ -547,8 +520,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
547
520
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
548
521
|
|
|
549
522
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
550
|
-
2. Parses and validates the reference format (
|
|
551
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
523
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
524
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
552
525
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
553
526
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
554
527
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -559,7 +532,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
559
532
|
|
|
560
533
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
561
534
|
|
|
562
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
535
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
563
536
|
- Secret value is not valid JSON
|
|
564
537
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
565
538
|
- JSON contains an empty string key
|
|
@@ -573,14 +546,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
573
546
|
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:
|
|
574
547
|
|
|
575
548
|
```
|
|
576
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
549
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
577
550
|
```
|
|
578
551
|
|
|
579
552
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
580
553
|
|
|
581
554
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
582
555
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
583
|
-
3. Groups references by `(ARN, version-
|
|
556
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
584
557
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
585
558
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
586
559
|
|
|
@@ -592,18 +565,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
592
565
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
593
566
|
```
|
|
594
567
|
|
|
595
|
-
|
|
568
|
+
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.
|
|
596
569
|
|
|
597
570
|
```
|
|
598
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
599
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
571
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
572
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
600
573
|
```
|
|
601
574
|
|
|
602
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
575
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
603
576
|
|
|
604
577
|
```
|
|
605
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
606
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
578
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
579
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
607
580
|
```
|
|
608
581
|
|
|
609
582
|
#### Validation errors
|
|
@@ -611,7 +584,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
611
584
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
612
585
|
|
|
613
586
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
614
|
-
- Empty `json-key
|
|
587
|
+
- Empty `json-key` or `version-id`
|
|
588
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
615
589
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
616
590
|
- The named JSON key does not exist in the fetched secret
|
|
617
591
|
- The secret value is not valid JSON
|
|
@@ -62,7 +62,7 @@ def register(request):
|
|
|
62
62
|
You can override the delay for a specific enqueue by passing `_delay`:
|
|
63
63
|
|
|
64
64
|
```python
|
|
65
|
-
# Delay this particular invocation by 60 seconds
|
|
65
|
+
# Delay this particular invocation by 60 seconds
|
|
66
66
|
send_welcome_email.execute_on_commit(user_id=user.id, template="welcome", _delay=60)
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -138,7 +138,7 @@ LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT = 270
|
|
|
138
138
|
|
|
139
139
|
| Setting | Type | Description |
|
|
140
140
|
|---|---|---|
|
|
141
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-
|
|
141
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | env var | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at Lambda cold start. |
|
|
142
142
|
| `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
143
|
|
|
144
144
|
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,14 +191,13 @@ Key characteristics:
|
|
|
191
191
|
|
|
192
192
|
```python
|
|
193
193
|
@lambda_task(
|
|
194
|
-
delay=0, # seconds — SQS DelaySeconds before message becomes visible
|
|
195
194
|
soft_timeout=60, # seconds — overrides global default for this task
|
|
196
195
|
hard_timeout=90, # seconds — overrides global default for this task
|
|
197
196
|
queue="default", # named queue from LAMBDA_TASKS_QUEUES
|
|
198
|
-
ignore_errors=(), # exception types treated as non-fatal
|
|
199
197
|
retry_on=(), # exception types that trigger automatic retry
|
|
200
198
|
retry_delay=0, # base retry delay in seconds (jitter always added, capped at 900)
|
|
201
199
|
singleton=False, # prevent concurrent execution via Redis lock
|
|
200
|
+
retry_singleton=True, # retry on LockError for singleton tasks (or treat as success if False)
|
|
202
201
|
)
|
|
203
202
|
def my_task(*, arg: str) -> None:
|
|
204
203
|
...
|
|
@@ -206,32 +205,31 @@ def my_task(*, arg: str) -> None:
|
|
|
206
205
|
|
|
207
206
|
| Parameter | Type | Default | Description |
|
|
208
207
|
|---|---|---|---|
|
|
209
|
-
| `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
|
|
210
208
|
| `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
|
|
211
209
|
| `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
|
|
212
210
|
| `queue` | `str` | `"default"` | Named queue to route this task to. |
|
|
213
|
-
| `ignore_errors` | `tuple[type[BaseException], ...]` | `()` | Exception types to treat as non-fatal (see [Ignored exceptions](#ignored-exceptions)). |
|
|
214
211
|
| `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
|
|
215
212
|
| `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
|
|
216
213
|
| `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
|
|
214
|
+
| `retry_singleton` | `bool` | `True` | When `True`, `LockError` on a singleton task triggers a retry. When `False`, lock contention is treated as a successful no-op (traceback recorded). |
|
|
217
215
|
|
|
218
216
|
### Per-call delay override
|
|
219
217
|
|
|
220
|
-
|
|
218
|
+
By default, `execute_on_commit()` uses a delay of 0 seconds. To set the SQS `DelaySeconds` for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
|
|
221
219
|
|
|
222
220
|
```python
|
|
223
|
-
@lambda_task
|
|
221
|
+
@lambda_task
|
|
224
222
|
def notify_user(*, user_id: int) -> None:
|
|
225
223
|
...
|
|
226
224
|
|
|
227
|
-
#
|
|
225
|
+
# Default (0 seconds delay)
|
|
228
226
|
notify_user.execute_on_commit(user_id=1)
|
|
229
227
|
|
|
230
|
-
#
|
|
228
|
+
# Delay this specific invocation by 120 seconds
|
|
231
229
|
notify_user.execute_on_commit(user_id=1, _delay=120)
|
|
232
230
|
```
|
|
233
231
|
|
|
234
|
-
`_delay` is validated against the
|
|
232
|
+
`_delay` is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
|
|
235
233
|
|
|
236
234
|
---
|
|
237
235
|
|
|
@@ -374,35 +372,6 @@ fields @timestamp, @message
|
|
|
374
372
|
|
|
375
373
|
---
|
|
376
374
|
|
|
377
|
-
## Ignored exceptions
|
|
378
|
-
|
|
379
|
-
Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor treats it as a non-fatal outcome:
|
|
380
|
-
|
|
381
|
-
- `TaskRecord.status` is set to `SUCCESS`
|
|
382
|
-
- The exception traceback is saved to `TaskRecord.traceback` for observability
|
|
383
|
-
- Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
|
|
384
|
-
- The `TaskRecord` update is committed outside the atomic block
|
|
385
|
-
|
|
386
|
-
Exceptions not in `ignore_errors` continue to produce `FAILED` with a rollback. The default (`()`) preserves existing behaviour.
|
|
387
|
-
|
|
388
|
-
```python
|
|
389
|
-
from lambda_tasks.decorators import lambda_task
|
|
390
|
-
|
|
391
|
-
class RecordNotFound(Exception):
|
|
392
|
-
pass
|
|
393
|
-
|
|
394
|
-
@lambda_task(ignore_errors=(RecordNotFound,))
|
|
395
|
-
def sync_user(*, user_id: int) -> None:
|
|
396
|
-
user = fetch_user(user_id) # raises RecordNotFound if already deleted
|
|
397
|
-
update_profile(user)
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
If `sync_user` raises `RecordNotFound`, the `TaskRecord` will have `status=SUCCESS` and the traceback recorded in `traceback`. Any ORM writes made before the exception are rolled back.
|
|
401
|
-
|
|
402
|
-
`ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
|
|
403
|
-
|
|
404
|
-
---
|
|
405
|
-
|
|
406
375
|
## Automatic retries
|
|
407
376
|
|
|
408
377
|
Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor re-enqueues the task with an incremented retry counter:
|
|
@@ -410,8 +379,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
|
|
|
410
379
|
- `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
|
|
411
380
|
- The retry is a new invocation — the current record is terminal at `RETRYING`
|
|
412
381
|
- Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES` (default `2880`), at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
413
|
-
- `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
|
|
414
|
-
- `retry_on` and `ignore_errors` must not overlap; overlapping raises `TypeError` at decoration time
|
|
415
382
|
|
|
416
383
|
```python
|
|
417
384
|
from lambda_tasks.decorators import lambda_task
|
|
@@ -438,19 +405,27 @@ from lambda_tasks.decorators import lambda_task
|
|
|
438
405
|
|
|
439
406
|
@lambda_task(singleton=True)
|
|
440
407
|
def sync_inventory(*, warehouse_id: int) -> None:
|
|
441
|
-
# Only one instance runs at a time
|
|
408
|
+
# Only one instance runs at a time; LockError → RETRYING + re-enqueued
|
|
442
409
|
...
|
|
443
410
|
```
|
|
444
411
|
|
|
445
412
|
- Lock key format: `lambda_tasks.singleton_lock.{task_name}`
|
|
446
413
|
- The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
|
|
447
|
-
- If the lock cannot be acquired (`LockError`), the task is retried
|
|
414
|
+
- If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (the default), the task is retried — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
|
|
415
|
+
- If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
|
|
448
416
|
- If `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
|
|
449
417
|
- The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
|
|
450
418
|
|
|
451
|
-
`LockError` is
|
|
419
|
+
`LockError` is handled automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time).
|
|
420
|
+
|
|
421
|
+
`singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
|
|
452
422
|
|
|
453
|
-
|
|
423
|
+
```python
|
|
424
|
+
@lambda_task(singleton=True, retry_singleton=False)
|
|
425
|
+
def sync_inventory(*, warehouse_id: int) -> None:
|
|
426
|
+
# Lock contention → SUCCESS with traceback (no retry)
|
|
427
|
+
...
|
|
428
|
+
```
|
|
454
429
|
|
|
455
430
|
---
|
|
456
431
|
|
|
@@ -458,8 +433,6 @@ def sync_inventory(*, warehouse_id: int) -> None:
|
|
|
458
433
|
|
|
459
434
|
Each task runs inside a `django.db.transaction.atomic` block. If the task raises an unhandled exception, all ORM writes made inside the task are rolled back. The `TaskRecord` failure update is always written outside the atomic block so it survives the rollback.
|
|
460
435
|
|
|
461
|
-
When an exception matches `ignore_errors`, the same rollback applies to task-side writes — the atomic block still exits via exception. Only the `TaskRecord` update (written outside the block) is committed, recording the `SUCCESS` outcome and traceback.
|
|
462
|
-
|
|
463
436
|
---
|
|
464
437
|
|
|
465
438
|
## Error handling
|
|
@@ -507,20 +480,20 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
507
480
|
| Environment Variable | Required | Description |
|
|
508
481
|
|---|---|---|
|
|
509
482
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
510
|
-
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-
|
|
483
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-id>`) to load as environment variables at cold start (runs first). |
|
|
511
484
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into individual env vars at cold start (runs second, after environment loading). |
|
|
512
485
|
|
|
513
486
|
### Loading environment variables from Secrets Manager
|
|
514
487
|
|
|
515
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.
|
|
516
489
|
|
|
517
|
-
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version
|
|
490
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version ID:
|
|
518
491
|
|
|
519
492
|
```
|
|
520
|
-
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:
|
|
493
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:v1
|
|
521
494
|
```
|
|
522
495
|
|
|
523
|
-
The format is `<arn>:<version-
|
|
496
|
+
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.
|
|
524
497
|
|
|
525
498
|
The secret value must be a flat JSON object where all keys and values are strings:
|
|
526
499
|
|
|
@@ -535,8 +508,8 @@ The secret value must be a flat JSON object where all keys and values are string
|
|
|
535
508
|
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
536
509
|
|
|
537
510
|
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
538
|
-
2. Parses and validates the reference format (
|
|
539
|
-
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=...,
|
|
511
|
+
2. Parses and validates the reference format (8 segments, non-empty version-id)
|
|
512
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionId=...)`
|
|
540
513
|
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
541
514
|
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
542
515
|
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
@@ -547,7 +520,7 @@ Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_
|
|
|
547
520
|
|
|
548
521
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
549
522
|
|
|
550
|
-
- Reference format is invalid (wrong segment count, empty version-
|
|
523
|
+
- Reference format is invalid (wrong segment count, empty version-id)
|
|
551
524
|
- Secret value is not valid JSON
|
|
552
525
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
553
526
|
- JSON contains an empty string key
|
|
@@ -561,14 +534,14 @@ The Lambda handler supports loading individual secret values from AWS Secrets Ma
|
|
|
561
534
|
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:
|
|
562
535
|
|
|
563
536
|
```
|
|
564
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
537
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
565
538
|
```
|
|
566
539
|
|
|
567
540
|
At cold start (on the first handler invocation), after `resolve_environment()` and before `django.setup()`, the handler calls `resolve_secrets_into_env()` which:
|
|
568
541
|
|
|
569
542
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
570
543
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
571
|
-
3. Groups references by `(ARN, version-
|
|
544
|
+
3. Groups references by `(ARN, version-id)` and makes one `GetSecretValue` call per unique combination
|
|
572
545
|
4. Extracts the named JSON key from the secret and writes it into `os.environ`
|
|
573
546
|
5. Caches fetched secrets in-process — warm invocations pay no extra cost
|
|
574
547
|
|
|
@@ -580,18 +553,18 @@ Every value must follow the full dynamic reference syntax:
|
|
|
580
553
|
<arn>:<json-key>:<version-stage>:<version-id>
|
|
581
554
|
```
|
|
582
555
|
|
|
583
|
-
|
|
556
|
+
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.
|
|
584
557
|
|
|
585
558
|
```
|
|
586
|
-
# arn (7 segments) : json-key : version-stage : version-id
|
|
587
|
-
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL
|
|
559
|
+
# arn (7 segments) : json-key : version-stage (empty) : version-id
|
|
560
|
+
arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL::v1
|
|
588
561
|
```
|
|
589
562
|
|
|
590
|
-
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-
|
|
563
|
+
Multiple env vars can reference different keys from the same secret — only one `GetSecretValue` call is made for that `(ARN, version-id)` combination:
|
|
591
564
|
|
|
592
565
|
```
|
|
593
|
-
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL
|
|
594
|
-
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY
|
|
566
|
+
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:...:myapp/prod:DATABASE_URL::v1
|
|
567
|
+
LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY::v1
|
|
595
568
|
```
|
|
596
569
|
|
|
597
570
|
#### Validation errors
|
|
@@ -599,7 +572,8 @@ LAMBDA_TASKS_SECRET_SECRET_KEY=arn:...:myapp/prod:SECRET_KEY:AWSCURRENT:v1
|
|
|
599
572
|
The following all raise `ValueError` at cold start, preventing the Lambda container from starting with a misconfigured environment:
|
|
600
573
|
|
|
601
574
|
- Wrong number of colon-separated segments (must be exactly 10)
|
|
602
|
-
- Empty `json-key
|
|
575
|
+
- Empty `json-key` or `version-id`
|
|
576
|
+
- Non-empty `version-stage` (version-stage is not supported)
|
|
603
577
|
- Both `LAMBDA_TASKS_SECRET_FOO` and `FOO` are set — use one or the other
|
|
604
578
|
- The named JSON key does not exist in the fetched secret
|
|
605
579
|
- The secret value is not valid JSON
|