django-lambda-tasks 0.4.8__tar.gz → 0.4.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/steering/product.md +10 -1
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/steering/structure.md +2 -1
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/PKG-INFO +1 -1
- django_lambda_tasks-0.4.10/lambda_tasks/apps.py +22 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/decorators.py +1 -1
- django_lambda_tasks-0.4.10/lambda_tasks/local_executor.py +151 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/pyproject.toml +1 -1
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_deferred_enqueue.py +16 -2
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_local_executor.py +209 -0
- django_lambda_tasks-0.4.8/lambda_tasks/apps.py +0 -8
- django_lambda_tasks-0.4.8/lambda_tasks/local_executor.py +0 -78
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.gitignore +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/README.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/README.md +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/manage.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/settings.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/tests/test_timeouts.py +0 -0
|
@@ -246,7 +246,16 @@ Implementation lives in `lambda_tasks/local_executor.py`:
|
|
|
246
246
|
- `get_pool()` — lazily creates and returns the shared `ProcessPoolExecutor`
|
|
247
247
|
- `submit_task(*, message_json: str)` — generates a UUID4 message_id and submits to the pool
|
|
248
248
|
- `_execute_in_worker(*, message_json: str, message_id: str)` — worker entry point; deserializes and calls `execute_immediately()`
|
|
249
|
-
- `_pool_initializer()` — calls `django.setup()` once per worker
|
|
249
|
+
- `_pool_initializer()` — calls `django.setup()` once per worker and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C
|
|
250
|
+
- `_install_shutdown_handlers()` — installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool, then chain to the previous handler
|
|
251
|
+
|
|
252
|
+
### Shutdown and the Ctrl+C semaphore-leak race
|
|
253
|
+
|
|
254
|
+
`runserver` runs the development server in an autoreloader **child** process spawned via `subprocess.run()`. On Ctrl+C the terminal delivers `SIGINT` to the whole process group; the autoreloader **parent** unwinds out of `subprocess.run` and immediately calls `process.kill()` (`SIGKILL`) on the child. The pool's POSIX semaphores must be unlinked before that `SIGKILL` lands, otherwise multiprocessing's `resource_tracker` prints `There appear to be N leaked semaphore objects to clean up at shutdown`.
|
|
255
|
+
|
|
256
|
+
Relying on `atexit` alone loses this race in applications with a heavy shutdown sequence (many `atexit` handlers, open DB connections), because `atexit` runs only after the full interpreter unwind — `SIGKILL` arrives first. To win the race, `LambdaTasksConfig.ready()` calls `_install_shutdown_handlers()` when `LOCAL_WORKERS > 0`. The handler shuts the pool down as its **first** action, then chains to the previously installed handler so normal shutdown behaviour (`KeyboardInterrupt`, autoreloader exit) is preserved. `atexit` registration remains as a fallback for non-signal exits. Handler installation is idempotent and only effective on the main thread (`signal.signal` raises off the main thread, in which case it is a no-op).
|
|
257
|
+
|
|
258
|
+
`_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`.
|
|
250
259
|
|
|
251
260
|
## Logging
|
|
252
261
|
|
|
@@ -41,13 +41,14 @@ django-lambda-tasks/
|
|
|
41
41
|
|
|
42
42
|
- `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
|
|
43
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`)
|
|
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
|
|
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
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`
|
|
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
49
|
- `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
|
|
50
50
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
51
|
+
- `apps.py` — Django `AppConfig`; `ready()` installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
51
52
|
- `tasks.py` — built-in maintenance tasks; `cleanup_task_records` deletes old `TaskRecord` rows
|
|
52
53
|
|
|
53
54
|
## Conventions
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Django AppConfig for the lambda_tasks library."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LambdaTasksConfig(AppConfig):
|
|
7
|
+
name = "lambda_tasks"
|
|
8
|
+
verbose_name = "Lambda Tasks"
|
|
9
|
+
|
|
10
|
+
def ready(self) -> None:
|
|
11
|
+
"""Install pool shutdown signal handlers in async-local mode.
|
|
12
|
+
|
|
13
|
+
Only relevant when LOCAL_WORKERS > 0 (development async execution). In
|
|
14
|
+
that mode the process pool's POSIX semaphores must be released promptly
|
|
15
|
+
on Ctrl+C, before Django's autoreloader parent SIGKILLs this child. See
|
|
16
|
+
``local_executor._install_shutdown_handlers`` for the full rationale.
|
|
17
|
+
"""
|
|
18
|
+
from lambda_tasks.local_executor import _install_shutdown_handlers
|
|
19
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
20
|
+
|
|
21
|
+
if LambdaTasksSettings().LOCAL_WORKERS > 0:
|
|
22
|
+
_install_shutdown_handlers()
|
|
@@ -189,7 +189,7 @@ class LambdaTaskWrapper:
|
|
|
189
189
|
pydantic.ValidationError: if kwargs fail the task's declared type annotations.
|
|
190
190
|
"""
|
|
191
191
|
task = self._build_task(kwargs=kwargs)
|
|
192
|
-
return task.model_dump()
|
|
192
|
+
return task.model_dump(mode="json")
|
|
193
193
|
|
|
194
194
|
def execute_on_commit(self, **kwargs: Any) -> None:
|
|
195
195
|
"""Enqueue the task to run after the current transaction commits."""
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Process pool executor for async local task execution."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import logging
|
|
5
|
+
import signal
|
|
6
|
+
import threading
|
|
7
|
+
import uuid
|
|
8
|
+
from concurrent.futures import Future, ProcessPoolExecutor
|
|
9
|
+
|
|
10
|
+
from lambda_tasks.settings import LambdaTasksSettings
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_pool: ProcessPoolExecutor | None = None
|
|
15
|
+
_handlers_installed: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pool_initializer() -> None:
|
|
19
|
+
"""Run once per worker process.
|
|
20
|
+
|
|
21
|
+
Ignores SIGINT so that Ctrl+C is handled exclusively by the parent
|
|
22
|
+
process, which releases the pool via its SIGINT/SIGTERM handler (and
|
|
23
|
+
atexit as a fallback). This prevents workers from being killed
|
|
24
|
+
mid-operation and leaking semaphores.
|
|
25
|
+
|
|
26
|
+
Then sets up Django for task execution.
|
|
27
|
+
"""
|
|
28
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
29
|
+
|
|
30
|
+
import django
|
|
31
|
+
|
|
32
|
+
django.setup()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _shutdown_pool() -> None:
|
|
36
|
+
"""Shut down the pool, releasing its POSIX semaphores promptly.
|
|
37
|
+
|
|
38
|
+
On shutdown we must unlink the pool's semaphores quickly, because under
|
|
39
|
+
``runserver`` the autoreloader parent SIGKILLs this child almost immediately
|
|
40
|
+
after Ctrl+C (see ``_install_shutdown_handlers``). ``pool.shutdown(wait=True)``
|
|
41
|
+
blocks on joining the worker processes — if a worker is slow to exit (e.g.
|
|
42
|
+
it ran a heavy ``django.setup()``), the semaphores are not unlinked until
|
|
43
|
+
the worker dies, and the SIGKILL wins the race, leaking the semaphores.
|
|
44
|
+
|
|
45
|
+
To avoid that, terminate the worker processes first (a near-instant
|
|
46
|
+
SIGTERM/SIGKILL to children we own), then shut the pool down without
|
|
47
|
+
waiting. With the workers already gone, ``concurrent.futures`` releases the
|
|
48
|
+
queue semaphores immediately.
|
|
49
|
+
"""
|
|
50
|
+
global _pool
|
|
51
|
+
if _pool is not None:
|
|
52
|
+
pool = _pool
|
|
53
|
+
_pool = None
|
|
54
|
+
processes = getattr(pool, "_processes", None)
|
|
55
|
+
if processes:
|
|
56
|
+
for process in list(processes.values()):
|
|
57
|
+
process.terminate()
|
|
58
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _install_shutdown_handlers() -> None:
|
|
62
|
+
"""Install SIGINT/SIGTERM handlers that release the pool promptly.
|
|
63
|
+
|
|
64
|
+
Under Django's ``runserver`` autoreloader the development server runs in a
|
|
65
|
+
child process spawned by ``subprocess.run()``. On Ctrl+C the terminal
|
|
66
|
+
delivers SIGINT to the whole process group; the autoreloader parent unwinds
|
|
67
|
+
out of ``subprocess.run`` and immediately calls ``process.kill()``
|
|
68
|
+
(SIGKILL) on this child. That is a race: this process must unlink the
|
|
69
|
+
pool's POSIX semaphores before the SIGKILL lands, otherwise multiprocessing's
|
|
70
|
+
``resource_tracker`` reports them as leaked at shutdown.
|
|
71
|
+
|
|
72
|
+
Relying on ``atexit`` loses that race in applications with a heavy shutdown
|
|
73
|
+
sequence, because ``atexit`` runs only after the full interpreter unwind.
|
|
74
|
+
Instead we shut the pool down as the very first action of the signal
|
|
75
|
+
handler, then chain to the previously installed handler so normal shutdown
|
|
76
|
+
behaviour (KeyboardInterrupt, autoreloader exit) is preserved.
|
|
77
|
+
|
|
78
|
+
Idempotent and only effective on the main thread — ``signal.signal`` raises
|
|
79
|
+
``ValueError`` off the main thread, in which case this is a no-op.
|
|
80
|
+
"""
|
|
81
|
+
global _handlers_installed
|
|
82
|
+
if _handlers_installed:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if threading.current_thread() is not threading.main_thread():
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def make_handler(previous): # type: ignore[no-untyped-def]
|
|
89
|
+
def handler(signum, frame): # type: ignore[no-untyped-def]
|
|
90
|
+
# Release the pool's semaphores first, before the autoreloader
|
|
91
|
+
# parent can SIGKILL us.
|
|
92
|
+
_shutdown_pool()
|
|
93
|
+
if callable(previous):
|
|
94
|
+
previous(signum, frame)
|
|
95
|
+
elif previous == signal.SIG_DFL:
|
|
96
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
97
|
+
signal.raise_signal(signum)
|
|
98
|
+
# previous == SIG_IGN: nothing to chain to.
|
|
99
|
+
|
|
100
|
+
return handler
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
104
|
+
previous = signal.getsignal(signum)
|
|
105
|
+
signal.signal(signum, make_handler(previous))
|
|
106
|
+
except ValueError:
|
|
107
|
+
# Not on the main thread; cannot install signal handlers here.
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
_handlers_installed = True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_pool() -> ProcessPoolExecutor:
|
|
114
|
+
"""Return the shared ProcessPoolExecutor, creating it on first call."""
|
|
115
|
+
global _pool
|
|
116
|
+
if _pool is None:
|
|
117
|
+
conf = LambdaTasksSettings()
|
|
118
|
+
_pool = ProcessPoolExecutor(
|
|
119
|
+
max_workers=conf.LOCAL_WORKERS,
|
|
120
|
+
initializer=_pool_initializer,
|
|
121
|
+
)
|
|
122
|
+
atexit.register(_shutdown_pool)
|
|
123
|
+
return _pool
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _execute_in_worker(*, message_json: str, message_id: str) -> None:
|
|
127
|
+
"""Worker entry point. Deserializes and executes the task.
|
|
128
|
+
|
|
129
|
+
Runs in a child process. Django is already set up via the pool initializer.
|
|
130
|
+
"""
|
|
131
|
+
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
132
|
+
|
|
133
|
+
message = SQSLambdaTaskMessage.model_validate_json(message_json)
|
|
134
|
+
message.execute_immediately(message_id=message_id)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _log_worker_exception(future: Future) -> None: # type: ignore[type-arg]
|
|
138
|
+
"""Callback attached to each worker future. Logs unhandled exceptions."""
|
|
139
|
+
exception = future.exception()
|
|
140
|
+
if exception is not None:
|
|
141
|
+
logger.error("Worker process raised an exception", exc_info=exception)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def submit_task(*, message_json: str) -> None:
|
|
145
|
+
"""Submit a task to the process pool. Fire-and-forget."""
|
|
146
|
+
pool = get_pool()
|
|
147
|
+
message_id = str(uuid.uuid4())
|
|
148
|
+
future = pool.submit(
|
|
149
|
+
_execute_in_worker, message_json=message_json, message_id=message_id
|
|
150
|
+
)
|
|
151
|
+
future.add_done_callback(_log_worker_exception)
|
|
@@ -4,6 +4,7 @@ Unit tests for LambdaTaskWrapper.to_json (TDD red state — to_json does not exi
|
|
|
4
4
|
Task 4.1: unit tests only (no hypothesis).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import uuid
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -19,8 +20,13 @@ def _task(*, x: int) -> None:
|
|
|
19
20
|
pass
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _uuid_task(*, user_id: uuid.UUID) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
22
27
|
_wrapper = LambdaTaskWrapper(_task, delay=0, queue="default")
|
|
23
28
|
_wrapper_with_defaults = LambdaTaskWrapper(_task, delay=5, queue="high_memory")
|
|
29
|
+
_uuid_wrapper = LambdaTaskWrapper(_uuid_task, delay=0, queue="default")
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
# ---------------------------------------------------------------------------
|
|
@@ -69,12 +75,20 @@ def test_to_json_raises_validation_error_for_missing_required_kwargs():
|
|
|
69
75
|
_wrapper.serialize()
|
|
70
76
|
|
|
71
77
|
|
|
78
|
+
def test_serialize_produces_json_serializable_output_with_uuid_kwargs():
|
|
79
|
+
"""serialize() with UUID kwargs returns a dict that is JSON-serializable."""
|
|
80
|
+
test_id = uuid.uuid4()
|
|
81
|
+
result = _uuid_wrapper.serialize(user_id=test_id)
|
|
82
|
+
# Must be JSON-serializable — this raises TypeError if UUID objects remain
|
|
83
|
+
json.dumps(result)
|
|
84
|
+
# The UUID should be serialized as a string
|
|
85
|
+
assert result["message"]["kwargs"]["user_id"] == str(test_id)
|
|
86
|
+
|
|
87
|
+
|
|
72
88
|
# ---------------------------------------------------------------------------
|
|
73
89
|
# Property-based tests (P1, P2)
|
|
74
90
|
# ---------------------------------------------------------------------------
|
|
75
91
|
|
|
76
|
-
import json
|
|
77
|
-
|
|
78
92
|
from hypothesis import HealthCheck, given, settings
|
|
79
93
|
from hypothesis import strategies as st
|
|
80
94
|
|
|
@@ -679,3 +679,212 @@ class TestWorkerExceptionIsolation:
|
|
|
679
679
|
|
|
680
680
|
# Pool.submit was called twice — pool did not crash
|
|
681
681
|
assert mock_pool.submit.call_count == 2
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ---------------------------------------------------------------------------
|
|
685
|
+
# Shutdown signal handlers (Ctrl+C / SIGINT race with the autoreloader parent)
|
|
686
|
+
# ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
import signal as signal_module
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@pytest.fixture()
|
|
692
|
+
def _reset_signal_handlers():
|
|
693
|
+
"""Save/restore SIGINT and SIGTERM handlers and the installed sentinel."""
|
|
694
|
+
import lambda_tasks.local_executor
|
|
695
|
+
|
|
696
|
+
original_sigint = signal_module.getsignal(signal_module.SIGINT)
|
|
697
|
+
original_sigterm = signal_module.getsignal(signal_module.SIGTERM)
|
|
698
|
+
original_flag = lambda_tasks.local_executor._handlers_installed
|
|
699
|
+
lambda_tasks.local_executor._handlers_installed = False
|
|
700
|
+
try:
|
|
701
|
+
yield
|
|
702
|
+
finally:
|
|
703
|
+
signal_module.signal(signal_module.SIGINT, original_sigint)
|
|
704
|
+
signal_module.signal(signal_module.SIGTERM, original_sigterm)
|
|
705
|
+
lambda_tasks.local_executor._handlers_installed = original_flag
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class TestShutdownSignalHandlers:
|
|
709
|
+
"""Tests for the SIGINT/SIGTERM handlers that release the pool promptly.
|
|
710
|
+
|
|
711
|
+
Under Django's autoreloader the server runs in a child spawned by
|
|
712
|
+
subprocess.run(); on Ctrl+C the parent SIGKILLs the child. The child must
|
|
713
|
+
release the pool's semaphores before that SIGKILL lands, so cleanup happens
|
|
714
|
+
at the front of signal handling rather than via atexit.
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
@pytest.mark.usefixtures("_reset_signal_handlers")
|
|
718
|
+
def test_installs_sigint_handler(self):
|
|
719
|
+
"""_install_shutdown_handlers() replaces the SIGINT handler with our own."""
|
|
720
|
+
from lambda_tasks.local_executor import _install_shutdown_handlers
|
|
721
|
+
|
|
722
|
+
_install_shutdown_handlers()
|
|
723
|
+
|
|
724
|
+
handler = signal_module.getsignal(signal_module.SIGINT)
|
|
725
|
+
assert callable(handler)
|
|
726
|
+
assert handler != signal_module.default_int_handler
|
|
727
|
+
|
|
728
|
+
@pytest.mark.usefixtures("_reset_signal_handlers")
|
|
729
|
+
def test_handler_shuts_pool_down_then_chains_to_previous(self):
|
|
730
|
+
"""The installed handler calls _shutdown_pool() then the previous handler."""
|
|
731
|
+
import lambda_tasks.local_executor as local_executor
|
|
732
|
+
|
|
733
|
+
call_order = []
|
|
734
|
+
|
|
735
|
+
def previous_handler(signum, frame):
|
|
736
|
+
call_order.append("previous")
|
|
737
|
+
|
|
738
|
+
signal_module.signal(signal_module.SIGINT, previous_handler)
|
|
739
|
+
|
|
740
|
+
with patch.object(
|
|
741
|
+
local_executor,
|
|
742
|
+
"_shutdown_pool",
|
|
743
|
+
side_effect=lambda: call_order.append("shutdown"),
|
|
744
|
+
) as mock_shutdown:
|
|
745
|
+
local_executor._install_shutdown_handlers()
|
|
746
|
+
installed = signal_module.getsignal(signal_module.SIGINT)
|
|
747
|
+
installed(signal_module.SIGINT, None)
|
|
748
|
+
|
|
749
|
+
mock_shutdown.assert_called_once()
|
|
750
|
+
assert call_order == ["shutdown", "previous"]
|
|
751
|
+
|
|
752
|
+
@pytest.mark.usefixtures("_reset_signal_handlers")
|
|
753
|
+
def test_idempotent_does_not_double_wrap(self):
|
|
754
|
+
"""Calling twice keeps a single wrapper (no nested chaining of our own handler)."""
|
|
755
|
+
from lambda_tasks.local_executor import _install_shutdown_handlers
|
|
756
|
+
|
|
757
|
+
_install_shutdown_handlers()
|
|
758
|
+
first = signal_module.getsignal(signal_module.SIGINT)
|
|
759
|
+
_install_shutdown_handlers()
|
|
760
|
+
second = signal_module.getsignal(signal_module.SIGINT)
|
|
761
|
+
|
|
762
|
+
assert first is second
|
|
763
|
+
|
|
764
|
+
@pytest.mark.usefixtures("_reset_signal_handlers")
|
|
765
|
+
def test_noop_outside_main_thread(self):
|
|
766
|
+
"""Installation is a no-op when not on the main thread (signal.signal would raise)."""
|
|
767
|
+
import threading
|
|
768
|
+
|
|
769
|
+
import lambda_tasks.local_executor as local_executor
|
|
770
|
+
|
|
771
|
+
results = {}
|
|
772
|
+
|
|
773
|
+
def worker():
|
|
774
|
+
local_executor._install_shutdown_handlers()
|
|
775
|
+
results["installed"] = local_executor._handlers_installed
|
|
776
|
+
|
|
777
|
+
thread = threading.Thread(target=worker)
|
|
778
|
+
thread.start()
|
|
779
|
+
thread.join()
|
|
780
|
+
|
|
781
|
+
assert results["installed"] is False
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class TestAppConfigReady:
|
|
785
|
+
"""AppConfig.ready() installs the shutdown handlers only in async-local mode."""
|
|
786
|
+
|
|
787
|
+
def test_ready_installs_handlers_when_local_workers_positive(self, settings):
|
|
788
|
+
from lambda_tasks.apps import LambdaTasksConfig
|
|
789
|
+
|
|
790
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 2
|
|
791
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
792
|
+
|
|
793
|
+
config = LambdaTasksConfig.create("lambda_tasks")
|
|
794
|
+
with patch(
|
|
795
|
+
"lambda_tasks.local_executor._install_shutdown_handlers"
|
|
796
|
+
) as mock_install:
|
|
797
|
+
config.ready()
|
|
798
|
+
|
|
799
|
+
mock_install.assert_called_once()
|
|
800
|
+
|
|
801
|
+
def test_ready_does_not_install_handlers_when_local_workers_zero(self, settings):
|
|
802
|
+
from lambda_tasks.apps import LambdaTasksConfig
|
|
803
|
+
|
|
804
|
+
settings.LAMBDA_TASKS_LOCAL_WORKERS = 0
|
|
805
|
+
settings.LAMBDA_TASKS_EAGER = False
|
|
806
|
+
|
|
807
|
+
config = LambdaTasksConfig.create("lambda_tasks")
|
|
808
|
+
with patch(
|
|
809
|
+
"lambda_tasks.local_executor._install_shutdown_handlers"
|
|
810
|
+
) as mock_install:
|
|
811
|
+
config.ready()
|
|
812
|
+
|
|
813
|
+
mock_install.assert_not_called()
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
# ---------------------------------------------------------------------------
|
|
817
|
+
# _shutdown_pool() terminate-first behaviour
|
|
818
|
+
# ---------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
class TestShutdownPool:
|
|
822
|
+
"""_shutdown_pool() must release semaphores promptly to win the SIGKILL race.
|
|
823
|
+
|
|
824
|
+
pool.shutdown(wait=True) blocks on joining worker processes; if a worker is
|
|
825
|
+
slow to exit, the autoreloader parent SIGKILLs this process before the
|
|
826
|
+
semaphores are unlinked. So _shutdown_pool() terminates the workers first,
|
|
827
|
+
then shuts down without waiting.
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
def test_shutdown_terminates_workers_before_shutdown(self):
|
|
831
|
+
"""_shutdown_pool() calls terminate() on each worker, then shutdown(wait=False)."""
|
|
832
|
+
import lambda_tasks.local_executor as local_executor
|
|
833
|
+
|
|
834
|
+
call_order = []
|
|
835
|
+
|
|
836
|
+
process_a = MagicMock()
|
|
837
|
+
process_a.terminate.side_effect = lambda: call_order.append("terminate_a")
|
|
838
|
+
process_b = MagicMock()
|
|
839
|
+
process_b.terminate.side_effect = lambda: call_order.append("terminate_b")
|
|
840
|
+
|
|
841
|
+
mock_pool = MagicMock()
|
|
842
|
+
mock_pool._processes = {"a": process_a, "b": process_b}
|
|
843
|
+
mock_pool.shutdown.side_effect = lambda **kwargs: call_order.append(
|
|
844
|
+
f"shutdown:{kwargs}"
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
original_pool = local_executor._pool
|
|
848
|
+
local_executor._pool = mock_pool
|
|
849
|
+
try:
|
|
850
|
+
local_executor._shutdown_pool()
|
|
851
|
+
finally:
|
|
852
|
+
local_executor._pool = original_pool
|
|
853
|
+
|
|
854
|
+
process_a.terminate.assert_called_once()
|
|
855
|
+
process_b.terminate.assert_called_once()
|
|
856
|
+
# Both terminates happen before the shutdown call.
|
|
857
|
+
assert call_order.index("terminate_a") < call_order.index(
|
|
858
|
+
"shutdown:{'wait': False, 'cancel_futures': True}"
|
|
859
|
+
)
|
|
860
|
+
assert call_order.index("terminate_b") < call_order.index(
|
|
861
|
+
"shutdown:{'wait': False, 'cancel_futures': True}"
|
|
862
|
+
)
|
|
863
|
+
mock_pool.shutdown.assert_called_once_with(wait=False, cancel_futures=True)
|
|
864
|
+
|
|
865
|
+
def test_shutdown_clears_module_pool(self):
|
|
866
|
+
"""_shutdown_pool() resets the module-level _pool to None."""
|
|
867
|
+
import lambda_tasks.local_executor as local_executor
|
|
868
|
+
|
|
869
|
+
mock_pool = MagicMock()
|
|
870
|
+
mock_pool._processes = {}
|
|
871
|
+
|
|
872
|
+
original_pool = local_executor._pool
|
|
873
|
+
local_executor._pool = mock_pool
|
|
874
|
+
try:
|
|
875
|
+
local_executor._shutdown_pool()
|
|
876
|
+
assert local_executor._pool is None
|
|
877
|
+
finally:
|
|
878
|
+
local_executor._pool = original_pool
|
|
879
|
+
|
|
880
|
+
def test_shutdown_is_noop_when_no_pool(self):
|
|
881
|
+
"""_shutdown_pool() does nothing when no pool exists."""
|
|
882
|
+
import lambda_tasks.local_executor as local_executor
|
|
883
|
+
|
|
884
|
+
original_pool = local_executor._pool
|
|
885
|
+
local_executor._pool = None
|
|
886
|
+
try:
|
|
887
|
+
local_executor._shutdown_pool() # must not raise
|
|
888
|
+
assert local_executor._pool is None
|
|
889
|
+
finally:
|
|
890
|
+
local_executor._pool = original_pool
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"""Process pool executor for async local task execution."""
|
|
2
|
-
|
|
3
|
-
import atexit
|
|
4
|
-
import logging
|
|
5
|
-
import signal
|
|
6
|
-
import uuid
|
|
7
|
-
from concurrent.futures import Future, ProcessPoolExecutor
|
|
8
|
-
|
|
9
|
-
from lambda_tasks.settings import LambdaTasksSettings
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
_pool: ProcessPoolExecutor | None = None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _pool_initializer() -> None:
|
|
17
|
-
"""Run once per worker process.
|
|
18
|
-
|
|
19
|
-
Ignores SIGINT so that Ctrl+C is handled exclusively by the parent
|
|
20
|
-
process, which shuts down the pool cleanly via atexit. This prevents
|
|
21
|
-
workers from being killed mid-operation and leaking semaphores.
|
|
22
|
-
|
|
23
|
-
Then sets up Django for task execution.
|
|
24
|
-
"""
|
|
25
|
-
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
26
|
-
|
|
27
|
-
import django
|
|
28
|
-
|
|
29
|
-
django.setup()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _shutdown_pool() -> None:
|
|
33
|
-
"""Shut down the pool at interpreter exit to release semaphores."""
|
|
34
|
-
global _pool
|
|
35
|
-
if _pool is not None:
|
|
36
|
-
_pool.shutdown(wait=True, cancel_futures=True)
|
|
37
|
-
_pool = None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_pool() -> ProcessPoolExecutor:
|
|
41
|
-
"""Return the shared ProcessPoolExecutor, creating it on first call."""
|
|
42
|
-
global _pool
|
|
43
|
-
if _pool is None:
|
|
44
|
-
conf = LambdaTasksSettings()
|
|
45
|
-
_pool = ProcessPoolExecutor(
|
|
46
|
-
max_workers=conf.LOCAL_WORKERS,
|
|
47
|
-
initializer=_pool_initializer,
|
|
48
|
-
)
|
|
49
|
-
atexit.register(_shutdown_pool)
|
|
50
|
-
return _pool
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _execute_in_worker(*, message_json: str, message_id: str) -> None:
|
|
54
|
-
"""Worker entry point. Deserializes and executes the task.
|
|
55
|
-
|
|
56
|
-
Runs in a child process. Django is already set up via the pool initializer.
|
|
57
|
-
"""
|
|
58
|
-
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
59
|
-
|
|
60
|
-
message = SQSLambdaTaskMessage.model_validate_json(message_json)
|
|
61
|
-
message.execute_immediately(message_id=message_id)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _log_worker_exception(future: Future) -> None: # type: ignore[type-arg]
|
|
65
|
-
"""Callback attached to each worker future. Logs unhandled exceptions."""
|
|
66
|
-
exception = future.exception()
|
|
67
|
-
if exception is not None:
|
|
68
|
-
logger.error("Worker process raised an exception", exc_info=exception)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def submit_task(*, message_json: str) -> None:
|
|
72
|
-
"""Submit a task to the process pool. Fire-and-forget."""
|
|
73
|
-
pool = get_pool()
|
|
74
|
-
message_id = str(uuid.uuid4())
|
|
75
|
-
future = pool.submit(
|
|
76
|
-
_execute_in_worker, message_json=message_json, message_id=message_id
|
|
77
|
-
)
|
|
78
|
-
future.add_done_callback(_log_worker_exception)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.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.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.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.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/singleton-task/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.kiro/specs/task-retry/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/.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
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/__init__.py
RENAMED
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/example/example_project/settings.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
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.4.8 → django_lambda_tasks-0.4.10}/lambda_tasks/migrations/__init__.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
|