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.
Files changed (105) hide show
  1. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/product.md +33 -3
  2. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/structure.md +4 -2
  3. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/PKG-INFO +36 -4
  4. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/README.md +35 -3
  5. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/apps.py +1 -0
  6. django_lambda_tasks-0.7.0/lambda_tasks/checks.py +45 -0
  7. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/handler.py +2 -0
  8. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/models.py +10 -0
  9. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/settings.py +13 -0
  10. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/pyproject.toml +1 -1
  11. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_handler.py +76 -0
  12. django_lambda_tasks-0.7.0/tests/test_noop_execution.py +165 -0
  13. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.github/workflows/ci.yml +0 -0
  14. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.github/workflows/release.yml +0 -0
  15. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.gitignore +0 -0
  16. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  17. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/design.md +0 -0
  18. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
  19. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
  20. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  21. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  22. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  23. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  24. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  25. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  26. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  27. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  28. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  29. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  30. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  31. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  32. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  33. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  34. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  35. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  36. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
  37. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/design.md +0 -0
  38. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/requirements.md +0 -0
  39. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/retry-delay/tasks.md +0 -0
  40. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  41. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
  42. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  43. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  44. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  45. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  46. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  47. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  48. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
  49. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/design.md +0 -0
  50. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/requirements.md +0 -0
  51. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/singleton-task/tasks.md +0 -0
  52. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  53. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  54. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  55. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  56. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/.config.kiro +0 -0
  57. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/design.md +0 -0
  58. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/requirements.md +0 -0
  59. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/specs/task-retry/tasks.md +0 -0
  60. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.kiro/steering/tech.md +0 -0
  61. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.pre-commit-config.yaml +0 -0
  62. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/.vscode/settings.json +0 -0
  63. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/README.md +0 -0
  64. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/__init__.py +0 -0
  65. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/apps.py +0 -0
  66. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/tasks.py +0 -0
  67. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/urls.py +0 -0
  68. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_app/views.py +0 -0
  69. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/__init__.py +0 -0
  70. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/settings.py +0 -0
  71. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/urls.py +0 -0
  72. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/example_project/wsgi.py +0 -0
  73. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/example/manage.py +0 -0
  74. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/__init__.py +0 -0
  75. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/admin.py +0 -0
  76. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/decorators.py +0 -0
  77. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/environment_loader.py +0 -0
  78. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/local_executor.py +0 -0
  79. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/logging.py +0 -0
  80. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/0001_initial.py +0 -0
  81. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  82. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/migrations/__init__.py +0 -0
  83. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/secret_loader.py +0 -0
  84. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/tasks.py +0 -0
  85. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/lambda_tasks/timeouts.py +0 -0
  86. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/conftest.py +0 -0
  87. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/settings.py +0 -0
  88. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_admin.py +0 -0
  89. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_batch.py +0 -0
  90. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_batch_worker.py +0 -0
  91. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_decorator.py +0 -0
  92. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_decorators.py +0 -0
  93. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_deferred_enqueue.py +0 -0
  94. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_environment_loader.py +0 -0
  95. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_kwargs_only.py +0 -0
  96. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_local_executor.py +0 -0
  97. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_logging.py +0 -0
  98. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_memory_limit.py +0 -0
  99. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_models.py +0 -0
  100. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_secret_loader.py +0 -0
  101. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_serializer.py +0 -0
  102. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_settings.py +0 -0
  103. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_tasks.py +0 -0
  104. {django_lambda_tasks-0.6.6 → django_lambda_tasks-0.7.0}/tests/test_timeout_validation.py +0 -0
  105. {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. **Eager mode** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
242
- 2. **Async local mode** (`LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
243
- 3. **SQS mode** (default) — async, Lambda workers, timeouts enforced
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.6.6
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. **Eager** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
197
- 2. **Async local** (`LAMBDA_TASKS_LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
198
- 3. **SQS** (default) — async, Lambda workers, timeouts enforced
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. **Eager** (`LAMBDA_TASKS_EAGER=True`) — synchronous, in-process, no timeouts
185
- 2. **Async local** (`LAMBDA_TASKS_LOCAL_WORKERS > 0`) — async, separate processes, timeouts enforced
186
- 3. **SQS** (default) — async, Lambda workers, timeouts enforced
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
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.6.6"
10
+ version = "0.7.0"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -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