django-lambda-tasks 0.6.6__tar.gz → 0.7.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.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/product.md +33 -3
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/structure.md +4 -2
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/PKG-INFO +36 -4
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/README.md +35 -3
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/apps.py +1 -0
- django_lambda_tasks-0.7.0/lambda_tasks/checks.py +45 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/handler.py +2 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/models.py +10 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/settings.py +13 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/pyproject.toml +1 -1
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_handler.py +76 -0
- django_lambda_tasks-0.7.0/tests/test_noop_execution.py +165 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.gitignore +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/README.md +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_timeouts.py +0 -0
|
@@ -143,6 +143,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
143
143
|
|
|
144
144
|
`handler(event, context)` in `handler.py`:
|
|
145
145
|
- Processes each SQS record independently
|
|
146
|
+
- Calls `django.db.close_old_connections()` before each record to close database connections that have become unusable between warm invocations (idle timeouts, RDS failovers, network interruptions)
|
|
146
147
|
- Passes `record["messageId"]` as `message_id` to `execute_immediately()` — this becomes the `TaskRecord` primary key, enabling deduplication on redelivery
|
|
147
148
|
- Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
|
|
148
149
|
- Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
|
|
@@ -215,11 +216,25 @@ Statuses: `RUNNING`, `SUCCESS`, `FAILED`, `RETRYING`
|
|
|
215
216
|
| `LAMBDA_TASKS_DEFAULT_HARD_TIMEOUT` | `300` | Hard timeout in seconds |
|
|
216
217
|
| `LAMBDA_TASKS_EAGER` | `False` | Run tasks synchronously in-process (no SQS) |
|
|
217
218
|
| `LAMBDA_TASKS_LOCAL_WORKERS` | `0` | Number of worker processes for async local execution (development only; mutually exclusive with `EAGER`) |
|
|
219
|
+
| `LAMBDA_TASKS_NOOP_EXECUTION` | `False` | Drop all tasks silently (for local tests; mutually exclusive with `EAGER` and `LOCAL_WORKERS`) |
|
|
218
220
|
| `LAMBDA_TASKS_MAX_RETRIES` | `2880` | Maximum retry attempts before `MaxRetriesExceededError` is raised (60 × 24 × 2) |
|
|
219
221
|
| `LAMBDA_TASKS_SINGLETON_CACHE` | `"default"` | Django cache backend used for singleton task locks |
|
|
220
222
|
|
|
221
223
|
`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.
|
|
222
224
|
|
|
225
|
+
## Noop Execution Mode
|
|
226
|
+
|
|
227
|
+
Set `LAMBDA_TASKS_NOOP_EXECUTION = True` to silently drop all tasks. Useful for local tests where task execution is irrelevant. When a task is dispatched, a warning is logged with the task name, kwargs, and queue, and execution returns immediately — no SQS message is sent, no task is executed.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
# settings/test.py
|
|
231
|
+
LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`LAMBDA_TASKS_NOOP_EXECUTION` can only be set when `LAMBDA_TASKS_EAGER` is `False` and `LAMBDA_TASKS_LOCAL_WORKERS` is `0`. Setting it alongside either raises `ImproperlyConfigured`.
|
|
235
|
+
|
|
236
|
+
A Django deployment check (`lambda_tasks.W001`) warns if this setting is enabled, ensuring it is not accidentally left on in production.
|
|
237
|
+
|
|
223
238
|
## Eager Mode
|
|
224
239
|
|
|
225
240
|
Set `LAMBDA_TASKS_EAGER = True` to run tasks synchronously in-process (no SQS). Useful for local development and tests.
|
|
@@ -238,12 +253,15 @@ LAMBDA_TASKS_LOCAL_WORKERS = 4
|
|
|
238
253
|
```
|
|
239
254
|
|
|
240
255
|
The execution mode hierarchy is:
|
|
241
|
-
1. **
|
|
242
|
-
2. **
|
|
243
|
-
3. **
|
|
256
|
+
1. **Noop mode** (`LAMBDA_TASKS_NOOP_EXECUTION=True`) — tasks are dropped with a warning log
|
|
257
|
+
2. **Eager mode** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
|
|
258
|
+
3. **Async local mode** (`LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
|
|
259
|
+
4. **SQS mode** (default) — async, Lambda workers, timeouts enforced
|
|
244
260
|
|
|
245
261
|
`LAMBDA_TASKS_LOCAL_WORKERS` and `LAMBDA_TASKS_EAGER` are mutually exclusive — setting both raises `ImproperlyConfigured`. A negative value also raises `ImproperlyConfigured`.
|
|
246
262
|
|
|
263
|
+
`LAMBDA_TASKS_NOOP_EXECUTION` is mutually exclusive with both `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS`.
|
|
264
|
+
|
|
247
265
|
Key behaviours:
|
|
248
266
|
- The process pool is created lazily on first task submission and reused for the server lifetime
|
|
249
267
|
- Each worker process calls `django.setup()` once via the pool initializer
|
|
@@ -267,6 +285,18 @@ Relying on `atexit` alone loses this race in applications with a heavy shutdown
|
|
|
267
285
|
|
|
268
286
|
`_shutdown_pool()` does **not** use `pool.shutdown(wait=True)`. `wait=True` blocks on joining the worker processes, and a worker that ran a heavy `django.setup()` is slow to exit — `concurrent.futures` does not unlink the pool's queue semaphores until the workers actually die, so the parent's near-instant `SIGKILL` still wins and the semaphores leak. Instead `_shutdown_pool()` **terminates the worker processes first** (a near-instant signal to children we own), then calls `pool.shutdown(wait=False, cancel_futures=True)`. With the workers already gone, the queue semaphores are unlinked in milliseconds — fast enough to beat the parent's `SIGKILL`.
|
|
269
287
|
|
|
288
|
+
## Deployment Checks
|
|
289
|
+
|
|
290
|
+
`lambda_tasks.checks` registers a Django deployment check (runs with `manage.py check --deploy`) that warns if any non-production execution mode is enabled:
|
|
291
|
+
|
|
292
|
+
| ID | Condition | Message |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `lambda_tasks.W001` | `LAMBDA_TASKS_NOOP_EXECUTION = True` | Tasks will be silently dropped |
|
|
295
|
+
| `lambda_tasks.W002` | `LAMBDA_TASKS_EAGER = True` | Tasks will run synchronously in-process |
|
|
296
|
+
| `lambda_tasks.W003` | `LAMBDA_TASKS_LOCAL_WORKERS > 0` | Tasks will run in a local process pool |
|
|
297
|
+
|
|
298
|
+
The checks are registered via `AppConfig.ready()` and tagged with `deploy=True` so they only fire during `--deploy` checks.
|
|
299
|
+
|
|
270
300
|
## Logging
|
|
271
301
|
|
|
272
302
|
Import `task_logger` to emit log records that are automatically prefixed with the active `message_id`:
|
|
@@ -12,6 +12,7 @@ django-lambda-tasks/
|
|
|
12
12
|
│ ├── __init__.py
|
|
13
13
|
│ ├── admin.py # Django admin for TaskRecord
|
|
14
14
|
│ ├── apps.py # Django AppConfig
|
|
15
|
+
│ ├── checks.py # Django deployment checks (noop, eager, local_workers)
|
|
15
16
|
│ ├── decorators.py # LambdaTaskWrapper, @lambda_task
|
|
16
17
|
│ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
|
|
17
18
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
@@ -42,13 +43,14 @@ django-lambda-tasks/
|
|
|
42
43
|
- `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
44
|
- `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
45
|
- `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 (`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
|
+
- `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; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; 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
47
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
47
48
|
- `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
49
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
49
50
|
- `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
51
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
51
|
-
- `apps.py` — Django `AppConfig`; `ready()` installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
52
|
+
- `apps.py` — Django `AppConfig`; `ready()` imports `checks` to register deployment checks and installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
53
|
+
- `checks.py` — Django deployment checks; `check_noop_not_in_production` warns if `NOOP_EXECUTION`, `EAGER`, or `LOCAL_WORKERS > 0` are set (runs with `manage.py check --deploy`)
|
|
52
54
|
- `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
55
|
|
|
54
56
|
## Conventions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -193,9 +193,10 @@ LAMBDA_TASKS_LOCAL_WORKERS = 4
|
|
|
193
193
|
Async local mode bridges the gap between eager mode and full SQS/Lambda deployment. Tasks execute in background worker processes with true parallelism and full timeout enforcement via `SIGALRM`, without requiring AWS infrastructure.
|
|
194
194
|
|
|
195
195
|
The execution mode hierarchy is:
|
|
196
|
-
1. **
|
|
197
|
-
2. **
|
|
198
|
-
3. **
|
|
196
|
+
1. **Noop** (`LAMBDA_TASKS_NOOP_EXECUTION=True`) — tasks are dropped with a warning log
|
|
197
|
+
2. **Eager** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
|
|
198
|
+
3. **Async local** (`LAMBDA_TASKS_LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
|
|
199
|
+
4. **SQS** (default) — async, Lambda workers, timeouts enforced
|
|
199
200
|
|
|
200
201
|
Key characteristics:
|
|
201
202
|
- `LAMBDA_TASKS_LOCAL_WORKERS` and `LAMBDA_TASKS_EAGER` are mutually exclusive — setting both raises `ImproperlyConfigured`
|
|
@@ -208,6 +209,35 @@ Key characteristics:
|
|
|
208
209
|
|
|
209
210
|
---
|
|
210
211
|
|
|
212
|
+
### Noop execution (testing)
|
|
213
|
+
|
|
214
|
+
| Setting | Type | Default | Description |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| `LAMBDA_TASKS_NOOP_EXECUTION` | `bool` | `False` | When `True`, all tasks are silently dropped. A warning is logged with the task name, kwargs, and queue. |
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
# settings/test.py
|
|
220
|
+
LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Noop mode is useful for test suites where task execution is irrelevant and you want to avoid mocking SQS or running tasks at all. When a task is dispatched via `execute_on_commit()`, it logs a warning and returns immediately — no SQS message is sent, no task is executed, no `TaskRecord` is created.
|
|
224
|
+
|
|
225
|
+
`LAMBDA_TASKS_NOOP_EXECUTION` is mutually exclusive with both `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — setting it alongside either raises `ImproperlyConfigured`.
|
|
226
|
+
|
|
227
|
+
### Deployment checks
|
|
228
|
+
|
|
229
|
+
A Django deployment check (runs with `manage.py check --deploy`) warns if any non-production execution mode is enabled:
|
|
230
|
+
|
|
231
|
+
| Check ID | Condition | Message |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `lambda_tasks.W001` | `LAMBDA_TASKS_NOOP_EXECUTION = True` | Tasks will be silently dropped |
|
|
234
|
+
| `lambda_tasks.W002` | `LAMBDA_TASKS_EAGER = True` | Tasks will run synchronously in-process |
|
|
235
|
+
| `lambda_tasks.W003` | `LAMBDA_TASKS_LOCAL_WORKERS > 0` | Tasks will run in a local process pool |
|
|
236
|
+
|
|
237
|
+
Add `manage.py check --deploy` to your CI pipeline to catch these misconfigurations before they reach production.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
211
241
|
## Decorator options
|
|
212
242
|
|
|
213
243
|
```python
|
|
@@ -517,6 +547,8 @@ lambda_tasks.handler.handler
|
|
|
517
547
|
|
|
518
548
|
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
519
549
|
|
|
550
|
+
Before processing each SQS record, the handler calls `django.db.close_old_connections()` to close any database connections that have become unusable between invocations. Lambda containers are reused across invocations (warm starts), but database connections may be closed server-side due to idle timeouts, RDS failovers, or network interruptions. Without this, the first database query in a task would fail with `OperationalError: the connection is closed`.
|
|
551
|
+
|
|
520
552
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
521
553
|
|
|
522
554
|
| Environment Variable | Required | Description |
|
|
@@ -181,9 +181,10 @@ LAMBDA_TASKS_LOCAL_WORKERS = 4
|
|
|
181
181
|
Async local mode bridges the gap between eager mode and full SQS/Lambda deployment. Tasks execute in background worker processes with true parallelism and full timeout enforcement via `SIGALRM`, without requiring AWS infrastructure.
|
|
182
182
|
|
|
183
183
|
The execution mode hierarchy is:
|
|
184
|
-
1. **
|
|
185
|
-
2. **
|
|
186
|
-
3. **
|
|
184
|
+
1. **Noop** (`LAMBDA_TASKS_NOOP_EXECUTION=True`) — tasks are dropped with a warning log
|
|
185
|
+
2. **Eager** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
|
|
186
|
+
3. **Async local** (`LAMBDA_TASKS_LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
|
|
187
|
+
4. **SQS** (default) — async, Lambda workers, timeouts enforced
|
|
187
188
|
|
|
188
189
|
Key characteristics:
|
|
189
190
|
- `LAMBDA_TASKS_LOCAL_WORKERS` and `LAMBDA_TASKS_EAGER` are mutually exclusive — setting both raises `ImproperlyConfigured`
|
|
@@ -196,6 +197,35 @@ Key characteristics:
|
|
|
196
197
|
|
|
197
198
|
---
|
|
198
199
|
|
|
200
|
+
### Noop execution (testing)
|
|
201
|
+
|
|
202
|
+
| Setting | Type | Default | Description |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `LAMBDA_TASKS_NOOP_EXECUTION` | `bool` | `False` | When `True`, all tasks are silently dropped. A warning is logged with the task name, kwargs, and queue. |
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# settings/test.py
|
|
208
|
+
LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Noop mode is useful for test suites where task execution is irrelevant and you want to avoid mocking SQS or running tasks at all. When a task is dispatched via `execute_on_commit()`, it logs a warning and returns immediately — no SQS message is sent, no task is executed, no `TaskRecord` is created.
|
|
212
|
+
|
|
213
|
+
`LAMBDA_TASKS_NOOP_EXECUTION` is mutually exclusive with both `LAMBDA_TASKS_EAGER` and `LAMBDA_TASKS_LOCAL_WORKERS` — setting it alongside either raises `ImproperlyConfigured`.
|
|
214
|
+
|
|
215
|
+
### Deployment checks
|
|
216
|
+
|
|
217
|
+
A Django deployment check (runs with `manage.py check --deploy`) warns if any non-production execution mode is enabled:
|
|
218
|
+
|
|
219
|
+
| Check ID | Condition | Message |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `lambda_tasks.W001` | `LAMBDA_TASKS_NOOP_EXECUTION = True` | Tasks will be silently dropped |
|
|
222
|
+
| `lambda_tasks.W002` | `LAMBDA_TASKS_EAGER = True` | Tasks will run synchronously in-process |
|
|
223
|
+
| `lambda_tasks.W003` | `LAMBDA_TASKS_LOCAL_WORKERS > 0` | Tasks will run in a local process pool |
|
|
224
|
+
|
|
225
|
+
Add `manage.py check --deploy` to your CI pipeline to catch these misconfigurations before they reach production.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
199
229
|
## Decorator options
|
|
200
230
|
|
|
201
231
|
```python
|
|
@@ -505,6 +535,8 @@ lambda_tasks.handler.handler
|
|
|
505
535
|
|
|
506
536
|
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: temporary log handler attached → `resolve_environment()` → `resolve_secrets_into_env()` → log handler removed → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
507
537
|
|
|
538
|
+
Before processing each SQS record, the handler calls `django.db.close_old_connections()` to close any database connections that have become unusable between invocations. Lambda containers are reused across invocations (warm starts), but database connections may be closed server-side due to idle timeouts, RDS failovers, or network interruptions. Without this, the first database query in a task would fail with `OperationalError: the connection is closed`.
|
|
539
|
+
|
|
508
540
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
509
541
|
|
|
510
542
|
| Environment Variable | Required | Description |
|
|
@@ -15,6 +15,7 @@ class LambdaTasksConfig(AppConfig):
|
|
|
15
15
|
on Ctrl+C, before Django's autoreloader parent SIGKILLs this child. See
|
|
16
16
|
``local_executor._install_shutdown_handlers`` for the full rationale.
|
|
17
17
|
"""
|
|
18
|
+
import lambda_tasks.checks # noqa: F401 — register deployment checks
|
|
18
19
|
from lambda_tasks.local_executor import _install_shutdown_handlers
|
|
19
20
|
from lambda_tasks.settings import LambdaTasksSettings
|
|
20
21
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Django deployment checks for lambda_tasks."""
|
|
2
|
+
|
|
3
|
+
from django.conf import settings as django_settings
|
|
4
|
+
from django.core.checks import Warning, register
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register("lambda_tasks", deploy=True)
|
|
8
|
+
def check_noop_not_in_production(
|
|
9
|
+
*, app_configs: object, **kwargs: object
|
|
10
|
+
) -> list[Warning]:
|
|
11
|
+
"""Warn if any non-production execution mode is enabled."""
|
|
12
|
+
warnings: list[Warning] = []
|
|
13
|
+
|
|
14
|
+
if bool(getattr(django_settings, "LAMBDA_TASKS_NOOP_EXECUTION", False)):
|
|
15
|
+
warnings.append(
|
|
16
|
+
Warning(
|
|
17
|
+
"LAMBDA_TASKS_NOOP_EXECUTION is enabled. "
|
|
18
|
+
"Tasks will be silently dropped.",
|
|
19
|
+
hint="Set LAMBDA_TASKS_NOOP_EXECUTION = False for production.",
|
|
20
|
+
id="lambda_tasks.W001",
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if bool(getattr(django_settings, "LAMBDA_TASKS_EAGER", False)):
|
|
25
|
+
warnings.append(
|
|
26
|
+
Warning(
|
|
27
|
+
"LAMBDA_TASKS_EAGER is enabled. "
|
|
28
|
+
"Tasks will run synchronously in-process.",
|
|
29
|
+
hint="Set LAMBDA_TASKS_EAGER = False for production.",
|
|
30
|
+
id="lambda_tasks.W002",
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
local_workers = int(getattr(django_settings, "LAMBDA_TASKS_LOCAL_WORKERS", 0))
|
|
35
|
+
if local_workers > 0:
|
|
36
|
+
warnings.append(
|
|
37
|
+
Warning(
|
|
38
|
+
"LAMBDA_TASKS_LOCAL_WORKERS is set to a positive value. "
|
|
39
|
+
"Tasks will run in a local process pool.",
|
|
40
|
+
hint="Set LAMBDA_TASKS_LOCAL_WORKERS = 0 for production.",
|
|
41
|
+
id="lambda_tasks.W003",
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return warnings
|
|
@@ -21,6 +21,7 @@ import urllib.request
|
|
|
21
21
|
import uuid
|
|
22
22
|
|
|
23
23
|
import django
|
|
24
|
+
from django import db
|
|
24
25
|
from django.apps import apps
|
|
25
26
|
|
|
26
27
|
from lambda_tasks.environment_loader import resolve_environment
|
|
@@ -132,6 +133,7 @@ def handler(event: dict, context: object) -> dict:
|
|
|
132
133
|
|
|
133
134
|
for record in event["Records"]:
|
|
134
135
|
try:
|
|
136
|
+
db.close_old_connections()
|
|
135
137
|
SQSLambdaTaskMessage.model_validate_json(
|
|
136
138
|
record["body"]
|
|
137
139
|
).execute_immediately(message_id=record["messageId"])
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import datetime
|
|
5
|
+
import logging
|
|
5
6
|
import random
|
|
6
7
|
import traceback
|
|
7
8
|
import uuid
|
|
@@ -26,6 +27,8 @@ from lambda_tasks.settings import (
|
|
|
26
27
|
)
|
|
27
28
|
from lambda_tasks.timeouts import TimeoutContext
|
|
28
29
|
|
|
30
|
+
logger = logging.getLogger("lambda_tasks")
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
class MaxRetriesExceededError(Exception):
|
|
31
34
|
def __init__(self, *, task_name: str, n_retries: int) -> None:
|
|
@@ -260,6 +263,13 @@ class SQSLambdaTask(BaseModel):
|
|
|
260
263
|
"""
|
|
261
264
|
conf = LambdaTasksSettings()
|
|
262
265
|
|
|
266
|
+
if conf.NOOP_EXECUTION:
|
|
267
|
+
logger.warning(
|
|
268
|
+
f"Task {self.message.task_name} dropped (noop execution mode): "
|
|
269
|
+
f"kwargs={self.message.kwargs!r}, queue={self.queue!r}"
|
|
270
|
+
)
|
|
271
|
+
return
|
|
272
|
+
|
|
263
273
|
if conf.EAGER:
|
|
264
274
|
self.message.execute_immediately(message_id=str(uuid.uuid4()))
|
|
265
275
|
elif conf.LOCAL_WORKERS > 0:
|
|
@@ -108,3 +108,16 @@ class LambdaTasksSettings:
|
|
|
108
108
|
)
|
|
109
109
|
else:
|
|
110
110
|
return value
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def NOOP_EXECUTION(self) -> bool:
|
|
114
|
+
value = bool(getattr(django_settings, "LAMBDA_TASKS_NOOP_EXECUTION", False))
|
|
115
|
+
if value and self.EAGER:
|
|
116
|
+
raise ImproperlyConfigured(
|
|
117
|
+
"LAMBDA_TASKS_NOOP_EXECUTION and LAMBDA_TASKS_EAGER are mutually exclusive."
|
|
118
|
+
)
|
|
119
|
+
if value and int(getattr(django_settings, "LAMBDA_TASKS_LOCAL_WORKERS", 0)) > 0:
|
|
120
|
+
raise ImproperlyConfigured(
|
|
121
|
+
"LAMBDA_TASKS_NOOP_EXECUTION and LAMBDA_TASKS_LOCAL_WORKERS are mutually exclusive."
|
|
122
|
+
)
|
|
123
|
+
return value
|
|
@@ -26,6 +26,14 @@ from hypothesis import strategies as st
|
|
|
26
26
|
|
|
27
27
|
from lambda_tasks.handler import handler
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(autouse=True)
|
|
31
|
+
def _patch_close_old_connections():
|
|
32
|
+
"""Prevent close_old_connections from triggering the DB access guard in unit tests."""
|
|
33
|
+
with patch("lambda_tasks.handler.db.close_old_connections"):
|
|
34
|
+
yield
|
|
35
|
+
|
|
36
|
+
|
|
29
37
|
# ---------------------------------------------------------------------------
|
|
30
38
|
# Signature test
|
|
31
39
|
# ---------------------------------------------------------------------------
|
|
@@ -457,3 +465,71 @@ class TestColdStartLogging:
|
|
|
457
465
|
|
|
458
466
|
assert lambda_tasks_logger.handlers == []
|
|
459
467
|
assert lambda_tasks_logger.level == logging.NOTSET
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
# Stale connection handling
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class TestHandlerClosesOldConnections:
|
|
476
|
+
def test_close_old_connections_called_before_each_record(self):
|
|
477
|
+
"""close_old_connections() is called before processing each SQS record."""
|
|
478
|
+
call_order = []
|
|
479
|
+
|
|
480
|
+
def _track_close():
|
|
481
|
+
call_order.append("close")
|
|
482
|
+
|
|
483
|
+
def _make_message(body: str):
|
|
484
|
+
msg = MagicMock()
|
|
485
|
+
|
|
486
|
+
def _execute(**kwargs):
|
|
487
|
+
call_order.append("execute")
|
|
488
|
+
|
|
489
|
+
msg.execute_immediately.side_effect = _execute
|
|
490
|
+
return msg
|
|
491
|
+
|
|
492
|
+
records = [
|
|
493
|
+
_make_record("msg-1", _valid_body()),
|
|
494
|
+
_make_record("msg-2", _valid_body()),
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
with (
|
|
498
|
+
patch(
|
|
499
|
+
"lambda_tasks.handler.db.close_old_connections",
|
|
500
|
+
side_effect=_track_close,
|
|
501
|
+
),
|
|
502
|
+
_patch_model_validate(_make_message),
|
|
503
|
+
):
|
|
504
|
+
handler(event={"Records": records}, context=None)
|
|
505
|
+
|
|
506
|
+
assert call_order == ["close", "execute", "close", "execute"]
|
|
507
|
+
|
|
508
|
+
def test_close_old_connections_called_even_when_record_fails(self):
|
|
509
|
+
"""close_old_connections() is called before each record, including ones that fail."""
|
|
510
|
+
close_count = 0
|
|
511
|
+
|
|
512
|
+
def _track_close():
|
|
513
|
+
nonlocal close_count
|
|
514
|
+
close_count += 1
|
|
515
|
+
|
|
516
|
+
def _make_message(body: str):
|
|
517
|
+
msg = MagicMock()
|
|
518
|
+
msg.execute_immediately.side_effect = RuntimeError("boom")
|
|
519
|
+
return msg
|
|
520
|
+
|
|
521
|
+
records = [
|
|
522
|
+
_make_record("msg-1", _valid_body()),
|
|
523
|
+
_make_record("msg-2", _valid_body()),
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
with (
|
|
527
|
+
patch(
|
|
528
|
+
"lambda_tasks.handler.db.close_old_connections",
|
|
529
|
+
side_effect=_track_close,
|
|
530
|
+
),
|
|
531
|
+
_patch_model_validate(_make_message),
|
|
532
|
+
):
|
|
533
|
+
handler(event={"Records": records}, context=None)
|
|
534
|
+
|
|
535
|
+
assert close_count == 2
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tests for LAMBDA_TASKS_NOOP_EXECUTION setting and deployment check."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
8
|
+
|
|
9
|
+
DEFAULT_QUEUE = {
|
|
10
|
+
"queue_url": "https://sqs.us-east-1.amazonaws.com/000000000000/default"
|
|
11
|
+
}
|
|
12
|
+
QUEUES = {"default": DEFAULT_QUEUE}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# NOOP_EXECUTION setting property
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_noop_execution_defaults_to_false():
|
|
21
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
22
|
+
|
|
23
|
+
conf = LambdaTasksSettings()
|
|
24
|
+
assert conf.NOOP_EXECUTION is False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_noop_execution_true(settings):
|
|
28
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
29
|
+
|
|
30
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
31
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
32
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
33
|
+
conf = LambdaTasksSettings()
|
|
34
|
+
assert conf.NOOP_EXECUTION is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_noop_execution_with_eager_raises(settings):
|
|
38
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
39
|
+
|
|
40
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
41
|
+
settings.LAMBDA_TASKS_EAGER = True
|
|
42
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
43
|
+
conf = LambdaTasksSettings()
|
|
44
|
+
with pytest.raises(ImproperlyConfigured):
|
|
45
|
+
_ = conf.NOOP_EXECUTION
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_noop_execution_with_local_workers_raises(settings):
|
|
49
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
50
|
+
|
|
51
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
52
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
53
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
|
|
54
|
+
conf = LambdaTasksSettings()
|
|
55
|
+
with pytest.raises(ImproperlyConfigured):
|
|
56
|
+
_ = conf.NOOP_EXECUTION
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# _execute() noop behaviour
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_execute_noop_does_not_call_sqs(settings):
|
|
65
|
+
from lambda_tasks.models import SQSLambdaTask, SQSLambdaTaskMessage
|
|
66
|
+
|
|
67
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
68
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
69
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
70
|
+
settings.LAMBDA_TASKS_QUEUES = QUEUES
|
|
71
|
+
|
|
72
|
+
message = SQSLambdaTaskMessage(
|
|
73
|
+
task_name="myapp.tasks.my_task",
|
|
74
|
+
kwargs={"user_id": 1},
|
|
75
|
+
n_retries=0,
|
|
76
|
+
)
|
|
77
|
+
task = SQSLambdaTask(message=message, delay=0, queue="default")
|
|
78
|
+
|
|
79
|
+
with patch("lambda_tasks.models.boto3") as mock_boto3:
|
|
80
|
+
task._execute()
|
|
81
|
+
mock_boto3.client.assert_not_called()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_execute_noop_logs_warning(settings, caplog):
|
|
85
|
+
from lambda_tasks.models import SQSLambdaTask, SQSLambdaTaskMessage
|
|
86
|
+
|
|
87
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
88
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
89
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
90
|
+
settings.LAMBDA_TASKS_QUEUES = QUEUES
|
|
91
|
+
|
|
92
|
+
message = SQSLambdaTaskMessage(
|
|
93
|
+
task_name="myapp.tasks.my_task",
|
|
94
|
+
kwargs={"user_id": 42},
|
|
95
|
+
n_retries=0,
|
|
96
|
+
)
|
|
97
|
+
task = SQSLambdaTask(message=message, delay=0, queue="default")
|
|
98
|
+
|
|
99
|
+
with caplog.at_level(logging.WARNING, logger="lambda_tasks"):
|
|
100
|
+
task._execute()
|
|
101
|
+
|
|
102
|
+
assert len(caplog.records) == 1
|
|
103
|
+
record = caplog.records[0]
|
|
104
|
+
assert record.levelname == "WARNING"
|
|
105
|
+
assert "myapp.tasks.my_task" in record.message
|
|
106
|
+
assert "noop" in record.message.lower()
|
|
107
|
+
assert "{'user_id': 42}" in record.message
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Django deployment check
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_deployment_check_passes_when_all_false(settings):
|
|
116
|
+
from lambda_tasks.checks import check_noop_not_in_production
|
|
117
|
+
|
|
118
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = False
|
|
119
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
120
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
121
|
+
errors = check_noop_not_in_production(app_configs=None)
|
|
122
|
+
assert errors == []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_deployment_check_warns_noop(settings):
|
|
126
|
+
from lambda_tasks.checks import check_noop_not_in_production
|
|
127
|
+
|
|
128
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
129
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
130
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
131
|
+
errors = check_noop_not_in_production(app_configs=None)
|
|
132
|
+
assert len(errors) == 1
|
|
133
|
+
assert errors[0].id == "lambda_tasks.W001"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_deployment_check_warns_eager(settings):
|
|
137
|
+
from lambda_tasks.checks import check_noop_not_in_production
|
|
138
|
+
|
|
139
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = False
|
|
140
|
+
settings.LAMBDA_TASKS_EAGER = True
|
|
141
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
142
|
+
errors = check_noop_not_in_production(app_configs=None)
|
|
143
|
+
assert len(errors) == 1
|
|
144
|
+
assert errors[0].id == "lambda_tasks.W002"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_deployment_check_warns_local_workers(settings):
|
|
148
|
+
from lambda_tasks.checks import check_noop_not_in_production
|
|
149
|
+
|
|
150
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = False
|
|
151
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
152
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
|
|
153
|
+
errors = check_noop_not_in_production(app_configs=None)
|
|
154
|
+
assert len(errors) == 1
|
|
155
|
+
assert errors[0].id == "lambda_tasks.W003"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_deployment_check_warns_multiple(settings):
|
|
159
|
+
from lambda_tasks.checks import check_noop_not_in_production
|
|
160
|
+
|
|
161
|
+
settings.LAMBDA_TASKS_NOOP_EXECUTION = True
|
|
162
|
+
settings.LAMBDA_TASKS_EAGER = True
|
|
163
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
|
|
164
|
+
errors = check_noop_not_in_production(app_configs=None)
|
|
165
|
+
assert len(errors) == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|