django-lambda-tasks 0.10.0__tar.gz → 0.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/workflows/ci.yml +32 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/product.md +27 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/structure.md +3 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/tech.md +1 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/PKG-INFO +52 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/README.md +51 -0
- django_lambda_tasks-0.11.1/example/example_app/tasks.py +31 -0
- django_lambda_tasks-0.11.1/example/example_app/urls.py +7 -0
- django_lambda_tasks-0.11.1/example/example_app/views.py +17 -0
- django_lambda_tasks-0.11.1/example/example_project/sentry.py +50 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/settings.py +7 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/models.py +138 -118
- django_lambda_tasks-0.11.1/lambda_tasks/sentry.py +186 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/pyproject.toml +2 -1
- django_lambda_tasks-0.11.1/tests/test_sentry.py +566 -0
- django_lambda_tasks-0.10.0/example/example_app/tasks.py +0 -18
- django_lambda_tasks-0.10.0/example/example_app/urls.py +0 -6
- django_lambda_tasks-0.10.0/example/example_app/views.py +0 -8
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/dependabot.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.gitignore +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/README.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/manage.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_timeouts.py +0 -0
|
@@ -53,3 +53,35 @@ jobs:
|
|
|
53
53
|
- name: Run tests
|
|
54
54
|
run: |
|
|
55
55
|
uv run pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
|
|
56
|
+
|
|
57
|
+
sentry-prerelease:
|
|
58
|
+
# Exercise the Sentry integration against the latest 3.x pre-release of
|
|
59
|
+
# sentry-sdk (stream mode). The committed dev dependency is pinned to the
|
|
60
|
+
# stable 2.x line; this job overrides it at runtime via `uv run --with`
|
|
61
|
+
# without touching pyproject.toml, so regressions in the 3.x API are caught
|
|
62
|
+
# before the SDK goes stable.
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
services:
|
|
65
|
+
postgres:
|
|
66
|
+
image: postgres:17
|
|
67
|
+
env:
|
|
68
|
+
POSTGRES_DB: lambda_tasks_test
|
|
69
|
+
POSTGRES_USER: postgres
|
|
70
|
+
POSTGRES_PASSWORD: postgres
|
|
71
|
+
ports:
|
|
72
|
+
- 5432:5432
|
|
73
|
+
options: >-
|
|
74
|
+
--health-cmd "pg_isready -U postgres"
|
|
75
|
+
--health-interval 10s
|
|
76
|
+
--health-timeout 5s
|
|
77
|
+
--health-retries 5
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
80
|
+
- name: Install uv and set the python version
|
|
81
|
+
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
82
|
+
with:
|
|
83
|
+
python-version: '3.14'
|
|
84
|
+
- name: Run tests against the latest sentry-sdk 3.x pre-release
|
|
85
|
+
run: |
|
|
86
|
+
uv run --prerelease=allow --with "sentry-sdk>=3.0.0b1" \
|
|
87
|
+
pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
|
|
@@ -127,7 +127,7 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
127
127
|
|
|
128
128
|
## Execution
|
|
129
129
|
|
|
130
|
-
`SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py
|
|
130
|
+
`SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py` sets the `task_logger` `message_id` and enters `task_scope(...)` (Sentry transaction name + tags), then delegates the run to the private `_run(*, message_id)` method which:
|
|
131
131
|
1. Checks for an existing `TaskRecord` with the same `pk` (`message_id`) via `get_or_create`
|
|
132
132
|
2. If a record already exists (any status), logs and returns immediately — duplicate deliveries are silently skipped
|
|
133
133
|
3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s), validated against queue max (900 for SQS, 3600 for Batch)
|
|
@@ -313,6 +313,32 @@ def my_task(*, user_id: int) -> None:
|
|
|
313
313
|
|
|
314
314
|
`task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. The executor sets the `message_id` before each task runs and clears it in a `finally` block. Using your own `logging.getLogger(__name__)` is fine — those records just won't carry the prefix.
|
|
315
315
|
|
|
316
|
+
## Sentry
|
|
317
|
+
|
|
318
|
+
The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup (typically `DjangoIntegration` + `AwsLambdaIntegration`). The problem those integrations leave is attribution: on Lambda, `AwsLambdaIntegration` names every event's transaction after the Lambda function, so all tasks look alike; on AWS Batch there is no request and no Lambda integration, so the transaction/URL are `"--"`. Either way you cannot tell which task ran or raised.
|
|
319
|
+
|
|
320
|
+
`lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` fixes this. `execute_immediately` wraps the task body in it, which — when a Sentry client is active — opens an isolated `sentry_sdk.new_scope()` and traces the task. Trace shape depends on whether a trace is already active:
|
|
321
|
+
|
|
322
|
+
- **Lambda** — `AwsLambdaIntegration` already started an invocation transaction, so the task is traced as a **child span** of it (not a second transaction). This is deliberate: a nested *transaction* per task inherited (rather than independently re-rolled) the invocation's sampling decision, doubling transaction volume and defeating `traces_sample_rate`. A child span is sent only if its parent is sampled, so sampling is respected. `_start_task_root` picks child-vs-root by checking `sentry_sdk.get_current_scope().transaction` (2.x only; on 3.0 `start_span` is automatically root-or-child).
|
|
323
|
+
- **Batch** (nothing active) — the task starts its **own root transaction** so it is still traced.
|
|
324
|
+
|
|
325
|
+
It records, both on the span and as scope tags:
|
|
326
|
+
|
|
327
|
+
- `lambda_task` = fully-qualified task name (e.g. `myapp.tasks.my_task`) — also the task span's name
|
|
328
|
+
- `lambda_task.message_id` = the `TaskRecord` primary key
|
|
329
|
+
- `lambda_task.n_retries` = the retry counter
|
|
330
|
+
- `lambda_task.runtime` = the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (Batch job queue name); omitted when neither is present
|
|
331
|
+
|
|
332
|
+
`task_scope` deliberately does **not** call `scope.set_transaction_name(task_name)`: on Lambda that would rename the shared invocation transaction (fighting across sibling records in a batch) and, on SDK 3.0 stream mode, corrupt the root-segment naming. Error attribution relies on the `lambda_task` **tag** instead, which is reliable on both SDKs and independent of sampling.
|
|
333
|
+
|
|
334
|
+
**Reporting task failures:** the executor catches task exceptions (recording `FAILED` and not re-raising), so `lambda_tasks.sentry.capture_task_exception()` is called on the two **terminal** failure branches in `models.py` (plain failure and max-retries-exceeded) to report the error via `sentry_sdk.capture_exception()`. This is essential on 3.0, where a merely-caught exception produces no error event. The **retry** branch deliberately does not capture — transient, to-be-re-enqueued failures would flood Sentry; only terminal failures are reported.
|
|
335
|
+
|
|
336
|
+
**Error-level log forwarding:** on sentry-sdk 2.x, `LoggingIntegration` defaulted to `event_level=ERROR`, so `task_logger.error(...)` became Sentry events automatically. On 3.0 the default is `None`, so hosts that want error logs reported must configure `LoggingIntegration(event_level=logging.ERROR)` explicitly (the example app does this). This is a host-init concern, not something the library sets.
|
|
337
|
+
|
|
338
|
+
**SDK version compatibility:** `task_scope` is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. On 2.x it uses `start_transaction` + `set_tag` for a root and `start_span` for a child; on 3.0 (stream mode) `start_span` is both root and child and metadata is attached via `set_attribute`. The correct API is detected at runtime via `hasattr`, so no code change is needed when the host upgrades the SDK.
|
|
339
|
+
|
|
340
|
+
`task_scope` and `capture_task_exception` are no-ops when `sentry_sdk` is not installed or no client is initialised, so Sentry is not a dependency of the library. `sentry-sdk` is a dev dependency only (pinned `>=2,<3`), for the test suite. No custom Sentry integration is needed — per-task tracing at the one execution entry point covers both Lambda and Batch.
|
|
341
|
+
|
|
316
342
|
## Built-in Tasks
|
|
317
343
|
|
|
318
344
|
`lambda_tasks.tasks` provides maintenance tasks that ship with the library:
|
|
@@ -16,6 +16,7 @@ django-lambda-tasks/
|
|
|
16
16
|
│ ├── decorators.py # LambdaTaskWrapper, @lambda_task
|
|
17
17
|
│ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
|
|
18
18
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
19
|
+
│ ├── sentry.py # task_scope + capture_task_exception — per-task trace, tags, failure reporting
|
|
19
20
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
20
21
|
│ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
|
|
21
22
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
@@ -41,12 +42,13 @@ django-lambda-tasks/
|
|
|
41
42
|
## Module Responsibilities
|
|
42
43
|
|
|
43
44
|
- `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
|
|
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`)
|
|
45
|
+
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic; `execute_immediately` owns the logging + Sentry context and delegates the run to a private `_run` method), `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`)
|
|
45
46
|
- `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
|
|
46
47
|
- `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()`
|
|
47
48
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
48
49
|
- `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
|
|
49
50
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
51
|
+
- `sentry.py` — `task_scope` context manager + `capture_task_exception` helper. `task_scope` opens an isolated `new_scope` and traces the task: a **child span** of the invocation transaction on Lambda (where `AwsLambdaIntegration` already started one), or its **own root transaction** on Batch (nothing active). The child-span-on-Lambda behaviour avoids a second per-task transaction that would double trace volume and defeat `traces_sample_rate`. Records `lambda_task` / `lambda_task.message_id` / `lambda_task.n_retries` / `lambda_task.runtime` as span metadata and scope tags (runtime read from `AWS_LAMBDA_FUNCTION_NAME` or `AWS_BATCH_JQ_NAME`). Deliberately does not call `set_transaction_name` (would rename the shared invocation transaction and break 3.0 segment naming); error attribution rides on the `lambda_task` tag. `capture_task_exception` calls `sentry_sdk.capture_exception()` and is invoked on the terminal failure branches in `models.py` (plain failure + max-retries), since the executor catches exceptions without re-raising and 3.0 does not auto-report caught exceptions; the retry branch does not capture. Future-proof across the SDK 2.x→3.0 boundary (verified against 2.74 and 3.0.0b4): `start_transaction`/`set_tag`/child `start_span` on 2.x, `start_span`/`set_attribute` on 3.0, detected at runtime via `hasattr`. No-op when `sentry_sdk` is absent or uninitialised (`sentry-sdk` is a dev dependency only, pinned `>=2,<3`). Wrapped around the task body in `SQSLambdaTaskMessage.execute_immediately`. Note: `logger.error → Sentry event` needs `LoggingIntegration(event_level=logging.ERROR)` in the host init on 3.0 (default on 2.x); a host concern, demonstrated in the example app
|
|
50
52
|
- `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
|
|
51
53
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
52
54
|
- `apps.py` — Django `AppConfig`; `ready()` imports `checks` to register deployment checks and installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
@@ -23,6 +23,7 @@ inclusion: always
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `pytest` | `>= 9.0.2` | Test runner |
|
|
25
25
|
| `hypothesis` | present | Property-based testing (`.hypothesis/` directory exists) |
|
|
26
|
+
| `sentry-sdk` | `>=2,<3` | Testing the Sentry integration (`task_scope`, `capture_task_exception`); verified against 2.74 and 3.0.0b4. Not a runtime dependency |
|
|
26
27
|
|
|
27
28
|
## Package Manager
|
|
28
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -444,6 +444,57 @@ fields @timestamp, @message
|
|
|
444
444
|
| sort @timestamp asc
|
|
445
445
|
```
|
|
446
446
|
|
|
447
|
+
### Sentry
|
|
448
|
+
|
|
449
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks look the same), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task failed.
|
|
450
|
+
|
|
451
|
+
Keep your existing `sentry_sdk.init(...)`. On **sentry-sdk 2.x** no change is needed. On **sentry-sdk 3.0+** one option changed default and must be set explicitly if you want error-level logs reported (see below):
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
import logging
|
|
455
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
456
|
+
|
|
457
|
+
sentry_sdk.init(
|
|
458
|
+
dsn=SENTRY_DSN,
|
|
459
|
+
integrations=[
|
|
460
|
+
DjangoIntegration(),
|
|
461
|
+
AwsLambdaIntegration(),
|
|
462
|
+
# Only needed on sentry-sdk 3.0+: its default event_level is None, so
|
|
463
|
+
# logger.error(...) no longer becomes a Sentry event. On 2.x this was
|
|
464
|
+
# the default and setting it is harmless.
|
|
465
|
+
LoggingIntegration(event_level=logging.ERROR),
|
|
466
|
+
],
|
|
467
|
+
traces_sample_rate=0.1,
|
|
468
|
+
)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### What the library does per task
|
|
472
|
+
|
|
473
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task:
|
|
474
|
+
|
|
475
|
+
- **On Lambda**, the task is traced as a **child span** of the invocation transaction that `AwsLambdaIntegration` already started — not a second transaction. This keeps task tracing from doubling your transaction volume and respects `traces_sample_rate`.
|
|
476
|
+
- **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
|
|
477
|
+
|
|
478
|
+
Either way it records, as both span metadata and scope tags:
|
|
479
|
+
|
|
480
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`). This is also the task span's name.
|
|
481
|
+
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
482
|
+
- `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
|
|
483
|
+
|
|
484
|
+
Error attribution relies on the `lambda_task` **tag**, not on the transaction name, so it is reliable even when the trace is not sampled and regardless of whether the task ran as a child span (Lambda) or a root (Batch).
|
|
485
|
+
|
|
486
|
+
#### Task failures are reported
|
|
487
|
+
|
|
488
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. So the library calls `sentry_sdk.capture_exception()` itself on the **terminal failure paths** — a plain failure and max-retries-exceeded — to report the error to Sentry. This is essential on 3.0, where a merely-caught exception produces no error event at all.
|
|
489
|
+
|
|
490
|
+
Transient, retryable failures (a task that will be re-enqueued) are **not** reported, to avoid flooding Sentry on every retry; only the final, terminal failure is captured.
|
|
491
|
+
|
|
492
|
+
#### SDK version compatibility
|
|
493
|
+
|
|
494
|
+
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
|
|
495
|
+
|
|
496
|
+
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
|
|
497
|
+
|
|
447
498
|
---
|
|
448
499
|
|
|
449
500
|
## Automatic retries
|
|
@@ -432,6 +432,57 @@ fields @timestamp, @message
|
|
|
432
432
|
| sort @timestamp asc
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
+
### Sentry
|
|
436
|
+
|
|
437
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks look the same), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task failed.
|
|
438
|
+
|
|
439
|
+
Keep your existing `sentry_sdk.init(...)`. On **sentry-sdk 2.x** no change is needed. On **sentry-sdk 3.0+** one option changed default and must be set explicitly if you want error-level logs reported (see below):
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
import logging
|
|
443
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
444
|
+
|
|
445
|
+
sentry_sdk.init(
|
|
446
|
+
dsn=SENTRY_DSN,
|
|
447
|
+
integrations=[
|
|
448
|
+
DjangoIntegration(),
|
|
449
|
+
AwsLambdaIntegration(),
|
|
450
|
+
# Only needed on sentry-sdk 3.0+: its default event_level is None, so
|
|
451
|
+
# logger.error(...) no longer becomes a Sentry event. On 2.x this was
|
|
452
|
+
# the default and setting it is harmless.
|
|
453
|
+
LoggingIntegration(event_level=logging.ERROR),
|
|
454
|
+
],
|
|
455
|
+
traces_sample_rate=0.1,
|
|
456
|
+
)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### What the library does per task
|
|
460
|
+
|
|
461
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task:
|
|
462
|
+
|
|
463
|
+
- **On Lambda**, the task is traced as a **child span** of the invocation transaction that `AwsLambdaIntegration` already started — not a second transaction. This keeps task tracing from doubling your transaction volume and respects `traces_sample_rate`.
|
|
464
|
+
- **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
|
|
465
|
+
|
|
466
|
+
Either way it records, as both span metadata and scope tags:
|
|
467
|
+
|
|
468
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`). This is also the task span's name.
|
|
469
|
+
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
470
|
+
- `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
|
|
471
|
+
|
|
472
|
+
Error attribution relies on the `lambda_task` **tag**, not on the transaction name, so it is reliable even when the trace is not sampled and regardless of whether the task ran as a child span (Lambda) or a root (Batch).
|
|
473
|
+
|
|
474
|
+
#### Task failures are reported
|
|
475
|
+
|
|
476
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. So the library calls `sentry_sdk.capture_exception()` itself on the **terminal failure paths** — a plain failure and max-retries-exceeded — to report the error to Sentry. This is essential on 3.0, where a merely-caught exception produces no error event at all.
|
|
477
|
+
|
|
478
|
+
Transient, retryable failures (a task that will be re-enqueued) are **not** reported, to avoid flooding Sentry on every retry; only the final, terminal failure is captured.
|
|
479
|
+
|
|
480
|
+
#### SDK version compatibility
|
|
481
|
+
|
|
482
|
+
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
|
|
483
|
+
|
|
484
|
+
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
|
|
485
|
+
|
|
435
486
|
---
|
|
436
487
|
|
|
437
488
|
## Automatic retries
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
|
|
3
|
+
from lambda_tasks.decorators import lambda_task
|
|
4
|
+
from lambda_tasks.logging import task_logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@lambda_task()
|
|
8
|
+
def greet(*, name: str) -> str:
|
|
9
|
+
"""A successful task — produces one per-task Sentry transaction named
|
|
10
|
+
``example_app.tasks.greet``."""
|
|
11
|
+
task_logger.info(f"Running greet with: {name=}")
|
|
12
|
+
|
|
13
|
+
for ii in range(3):
|
|
14
|
+
task_logger.info(f"Sleeping: {ii=}")
|
|
15
|
+
sleep(1)
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
raise RuntimeError("testing")
|
|
19
|
+
except Exception:
|
|
20
|
+
task_logger.error("Raising", exc_info=True)
|
|
21
|
+
|
|
22
|
+
return f"Hello, {name}!"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@lambda_task()
|
|
26
|
+
def boom(*, name: str) -> str:
|
|
27
|
+
"""A failing task — the executor records it as a FAILED TaskRecord and the
|
|
28
|
+
error is reported to Sentry attributed to ``example_app.tasks.boom`` (not
|
|
29
|
+
the Lambda function name), inside its own per-task trace."""
|
|
30
|
+
task_logger.info(f"Running boom with: {name=}")
|
|
31
|
+
raise RuntimeError(f"boom for {name}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.http import HttpRequest, HttpResponse
|
|
2
|
+
from example_app.tasks import boom, greet
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def trigger(request: HttpRequest) -> HttpResponse:
|
|
6
|
+
"""Enqueue the successful task — produces a per-task Sentry transaction."""
|
|
7
|
+
name = request.GET.get("name", "world")
|
|
8
|
+
greet.execute_on_commit(name=name)
|
|
9
|
+
return HttpResponse(f"greet task triggered for: {name}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def trigger_error(request: HttpRequest) -> HttpResponse:
|
|
13
|
+
"""Enqueue the failing task — produces a Sentry error attributed to the
|
|
14
|
+
task name, inside its own per-task trace."""
|
|
15
|
+
name = request.GET.get("name", "world")
|
|
16
|
+
boom.execute_on_commit(name=name)
|
|
17
|
+
return HttpResponse(f"boom task triggered for: {name}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Optional Sentry initialisation for the example app.
|
|
2
|
+
|
|
3
|
+
Demonstrates how a host application wires Sentry so that
|
|
4
|
+
``django-lambda-tasks`` can attribute per-task traces and errors. This mirrors
|
|
5
|
+
a typical production ``sentry_sdk.init(...)`` (Django + AWS Lambda integrations
|
|
6
|
+
and tracing), but is driven entirely by environment variables so the example
|
|
7
|
+
runs with or without Sentry configured.
|
|
8
|
+
|
|
9
|
+
``init_sentry()`` is called from ``settings.py`` at import time. Because the
|
|
10
|
+
local process-pool workers call ``django.setup()`` (which imports settings) in
|
|
11
|
+
each child process, this runs in the web process *and* in every task worker —
|
|
12
|
+
which is exactly what is needed for the per-task trace to be captured where the
|
|
13
|
+
task actually executes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init_sentry() -> None:
|
|
21
|
+
"""Initialise Sentry if ``SENTRY_DSN`` is set; otherwise do nothing.
|
|
22
|
+
|
|
23
|
+
Idempotent enough for the example's purposes: ``sentry_sdk.init`` is safe to
|
|
24
|
+
call once per process, and this is only invoked from settings import.
|
|
25
|
+
"""
|
|
26
|
+
dsn = os.environ.get("SENTRY_DSN")
|
|
27
|
+
if not dsn:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
import sentry_sdk
|
|
31
|
+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
|
|
32
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
33
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
34
|
+
|
|
35
|
+
sentry_sdk.init(
|
|
36
|
+
dsn=dsn,
|
|
37
|
+
integrations=[
|
|
38
|
+
DjangoIntegration(),
|
|
39
|
+
AwsLambdaIntegration(),
|
|
40
|
+
# On sentry-sdk 2.x the logging integration defaulted to
|
|
41
|
+
# event_level=ERROR, so logger.error(...) calls (e.g. task_logger)
|
|
42
|
+
# became Sentry events automatically. On 3.0 the default is None, so
|
|
43
|
+
# error logs no longer surface as events. Set it explicitly so
|
|
44
|
+
# task_logger.error output appears in Sentry on both SDK versions.
|
|
45
|
+
LoggingIntegration(event_level=logging.ERROR),
|
|
46
|
+
],
|
|
47
|
+
# Sample every transaction so the per-task trace is always sent while
|
|
48
|
+
# you are testing. Lower this in a real deployment.
|
|
49
|
+
traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "1.0")),
|
|
50
|
+
)
|
{django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/settings.py
RENAMED
|
@@ -88,3 +88,10 @@ LOGGING = {
|
|
|
88
88
|
"level": "INFO",
|
|
89
89
|
},
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
# Optional Sentry integration — set SENTRY_DSN to enable. Initialised here (at
|
|
93
|
+
# settings import) so it also runs inside each local process-pool worker, which
|
|
94
|
+
# imports settings via django.setup(). See example_project/sentry.py.
|
|
95
|
+
from example_project.sentry import init_sentry # noqa: E402
|
|
96
|
+
|
|
97
|
+
init_sentry()
|