django-lambda-tasks 0.11.0__tar.gz → 0.11.2__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.11.0 → django_lambda_tasks-0.11.2}/.github/workflows/ci.yml +32 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/product.md +17 -8
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/structure.md +3 -3
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/tech.md +1 -1
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/PKG-INFO +46 -12
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/README.md +45 -11
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/README.md +13 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/sentry.py +13 -6
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/handler.py +39 -15
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/models.py +3 -3
- django_lambda_tasks-0.11.2/lambda_tasks/sentry.py +201 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/pyproject.toml +2 -2
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_handler.py +140 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_sentry.py +212 -37
- django_lambda_tasks-0.11.0/lambda_tasks/sentry.py +0 -143
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.github/dependabot.yml +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.gitignore +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/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/
|
|
@@ -315,21 +315,30 @@ def my_task(*, user_id: int) -> None:
|
|
|
315
315
|
|
|
316
316
|
## Sentry
|
|
317
317
|
|
|
318
|
-
The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup
|
|
318
|
+
The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup. The **recommended** host configuration is `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **without** `AwsLambdaIntegration`. `AwsLambdaIntegration` names every event's transaction after the Lambda function, which hides which task ran; omitting it lets each task be traced under its own task-named transaction.
|
|
319
319
|
|
|
320
|
-
`lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)`
|
|
320
|
+
`lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` does the per-task tracing. `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. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, both on the span and as scope tags:
|
|
321
321
|
|
|
322
|
-
-
|
|
323
|
-
- `lambda_task` = task name
|
|
322
|
+
- `lambda_task` = fully-qualified task name (e.g. `myapp.tasks.my_task`) — also the transaction/span name
|
|
324
323
|
- `lambda_task.message_id` = the `TaskRecord` primary key
|
|
325
324
|
- `lambda_task.n_retries` = the retry counter
|
|
326
|
-
- `lambda_task.runtime` = the
|
|
325
|
+
- `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
|
|
327
326
|
|
|
328
|
-
|
|
327
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
329
328
|
|
|
330
|
-
**
|
|
329
|
+
**If a host keeps `AwsLambdaIntegration` anyway:** the library still behaves correctly. `_start_task_root` detects the active invocation transaction (via `sentry_sdk.get_current_scope().transaction` on 2.x; on 3.0 `start_span` is automatically root-or-child) and traces the task as a **child span** of it rather than a second, competing transaction. A nested *transaction* per task would inherit (not independently re-roll) the invocation's sampling decision, doubling transaction volume and defeating `traces_sample_rate`; a child span is sent only if its parent is sampled. In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
331
330
|
|
|
332
|
-
`task_scope`
|
|
331
|
+
`task_scope` deliberately does **not** call `scope.set_transaction_name(task_name)`: when an invocation transaction is active it would rename the shared transaction (fighting across sibling records in a batch) and, on SDK 3.0 stream mode, corrupt the root-segment naming. The `lambda_task` tag is the reliable, version-independent attribution.
|
|
332
|
+
|
|
333
|
+
**Reporting task failures:** the executor catches task exceptions (recording `FAILED` and not re-raising). On the two **terminal** failure branches in `models.py` (plain failure and max-retries-exceeded) it logs the exception at ERROR via `task_logger.error(error, exc_info=True)`. With the assumed host config `LoggingIntegration(event_level=logging.ERROR)`, that log becomes a Sentry event, attributed to the task by the active `task_scope` tags. No explicit `capture_exception` call is made — the error log is the single reporting path, so there are no duplicate events. The **retry** branch logs at WARNING and is deliberately not reported — transient, to-be-re-enqueued failures would flood Sentry; only terminal failures are reported.
|
|
334
|
+
|
|
335
|
+
**Reporting failures outside a task:** pre-execution failures in the handler (malformed SQS message, un-importable task, misconfiguration, cold-start errors) happen before `task_scope` opens. `handler.py` logs them at ERROR inside `lambda_tasks.sentry.handler_error_scope(message_id=...)`, a context manager that tags an isolated scope (`lambda_task.handler_error` + the message id) so the resulting log-driven Sentry event is identifiable. This relies on the same `LoggingIntegration(event_level=ERROR)` as task failures. Cold-start failures are additionally re-raised so AWS marks the invocation failed and redrives.
|
|
336
|
+
|
|
337
|
+
**Error-level log forwarding (required):** both task-failure and handler-failure reporting flow through `logger.error(..., exc_info=True)` + the host's `LoggingIntegration(event_level=logging.ERROR)`. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so the host **must** configure it explicitly for error reporting to work (it is part of the recommended config and the example app). The library does not call `capture_exception` itself, so this integration is not optional for error visibility.
|
|
338
|
+
|
|
339
|
+
**SDK version compatibility:** the integration 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.
|
|
340
|
+
|
|
341
|
+
`task_scope` and `handler_error_scope` 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; a dedicated CI job additionally runs the suite against the latest sentry-sdk 3.x pre-release on Python 3.14.
|
|
333
342
|
|
|
334
343
|
## Built-in Tasks
|
|
335
344
|
|
|
@@ -16,7 +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 — per-task
|
|
19
|
+
│ ├── sentry.py # task_scope + handler_error_scope — per-task trace, tags, failure attribution
|
|
20
20
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
21
21
|
│ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
|
|
22
22
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
@@ -44,11 +44,11 @@ django-lambda-tasks/
|
|
|
44
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
|
|
45
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`)
|
|
46
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
|
|
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
|
|
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`; logs pre-execution failures (malformed message, import error, misconfiguration) at ERROR inside `handler_error_scope` so the host's `LoggingIntegration` reports them tagged `lambda_task.handler_error`; cold-start failures are reported the same way and re-raised. 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()`
|
|
48
48
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
49
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
|
|
50
50
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
51
|
-
- `sentry.py` — `task_scope` context manager
|
|
51
|
+
- `sentry.py` — `task_scope` context manager + `handler_error_scope` context manager. `task_scope` opens an isolated `new_scope` and traces the task. With the **recommended** host config (no `AwsLambdaIntegration`) nothing is active, so the task starts its **own transaction named after the task** on both Lambda and Batch. If a host keeps `AwsLambdaIntegration`, the task is instead traced as a **child span** of the active invocation transaction (detected via `sentry_sdk.get_current_scope().transaction` on 2.x; `start_span` is auto root-or-child on 3.0) — never a second, competing transaction, which 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 an active invocation transaction and break 3.0 segment naming); error attribution rides on the `lambda_task` tag. **Failure reporting** is via error logs, not explicit captures: the terminal failure branches in `models.py` (plain failure + max-retries) call `task_logger.error(error, exc_info=True)`, and the host's `LoggingIntegration(event_level=ERROR)` turns that into the Sentry event (the retry branch logs at WARNING and is not reported). `handler_error_scope(message_id=...)` is a context manager used in `handler.py` to tag an isolated scope (`lambda_task.handler_error` + message id) around the handler's `logger.error`, so pre-task failures are reported and identifiable via the same LoggingIntegration. 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`. Error reporting requires `LoggingIntegration(event_level=logging.ERROR)` in the host init (default on 2.x, must be set on 3.0); demonstrated in the example app
|
|
52
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
|
|
53
53
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
54
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,7 +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` |
|
|
26
|
+
| `sentry-sdk` | `>=2,<3` | Testing the Sentry integration (`task_scope`, `handler_error_scope`); verified against 2.74 and 3.0.0b4. Not a runtime dependency |
|
|
27
27
|
|
|
28
28
|
## Package Manager
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -446,29 +446,63 @@ fields @timestamp, @message
|
|
|
446
446
|
|
|
447
447
|
### Sentry
|
|
448
448
|
|
|
449
|
-
If your application uses Sentry,
|
|
449
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
#### Recommended `sentry_sdk.init(...)`
|
|
452
|
+
|
|
453
|
+
Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
|
|
452
454
|
|
|
453
455
|
```python
|
|
456
|
+
import logging
|
|
457
|
+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
|
|
458
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
459
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
460
|
+
|
|
454
461
|
sentry_sdk.init(
|
|
455
462
|
dsn=SENTRY_DSN,
|
|
456
|
-
integrations=[
|
|
457
|
-
|
|
463
|
+
integrations=[
|
|
464
|
+
DjangoIntegration(),
|
|
465
|
+
LoggingIntegration(event_level=logging.ERROR),
|
|
466
|
+
],
|
|
467
|
+
disabled_integrations=[
|
|
468
|
+
AwsLambdaIntegration(),
|
|
469
|
+
],
|
|
470
|
+
traces_sample_rate=0.1,
|
|
458
471
|
)
|
|
459
472
|
```
|
|
460
473
|
|
|
461
|
-
|
|
474
|
+
Two deliberate choices:
|
|
475
|
+
|
|
476
|
+
- **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
|
|
477
|
+
- **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
|
|
478
|
+
|
|
479
|
+
#### What the library does per task
|
|
480
|
+
|
|
481
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
|
|
482
|
+
|
|
483
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
|
|
484
|
+
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
485
|
+
- `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).
|
|
486
|
+
|
|
487
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
488
|
+
|
|
489
|
+
> If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
490
|
+
|
|
491
|
+
#### Task failures are reported
|
|
492
|
+
|
|
493
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
|
|
494
|
+
|
|
495
|
+
Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
|
|
496
|
+
|
|
497
|
+
#### Failures outside a task
|
|
462
498
|
|
|
463
|
-
- the
|
|
464
|
-
- `lambda_task` (task name), `lambda_task.message_id`, and `lambda_task.n_retries`
|
|
465
|
-
- `lambda_task.runtime` — the physical execution environment, derived from the AWS environment: the Lambda function name (`AWS_LAMBDA_FUNCTION_NAME`) on Lambda, or the Batch job queue name (`AWS_BATCH_JQ_NAME`) on Batch. Omitted when neither is set (e.g. local/eager execution).
|
|
499
|
+
Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
|
|
466
500
|
|
|
467
|
-
|
|
501
|
+
#### SDK version compatibility
|
|
468
502
|
|
|
469
|
-
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary
|
|
503
|
+
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.
|
|
470
504
|
|
|
471
|
-
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the
|
|
505
|
+
`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.
|
|
472
506
|
|
|
473
507
|
---
|
|
474
508
|
|
|
@@ -434,29 +434,63 @@ fields @timestamp, @message
|
|
|
434
434
|
|
|
435
435
|
### Sentry
|
|
436
436
|
|
|
437
|
-
If your application uses Sentry,
|
|
437
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
#### Recommended `sentry_sdk.init(...)`
|
|
440
|
+
|
|
441
|
+
Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
|
|
440
442
|
|
|
441
443
|
```python
|
|
444
|
+
import logging
|
|
445
|
+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
|
|
446
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
447
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
448
|
+
|
|
442
449
|
sentry_sdk.init(
|
|
443
450
|
dsn=SENTRY_DSN,
|
|
444
|
-
integrations=[
|
|
445
|
-
|
|
451
|
+
integrations=[
|
|
452
|
+
DjangoIntegration(),
|
|
453
|
+
LoggingIntegration(event_level=logging.ERROR),
|
|
454
|
+
],
|
|
455
|
+
disabled_integrations=[
|
|
456
|
+
AwsLambdaIntegration(),
|
|
457
|
+
],
|
|
458
|
+
traces_sample_rate=0.1,
|
|
446
459
|
)
|
|
447
460
|
```
|
|
448
461
|
|
|
449
|
-
|
|
462
|
+
Two deliberate choices:
|
|
463
|
+
|
|
464
|
+
- **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
|
|
465
|
+
- **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
|
|
466
|
+
|
|
467
|
+
#### What the library does per task
|
|
468
|
+
|
|
469
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
|
|
470
|
+
|
|
471
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
|
|
472
|
+
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
473
|
+
- `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).
|
|
474
|
+
|
|
475
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
476
|
+
|
|
477
|
+
> If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
478
|
+
|
|
479
|
+
#### Task failures are reported
|
|
480
|
+
|
|
481
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
|
|
482
|
+
|
|
483
|
+
Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
|
|
484
|
+
|
|
485
|
+
#### Failures outside a task
|
|
450
486
|
|
|
451
|
-
- the
|
|
452
|
-
- `lambda_task` (task name), `lambda_task.message_id`, and `lambda_task.n_retries`
|
|
453
|
-
- `lambda_task.runtime` — the physical execution environment, derived from the AWS environment: the Lambda function name (`AWS_LAMBDA_FUNCTION_NAME`) on Lambda, or the Batch job queue name (`AWS_BATCH_JQ_NAME`) on Batch. Omitted when neither is set (e.g. local/eager execution).
|
|
487
|
+
Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
|
|
454
488
|
|
|
455
|
-
|
|
489
|
+
#### SDK version compatibility
|
|
456
490
|
|
|
457
|
-
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary
|
|
491
|
+
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.
|
|
458
492
|
|
|
459
|
-
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the
|
|
493
|
+
`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.
|
|
460
494
|
|
|
461
495
|
---
|
|
462
496
|
|
|
@@ -47,3 +47,16 @@ The task runs in a background worker process and the response confirms it was tr
|
|
|
47
47
|
Visit: `http://127.0.0.1:8000/admin/` → Lambda Tasks → Task records
|
|
48
48
|
|
|
49
49
|
Log in with the superuser credentials you created above.
|
|
50
|
+
|
|
51
|
+
## Testing the Sentry integration (optional)
|
|
52
|
+
|
|
53
|
+
Set `SENTRY_DSN` to see per-task traces and error attribution in Sentry. The init lives in `example_project/sentry.py` and uses `DjangoIntegration` + `LoggingIntegration(event_level=ERROR)` **without** `AwsLambdaIntegration`, so each task is traced under its own task-named transaction (see the library README's Sentry section for why).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
SENTRY_DSN="https://<key>@<org>.ingest.sentry.io/<project>" uv run python manage.py runserver
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `http://127.0.0.1:8000/trigger/?name=Alice` — a successful task; produces a transaction named `example_app.tasks.greet`.
|
|
60
|
+
- `http://127.0.0.1:8000/trigger-error/?name=Bob` — a failing task; produces an error attributed to `example_app.tasks.boom` (via the library's `capture_exception` on the terminal failure path).
|
|
61
|
+
|
|
62
|
+
Without `SENTRY_DSN` set, Sentry initialisation is skipped and the app runs exactly as above.
|
|
@@ -36,14 +36,21 @@ def init_sentry() -> None:
|
|
|
36
36
|
dsn=dsn,
|
|
37
37
|
integrations=[
|
|
38
38
|
DjangoIntegration(),
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
39
|
+
# LoggingIntegration(event_level=ERROR) is REQUIRED, not optional:
|
|
40
|
+
# the library reports task and handler failures by logging them at
|
|
41
|
+
# ERROR (not by calling capture_exception), so this integration is
|
|
42
|
+
# what turns those logs into Sentry events. On sentry-sdk 2.x
|
|
43
|
+
# event_level=ERROR was the default; on 3.0 the default is None, so
|
|
44
|
+
# it must be set explicitly.
|
|
45
45
|
LoggingIntegration(event_level=logging.ERROR),
|
|
46
46
|
],
|
|
47
|
+
disabled_integrations=[
|
|
48
|
+
# AwsLambdaIntegration is intentionally omitted. It names every
|
|
49
|
+
# event's transaction after the Lambda function, which hides the
|
|
50
|
+
# task name. Without it, each task's trace is named after the task
|
|
51
|
+
# (see lambda_tasks.sentry).
|
|
52
|
+
AwsLambdaIntegration(),
|
|
53
|
+
],
|
|
47
54
|
# Sample every transaction so the per-task trace is always sent while
|
|
48
55
|
# you are testing. Lower this in a real deployment.
|
|
49
56
|
traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "1.0")),
|
|
@@ -26,6 +26,7 @@ from django.apps import apps
|
|
|
26
26
|
|
|
27
27
|
from lambda_tasks.environment_loader import resolve_environment
|
|
28
28
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
29
|
+
from lambda_tasks.sentry import handler_error_scope
|
|
29
30
|
|
|
30
31
|
logger = logging.getLogger(__name__)
|
|
31
32
|
|
|
@@ -124,7 +125,17 @@ def handler(event: dict, context: object) -> dict:
|
|
|
124
125
|
Returns a partial-batch failure report so AWS only re-drives failed records.
|
|
125
126
|
Signature is fixed by AWS and uses two args only.
|
|
126
127
|
"""
|
|
127
|
-
|
|
128
|
+
try:
|
|
129
|
+
_perform_cold_start()
|
|
130
|
+
except Exception:
|
|
131
|
+
# Cold-start failures (secret resolution, django.setup(), memory limit)
|
|
132
|
+
# escape handler() so the invocation is marked failed and redriven, as
|
|
133
|
+
# AWS expects. The error log is reported to Sentry by the host's
|
|
134
|
+
# LoggingIntegration; handler_error_scope tags it as a handler error.
|
|
135
|
+
# Re-raise to preserve the AWS failure contract.
|
|
136
|
+
with handler_error_scope():
|
|
137
|
+
logger.error("Cold-start initialisation failed", exc_info=True)
|
|
138
|
+
raise
|
|
128
139
|
|
|
129
140
|
# Local import due to AppRegistryNotReady
|
|
130
141
|
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
@@ -138,10 +149,13 @@ def handler(event: dict, context: object) -> dict:
|
|
|
138
149
|
record["body"]
|
|
139
150
|
).execute_immediately(message_id=record["messageId"])
|
|
140
151
|
except Exception:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
# Pre-execution failures (malformed message, import error,
|
|
153
|
+
# misconfiguration) happen before execute_immediately opens its
|
|
154
|
+
# task_scope. The error log is reported to Sentry by the host's
|
|
155
|
+
# LoggingIntegration; handler_error_scope tags it with the message
|
|
156
|
+
# id and marks it a handler error.
|
|
157
|
+
with handler_error_scope(message_id=record.get("messageId")):
|
|
158
|
+
logger.error("Failed to process SQS record", exc_info=True)
|
|
145
159
|
batch_item_failures.append({"itemIdentifier": record["messageId"]})
|
|
146
160
|
|
|
147
161
|
return {"batchItemFailures": batch_item_failures}
|
|
@@ -154,21 +168,31 @@ def main() -> int:
|
|
|
154
168
|
constructs a synthetic SQS event, and delegates to handler().
|
|
155
169
|
|
|
156
170
|
Returns 0 on success, 1 on failure.
|
|
171
|
+
|
|
172
|
+
Wrapped in a top-level handler so that failures on the Batch path — a
|
|
173
|
+
missing message, or an exception escaping cold-start/``handler()`` (e.g.
|
|
174
|
+
secret resolution or ``django.setup()``) — are reported to Sentry. The
|
|
175
|
+
Batch container has no AWS-side Sentry integration, so without this such
|
|
176
|
+
failures would only reach stderr.
|
|
157
177
|
"""
|
|
158
178
|
logging.basicConfig(level=logging.INFO)
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
try:
|
|
181
|
+
message_json = os.environ.get("LAMBDA_TASKS_MESSAGE")
|
|
182
|
+
if not message_json:
|
|
183
|
+
raise RuntimeError("LAMBDA_TASKS_MESSAGE environment variable is not set.")
|
|
184
|
+
|
|
185
|
+
message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
|
|
186
|
+
|
|
187
|
+
result = handler(
|
|
188
|
+
event={"Records": [{"messageId": message_id, "body": message_json}]},
|
|
189
|
+
context=None,
|
|
190
|
+
)
|
|
191
|
+
except Exception:
|
|
192
|
+
with handler_error_scope(message_id=os.environ.get("AWS_BATCH_JOB_ID")):
|
|
193
|
+
logger.error("AWS Batch entry point failed", exc_info=True)
|
|
163
194
|
return 1
|
|
164
195
|
|
|
165
|
-
message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
|
|
166
|
-
|
|
167
|
-
result = handler(
|
|
168
|
-
event={"Records": [{"messageId": message_id, "body": message_json}]},
|
|
169
|
-
context=None,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
196
|
if result["batchItemFailures"]:
|
|
173
197
|
return 1
|
|
174
198
|
else:
|
|
@@ -21,7 +21,7 @@ from redis.exceptions import LockError
|
|
|
21
21
|
|
|
22
22
|
from lambda_tasks.local_executor import submit_task
|
|
23
23
|
from lambda_tasks.logging import task_logger
|
|
24
|
-
from lambda_tasks.sentry import
|
|
24
|
+
from lambda_tasks.sentry import task_scope
|
|
25
25
|
from lambda_tasks.settings import (
|
|
26
26
|
MAX_DELAY,
|
|
27
27
|
BatchQueueConfig,
|
|
@@ -210,7 +210,8 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
210
210
|
elif effective_retry_on and isinstance(error, effective_retry_on):
|
|
211
211
|
conf = LambdaTasksSettings()
|
|
212
212
|
if self.n_retries >= conf.MAX_RETRIES:
|
|
213
|
-
|
|
213
|
+
task_logger.error("Maximum retries exceeded", exc_info=True)
|
|
214
|
+
|
|
214
215
|
record.status = TaskStatus.FAILED
|
|
215
216
|
record.traceback = traceback.format_exc()
|
|
216
217
|
record.end_time = now()
|
|
@@ -252,7 +253,6 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
252
253
|
else:
|
|
253
254
|
task_logger.error(error, exc_info=True)
|
|
254
255
|
|
|
255
|
-
capture_task_exception()
|
|
256
256
|
record.status = TaskStatus.FAILED
|
|
257
257
|
record.traceback = traceback.format_exc()
|
|
258
258
|
record.end_time = now()
|