django-lambda-tasks 0.5.0__tar.gz → 0.6.1__tar.gz

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