django-lambda-tasks 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-lambda-tasks might be problematic. Click here for more details.
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/steering/product.md +35 -6
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/steering/structure.md +10 -10
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/PKG-INFO +77 -1
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/README.md +76 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/admin.py +18 -2
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/decorators.py +125 -59
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/handler.py +34 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/models.py +13 -5
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/settings.py +32 -0
- django_lambda_tasks-0.6.0/lambda_tasks/tasks.py +74 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/pyproject.toml +1 -1
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_admin.py +5 -0
- django_lambda_tasks-0.6.0/tests/test_batch.py +210 -0
- django_lambda_tasks-0.6.0/tests/test_batch_decorators.py +158 -0
- django_lambda_tasks-0.6.0/tests/test_batch_worker.py +66 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_local_executor.py +6 -6
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_models.py +21 -25
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_settings.py +78 -0
- django_lambda_tasks-0.5.0/lambda_tasks/tasks.py +0 -23
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.gitignore +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/README.md +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.5.0 → django_lambda_tasks-0.6.0}/tests/test_timeouts.py +0 -0
|
@@ -4,37 +4,58 @@ inclusion: always
|
|
|
4
4
|
|
|
5
5
|
# django-lambda-tasks
|
|
6
6
|
|
|
7
|
-
A Django library for offloading tasks to AWS Lambda outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda worker.
|
|
7
|
+
A Django library for offloading tasks to AWS Lambda or AWS Batch outside of the request-response cycle. Tasks are defined with a decorator, enqueued to SQS on transaction commit, and executed by a Lambda worker or Batch container.
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
11
|
+
### Lambda tasks
|
|
12
|
+
|
|
11
13
|
```
|
|
12
14
|
View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLambdaTaskMessage.execute_immediately() → TaskRecord
|
|
13
15
|
```
|
|
14
16
|
|
|
17
|
+
### Batch tasks
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
View → @batch_task.execute_on_commit() → SQS → Lambda handler → submit_batch_job → batch.submit_job()
|
|
21
|
+
→ Container → python -m lambda_tasks.handler → handler() → execute_immediately() → TaskRecord
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Retry path (Batch):
|
|
25
|
+
```
|
|
26
|
+
Container (retry_on exception) → execute_on_commit() → SQS → Lambda → submit_batch_job → new Batch job
|
|
27
|
+
```
|
|
28
|
+
|
|
15
29
|
Key modules:
|
|
16
|
-
- `decorators.py` —
|
|
17
|
-
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish or local pool submit)
|
|
30
|
+
- `decorators.py` — `BaseTaskWrapper`, `LambdaTaskWrapper`, `BatchTaskWrapper`, `@lambda_task`, `@batch_task`
|
|
31
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish, Batch submit, or local pool submit)
|
|
18
32
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution for development
|
|
19
|
-
- `handler.py` — AWS Lambda entry point
|
|
33
|
+
- `handler.py` — AWS Lambda entry point + AWS Batch container entry point (`main()`)
|
|
20
34
|
- `logging.py` — `task_logger` for invocation-scoped log output
|
|
21
|
-
- `settings.py` — lazy `LambdaTasksSettings` reading from Django settings
|
|
35
|
+
- `settings.py` — `TaskBackend` enum, lazy `LambdaTasksSettings` reading from Django settings
|
|
22
36
|
|
|
23
37
|
## Task Definition
|
|
24
38
|
|
|
25
39
|
```python
|
|
26
|
-
#
|
|
40
|
+
# Lambda task — max timeout 900s
|
|
27
41
|
@lambda_task(retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
|
|
28
42
|
retry_on=(TransientError,),
|
|
29
43
|
singleton=True, retry_singleton=True)
|
|
30
44
|
def my_task(*, user_id: int, action: str) -> None:
|
|
31
45
|
...
|
|
46
|
+
|
|
47
|
+
# Batch task — max timeout 3600s, for long-running or storage-heavy workloads
|
|
48
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500, queue="default",
|
|
49
|
+
retry_on=(TransientError,))
|
|
50
|
+
def heavy_task(*, file_id: int) -> None:
|
|
51
|
+
...
|
|
32
52
|
```
|
|
33
53
|
|
|
34
54
|
- Tasks are resolved at execution time via `import_string` on the fully-qualified task name
|
|
35
55
|
- Task files should be named `tasks.py` within the Django app
|
|
36
56
|
- Direct call `my_task(user_id=1, action="x")` runs synchronously
|
|
37
57
|
- `my_task.execute_on_commit(user_id=1, action="x")` enqueues after transaction commit
|
|
58
|
+
- Both decorators share `BaseTaskWrapper` — same validation, kwargs building, retry, and singleton logic
|
|
38
59
|
|
|
39
60
|
## Enqueuing
|
|
40
61
|
|
|
@@ -132,6 +153,12 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
132
153
|
- A temporary `StreamHandler` is attached to the `lambda_tasks` logger for the duration of the loaders so their log output is visible before Django's `LOGGING` dictConfig has run; it is removed immediately after so that Django's configuration is the sole authority on logging from that point on
|
|
133
154
|
- `resource.setrlimit(RLIMIT_DATA)` is set from `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` so that excessive allocation raises `MemoryError` instead of triggering the OOM killer
|
|
134
155
|
|
|
156
|
+
`main()` in `handler.py` — AWS Batch container entry point (`python -m lambda_tasks.handler`):
|
|
157
|
+
- Reads `LAMBDA_TASKS_MESSAGE` env var (the serialized task JSON, set by `submit_batch_job` via container overrides)
|
|
158
|
+
- Uses `AWS_BATCH_JOB_ID` as the `message_id` (falls back to UUID4 if not set)
|
|
159
|
+
- Constructs a synthetic SQS event and delegates to `handler()` — reuses the full cold-start and execution path
|
|
160
|
+
- Returns exit code 0 on success, 1 on failure
|
|
161
|
+
|
|
135
162
|
## Environment Loader
|
|
136
163
|
|
|
137
164
|
`resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
|
|
@@ -184,6 +211,7 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
|
184
211
|
| Setting | Default | Description |
|
|
185
212
|
|---|---|---|
|
|
186
213
|
| `LAMBDA_TASKS_QUEUES` | — | Dict of `{queue_name: sqs_url}`, must include `"default"` |
|
|
214
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `{}` | Dict of `{queue_name: {"job_queue": ..., "job_definition": ...}}`, must include `"default"` when set |
|
|
187
215
|
| `LAMBDA_TASKS_DEFAULT_SOFT_TIMEOUT` | `270` | Soft timeout in seconds |
|
|
188
216
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `300` | Hard timeout in seconds |
|
|
189
217
|
| `LAMBDA_TASKS_EAGER` | `False` | Run tasks synchronously in-process (no SQS) |
|
|
@@ -261,6 +289,7 @@ def my_task(*, user_id: int) -> None:
|
|
|
261
289
|
`lambda_tasks.tasks` provides maintenance tasks that ship with the library:
|
|
262
290
|
|
|
263
291
|
- `cleanup_task_records(*, retention_days: int = 7) -> int` — deletes `TaskRecord` rows whose `start_time` is strictly older than `retention_days`. Returns the number of deleted rows. Decorated with `@lambda_task` so it can be enqueued via `cleanup_task_records.execute_on_commit()` or called directly. Users are responsible for scheduling (e.g. EventBridge rule, cron, management command).
|
|
292
|
+
- `submit_batch_job(*, message_json: str, batch_queue: str) -> str` — submits a batch task to AWS Batch. Reads `job_queue` and `job_definition` from `LAMBDA_TASKS_BATCH_QUEUES[batch_queue]`, sanitizes the task name for use as `jobName`, and calls `batch.submit_job()` with the message passed as a `LAMBDA_TASKS_MESSAGE` environment variable override. Returns the Batch job ID. This task is enqueued automatically by `SQSLambdaTask._execute()` when `backend == TaskBackend.BATCH` — users do not call it directly.
|
|
264
293
|
|
|
265
294
|
## Conventions
|
|
266
295
|
|
|
@@ -12,15 +12,15 @@ django-lambda-tasks/
|
|
|
12
12
|
│ ├── __init__.py
|
|
13
13
|
│ ├── admin.py # Django admin for TaskRecord
|
|
14
14
|
│ ├── apps.py # Django AppConfig
|
|
15
|
-
│ ├── decorators.py # @lambda_task
|
|
16
|
-
│ ├── handler.py # AWS Lambda entry point
|
|
15
|
+
│ ├── decorators.py # BaseTaskWrapper, LambdaTaskWrapper, BatchTaskWrapper, @lambda_task, @batch_task
|
|
16
|
+
│ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
|
|
17
17
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
18
18
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
19
|
-
│ ├── settings.py # LambdaTasksSettings
|
|
19
|
+
│ ├── settings.py # LambdaTasksSettings, TaskBackend enum, constants
|
|
20
20
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
21
21
|
│ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
|
|
22
22
|
│ ├── local_executor.py # ProcessPoolExecutor for async local task execution
|
|
23
|
-
│ ├── tasks.py # Built-in
|
|
23
|
+
│ ├── tasks.py # Built-in tasks (cleanup_task_records, submit_batch_job)
|
|
24
24
|
│ ├── timeouts.py # TimeoutContext implementation
|
|
25
25
|
│ └── migrations/ # Django migrations for TaskRecord
|
|
26
26
|
├── tests/ # pytest test suite
|
|
@@ -39,23 +39,23 @@ django-lambda-tasks/
|
|
|
39
39
|
|
|
40
40
|
## Module Responsibilities
|
|
41
41
|
|
|
42
|
-
- `decorators.py` — defines `@lambda_task
|
|
43
|
-
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS, executes eagerly, or submits to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
42
|
+
- `decorators.py` — defines `BaseTaskWrapper` (shared validation, kwargs building, execution logic), `LambdaTaskWrapper` and `BatchTaskWrapper` (subclasses that set `_max_timeout` and `_backend`), plus `@lambda_task` and `@batch_task` decorator factories; enforces kwargs-only at decoration time
|
|
43
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing + `backend` field; `_execute()` publishes to SQS, submits to Batch via `submit_batch_job`, executes eagerly, or submits to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
44
44
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
|
|
45
|
-
- `handler.py` — Lambda entry point; cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
|
|
45
|
+
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
|
|
46
46
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
47
47
|
- `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
|
|
48
48
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
49
|
-
- `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
|
|
49
|
+
- `settings.py` — `TaskBackend` StrEnum (`LAMBDA`, `BATCH`); constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `LambdaTasksSettings` instantiated fresh per use (reads live Django settings including `BATCH_QUEUES`)
|
|
50
50
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
51
51
|
- `apps.py` — Django `AppConfig`; `ready()` installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
52
|
-
- `tasks.py` — built-in
|
|
52
|
+
- `tasks.py` — built-in tasks; `cleanup_task_records` deletes old `TaskRecord` rows; `submit_batch_job` receives a serialized task message and submits it to AWS Batch via `batch.submit_job()`
|
|
53
53
|
|
|
54
54
|
## Conventions
|
|
55
55
|
|
|
56
56
|
- Task definitions belong in `tasks.py` within each Django app
|
|
57
57
|
- All task functions must use keyword-only arguments — positional args raise `TypeError` at decoration time
|
|
58
|
-
- Task logic must be self-contained; no Django request context is available in Lambda
|
|
58
|
+
- Task logic must be self-contained; no Django request context is available in Lambda or Batch
|
|
59
59
|
- New modules go inside `lambda_tasks/`; add a corresponding `tests/test_<module>.py`
|
|
60
60
|
- Tests for `models.py` (including `SQSLambdaTask._execute` and `SQSLambdaTask.execute_on_commit`) live in `tests/test_models.py`
|
|
61
61
|
- Do not add application-level code to `main.py` — it is a placeholder only
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -110,12 +110,20 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
110
110
|
| Setting | Type | Description |
|
|
111
111
|
|---|---|---|
|
|
112
112
|
| `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
|
|
113
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `dict[str, dict]` | Map of queue name → `{"job_queue": ..., "job_definition": ...}`. Must include a `"default"` key when set. Default `{}`. |
|
|
113
114
|
|
|
114
115
|
```python
|
|
115
116
|
LAMBDA_TASKS_QUEUES = {
|
|
116
117
|
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
117
118
|
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
118
119
|
}
|
|
120
|
+
|
|
121
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
122
|
+
"default": {
|
|
123
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
124
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
125
|
+
},
|
|
126
|
+
}
|
|
119
127
|
```
|
|
120
128
|
|
|
121
129
|
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
@@ -623,3 +631,71 @@ result = send_welcome_email(user_id=1, template="welcome")
|
|
|
623
631
|
```
|
|
624
632
|
|
|
625
633
|
This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## AWS Batch tasks
|
|
638
|
+
|
|
639
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, use `@batch_task` to route execution to AWS Batch containers.
|
|
640
|
+
|
|
641
|
+
### 1. Define a batch task
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
# myapp/tasks.py
|
|
645
|
+
from lambda_tasks.decorators import batch_task
|
|
646
|
+
|
|
647
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500)
|
|
648
|
+
def process_large_file(*, file_id: int) -> str:
|
|
649
|
+
# long-running work with large disk usage
|
|
650
|
+
return "done"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 2. Configure AWS Batch queues
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
# settings.py
|
|
657
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
658
|
+
"default": {
|
|
659
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
660
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### 3. Enqueue from a view
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
process_large_file.execute_on_commit(file_id=42)
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### How it works
|
|
672
|
+
|
|
673
|
+
1. `execute_on_commit()` enqueues a message to SQS (same as Lambda tasks)
|
|
674
|
+
2. The Lambda handler picks up the message and runs the built-in `submit_batch_job` task
|
|
675
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
676
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
677
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
678
|
+
|
|
679
|
+
### Batch task timeouts
|
|
680
|
+
|
|
681
|
+
Batch tasks support the same two-phase timeout mechanism as Lambda tasks (soft + hard), but with a maximum of **3600 seconds** (1 hour) instead of 900.
|
|
682
|
+
|
|
683
|
+
### Retries
|
|
684
|
+
|
|
685
|
+
Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
|
|
686
|
+
|
|
687
|
+
### Batch job definition setup
|
|
688
|
+
|
|
689
|
+
Your Batch job definition should:
|
|
690
|
+
- Use the same Docker image as your application
|
|
691
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
692
|
+
- Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
|
|
693
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
694
|
+
|
|
695
|
+
### Container overrides limit
|
|
696
|
+
|
|
697
|
+
The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
|
|
698
|
+
|
|
699
|
+
### Eager and local worker modes
|
|
700
|
+
|
|
701
|
+
Batch tasks respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like Lambda tasks, without requiring AWS infrastructure.
|
|
@@ -98,12 +98,20 @@ All settings are read from your Django settings module and prefixed with `LAMBDA
|
|
|
98
98
|
| Setting | Type | Description |
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| `LAMBDA_TASKS_QUEUES` | `dict[str, str]` | Map of queue name → SQS queue URL. Must include a `"default"` key. |
|
|
101
|
+
| `LAMBDA_TASKS_BATCH_QUEUES` | `dict[str, dict]` | Map of queue name → `{"job_queue": ..., "job_definition": ...}`. Must include a `"default"` key when set. Default `{}`. |
|
|
101
102
|
|
|
102
103
|
```python
|
|
103
104
|
LAMBDA_TASKS_QUEUES = {
|
|
104
105
|
"default": "https://sqs.us-east-1.amazonaws.com/123456789/default",
|
|
105
106
|
"high_memory": "https://sqs.us-east-1.amazonaws.com/123456789/high-memory",
|
|
106
107
|
}
|
|
108
|
+
|
|
109
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
110
|
+
"default": {
|
|
111
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
112
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
113
|
+
},
|
|
114
|
+
}
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
`LAMBDA_TASKS_QUEUES` must be set and must include a `"default"` key, otherwise `ImproperlyConfigured` is raised on first use.
|
|
@@ -611,3 +619,71 @@ result = send_welcome_email(user_id=1, template="welcome")
|
|
|
611
619
|
```
|
|
612
620
|
|
|
613
621
|
This bypasses the queue entirely and runs the function in the current process. No `TaskRecord` is created, no `transaction.atomic()` block is used, and no timeout enforcement applies — it behaves exactly like calling the underlying function directly. Kwargs are still validated against the task's type annotations via Pydantic.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## AWS Batch tasks
|
|
626
|
+
|
|
627
|
+
For tasks that exceed Lambda's 15-minute timeout or 10GB ephemeral storage limit, use `@batch_task` to route execution to AWS Batch containers.
|
|
628
|
+
|
|
629
|
+
### 1. Define a batch task
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
# myapp/tasks.py
|
|
633
|
+
from lambda_tasks.decorators import batch_task
|
|
634
|
+
|
|
635
|
+
@batch_task(soft_timeout=1800, hard_timeout=3500)
|
|
636
|
+
def process_large_file(*, file_id: int) -> str:
|
|
637
|
+
# long-running work with large disk usage
|
|
638
|
+
return "done"
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### 2. Configure AWS Batch queues
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
# settings.py
|
|
645
|
+
LAMBDA_TASKS_BATCH_QUEUES = {
|
|
646
|
+
"default": {
|
|
647
|
+
"job_queue": "arn:aws:batch:eu-west-1:123456789:job-queue/my-queue",
|
|
648
|
+
"job_definition": "arn:aws:batch:eu-west-1:123456789:job-definition/my-def:1",
|
|
649
|
+
},
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 3. Enqueue from a view
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
process_large_file.execute_on_commit(file_id=42)
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### How it works
|
|
660
|
+
|
|
661
|
+
1. `execute_on_commit()` enqueues a message to SQS (same as Lambda tasks)
|
|
662
|
+
2. The Lambda handler picks up the message and runs the built-in `submit_batch_job` task
|
|
663
|
+
3. `submit_batch_job` calls `batch.submit_job()` with the task message as a `LAMBDA_TASKS_MESSAGE` environment variable override
|
|
664
|
+
4. The Batch container runs `python -m lambda_tasks.handler`, which reads the env var, performs cold-start init, and calls `execute_immediately()`
|
|
665
|
+
5. The task runs with full timeout enforcement (`SIGALRM`), retry support, and `TaskRecord` tracking
|
|
666
|
+
|
|
667
|
+
### Batch task timeouts
|
|
668
|
+
|
|
669
|
+
Batch tasks support the same two-phase timeout mechanism as Lambda tasks (soft + hard), but with a maximum of **3600 seconds** (1 hour) instead of 900.
|
|
670
|
+
|
|
671
|
+
### Retries
|
|
672
|
+
|
|
673
|
+
Batch tasks support `retry_on` — when a retryable exception is raised, the retry goes back through SQS → Lambda → `submit_batch_job` → new Batch job. The same `LAMBDA_TASKS_MAX_RETRIES` limit applies.
|
|
674
|
+
|
|
675
|
+
### Batch job definition setup
|
|
676
|
+
|
|
677
|
+
Your Batch job definition should:
|
|
678
|
+
- Use the same Docker image as your application
|
|
679
|
+
- Set `DJANGO_SETTINGS_MODULE` as an environment variable
|
|
680
|
+
- Configure secrets via the job definition's `secrets` field or via `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` / `LAMBDA_TASKS_SECRET_*` env vars (same mechanism as Lambda)
|
|
681
|
+
- Set the default command to `["python", "-m", "lambda_tasks.handler"]`
|
|
682
|
+
|
|
683
|
+
### Container overrides limit
|
|
684
|
+
|
|
685
|
+
The total `containerOverrides` payload (including JSON formatting) is limited to 8192 characters by AWS Batch. For typical task kwargs (IDs, short strings) this is not a concern.
|
|
686
|
+
|
|
687
|
+
### Eager and local worker modes
|
|
688
|
+
|
|
689
|
+
Batch tasks respect `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — in development, they run in-process or in the local process pool just like Lambda tasks, without requiring AWS infrastructure.
|
|
@@ -17,6 +17,8 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
17
17
|
list_display = (
|
|
18
18
|
"pk",
|
|
19
19
|
"task_name",
|
|
20
|
+
"backend",
|
|
21
|
+
"queue",
|
|
20
22
|
"status",
|
|
21
23
|
"start_time",
|
|
22
24
|
"end_time",
|
|
@@ -59,10 +61,24 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
59
61
|
total_seconds = d.total_seconds()
|
|
60
62
|
return f"{total_seconds:.3f}s"
|
|
61
63
|
|
|
64
|
+
@admin.display(description="Backend")
|
|
65
|
+
def backend(self, obj: TaskRecord) -> str:
|
|
66
|
+
try:
|
|
67
|
+
return import_string(obj.task_name).backend.value
|
|
68
|
+
except Exception:
|
|
69
|
+
return "?"
|
|
70
|
+
|
|
71
|
+
@admin.display(description="Queue")
|
|
72
|
+
def queue(self, obj: TaskRecord) -> str:
|
|
73
|
+
try:
|
|
74
|
+
return import_string(obj.task_name).queue
|
|
75
|
+
except Exception:
|
|
76
|
+
return "?"
|
|
77
|
+
|
|
62
78
|
@admin.action(description="Replay selected tasks", permissions=("change",))
|
|
63
79
|
def replay_tasks(self, request: HttpRequest, queryset: QuerySet) -> None:
|
|
64
80
|
for record in queryset:
|
|
65
|
-
|
|
81
|
+
wrapper = import_string(record.task_name)
|
|
66
82
|
task = SQSLambdaTask(
|
|
67
83
|
message=SQSLambdaTaskMessage(
|
|
68
84
|
task_name=record.task_name,
|
|
@@ -70,6 +86,6 @@ class TaskRecordAdmin(admin.ModelAdmin):
|
|
|
70
86
|
n_retries=0,
|
|
71
87
|
),
|
|
72
88
|
delay=0,
|
|
73
|
-
queue=queue,
|
|
89
|
+
queue=wrapper.queue,
|
|
74
90
|
)
|
|
75
91
|
task.execute_on_commit()
|