django-lambda-tasks 0.10.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/steering/product.md +19 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/steering/structure.md +3 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/steering/tech.md +1 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/PKG-INFO +27 -1
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/README.md +26 -0
- django_lambda_tasks-0.11.0/example/example_app/tasks.py +31 -0
- django_lambda_tasks-0.11.0/example/example_app/urls.py +7 -0
- django_lambda_tasks-0.11.0/example/example_app/views.py +17 -0
- django_lambda_tasks-0.11.0/example/example_project/sentry.py +50 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_project/settings.py +7 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/models.py +138 -118
- django_lambda_tasks-0.11.0/lambda_tasks/sentry.py +143 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/pyproject.toml +2 -1
- django_lambda_tasks-0.11.0/tests/test_sentry.py +448 -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.0}/.github/dependabot.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.gitignore +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/README.md +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/example/manage.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/handler.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/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.0}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.0}/tests/test_timeouts.py +0 -0
|
@@ -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,24 @@ 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 share one transaction (and a single invocation-level transaction is shared across a batch of SQS records); on AWS Batch there is no request and no Lambda integration, so the transaction and URL are `"--"` and `traces_sample_rate` produces no trace for the task body. 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 a per-task root span/transaction** named after the task. This gives one trace per task on both Lambda and Batch. It records, both on the span and as scope tags:
|
|
321
|
+
|
|
322
|
+
- transaction/span **name** = the fully-qualified task name (e.g. `myapp.tasks.my_task`)
|
|
323
|
+
- `lambda_task` = task name
|
|
324
|
+
- `lambda_task.message_id` = the `TaskRecord` primary key
|
|
325
|
+
- `lambda_task.n_retries` = the retry counter
|
|
326
|
+
- `lambda_task.runtime` = the physical execution environment, read 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 present
|
|
327
|
+
|
|
328
|
+
The scope covers the success, retry, and failure paths, so task-logic exceptions (which the executor records as `FAILED` and does not re-raise to the handler) still carry the task name when captured by Sentry. The scope tags are kept in addition to span metadata so error events are attributed even when the performance span is not sampled.
|
|
329
|
+
|
|
330
|
+
**SDK version compatibility:** `task_scope` is future-proof across the sentry-sdk 2.x→3.0 boundary. On 2.x it uses `start_transaction` + `set_tag` (a root span must be a transaction to be sent); on 3.0 (stream mode, where transactions are removed and tags no longer propagate to spans) it uses `start_span` + `set_attribute`. The correct API is detected at runtime via `hasattr`, so no code change is needed when the host upgrades the SDK.
|
|
331
|
+
|
|
332
|
+
`task_scope` is a no-op 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, for the test suite. No custom Sentry integration is needed — per-task tracing at the one execution entry point covers both Lambda and Batch.
|
|
333
|
+
|
|
316
334
|
## Built-in Tasks
|
|
317
335
|
|
|
318
336
|
`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 — per-task Sentry transaction name + tags
|
|
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; opens an isolated `new_scope` and a per-task root span/transaction named after the task, so both errors and performance traces are attributed per task (one trace per task on Lambda and Batch, not one invocation-level transaction shared across an SQS batch). Records `lambda_task` / `lambda_task.message_id` / `lambda_task.n_retries` / `lambda_task.runtime` as span metadata and scope tags; `lambda_task.runtime` is the execution environment read from `AWS_LAMBDA_FUNCTION_NAME` (Lambda) or `AWS_BATCH_JQ_NAME` (Batch). Future-proof across the SDK 2.x→3.0 boundary: it uses `start_transaction`+`set_tag` on 2.x and `start_span`+`set_attribute` on 3.0 (stream mode), detected at runtime via `hasattr`. A no-op when `sentry_sdk` is absent or uninitialised, so the library has no hard dependency on Sentry (`sentry-sdk` is a dev dependency only). Wrapped around the task body in `SQSLambdaTaskMessage.execute_immediately`
|
|
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` | present | Testing the `task_scope` Sentry integration; 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.0
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -444,6 +444,32 @@ fields @timestamp, @message
|
|
|
444
444
|
| sort @timestamp asc
|
|
445
445
|
```
|
|
446
446
|
|
|
447
|
+
### Sentry
|
|
448
|
+
|
|
449
|
+
If your application uses Sentry, tasks are automatically attributed to their task name. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks share one transaction), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task raised an error.
|
|
450
|
+
|
|
451
|
+
You do **not** need to add a custom integration or change your `sentry_sdk.init(...)`. Keep your existing setup:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
sentry_sdk.init(
|
|
455
|
+
dsn=SENTRY_DSN,
|
|
456
|
+
integrations=[DjangoIntegration(), AwsLambdaIntegration()],
|
|
457
|
+
# ...
|
|
458
|
+
)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope **and a per-task trace** (a root span/transaction named after the task). With `traces_sample_rate` set, you get one trace per task on both Lambda and Batch — rather than a single invocation-level transaction shared across a batch of SQS records. It sets, on both the span and the scope:
|
|
462
|
+
|
|
463
|
+
- the **transaction/span name** to the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`), overriding the Lambda function name on Lambda and replacing `"--"` on Batch
|
|
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).
|
|
466
|
+
|
|
467
|
+
This covers task failures too: the executor records failures as `FAILED` `TaskRecord`s rather than re-raising, so the trace wraps the task body to ensure Sentry error events still carry the task name.
|
|
468
|
+
|
|
469
|
+
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary: 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 the SDK.
|
|
470
|
+
|
|
471
|
+
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the enrichment is a silent no-op.
|
|
472
|
+
|
|
447
473
|
---
|
|
448
474
|
|
|
449
475
|
## Automatic retries
|
|
@@ -432,6 +432,32 @@ fields @timestamp, @message
|
|
|
432
432
|
| sort @timestamp asc
|
|
433
433
|
```
|
|
434
434
|
|
|
435
|
+
### Sentry
|
|
436
|
+
|
|
437
|
+
If your application uses Sentry, tasks are automatically attributed to their task name. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks share one transaction), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task raised an error.
|
|
438
|
+
|
|
439
|
+
You do **not** need to add a custom integration or change your `sentry_sdk.init(...)`. Keep your existing setup:
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
sentry_sdk.init(
|
|
443
|
+
dsn=SENTRY_DSN,
|
|
444
|
+
integrations=[DjangoIntegration(), AwsLambdaIntegration()],
|
|
445
|
+
# ...
|
|
446
|
+
)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope **and a per-task trace** (a root span/transaction named after the task). With `traces_sample_rate` set, you get one trace per task on both Lambda and Batch — rather than a single invocation-level transaction shared across a batch of SQS records. It sets, on both the span and the scope:
|
|
450
|
+
|
|
451
|
+
- the **transaction/span name** to the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`), overriding the Lambda function name on Lambda and replacing `"--"` on Batch
|
|
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).
|
|
454
|
+
|
|
455
|
+
This covers task failures too: the executor records failures as `FAILED` `TaskRecord`s rather than re-raising, so the trace wraps the task body to ensure Sentry error events still carry the task name.
|
|
456
|
+
|
|
457
|
+
The integration is future-proof across the sentry-sdk 2.x→3.0 boundary: 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 the SDK.
|
|
458
|
+
|
|
459
|
+
`sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the enrichment is a silent no-op.
|
|
460
|
+
|
|
435
461
|
---
|
|
436
462
|
|
|
437
463
|
## 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.0}/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()
|
|
@@ -21,6 +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 capture_task_exception, task_scope
|
|
24
25
|
from lambda_tasks.settings import (
|
|
25
26
|
MAX_DELAY,
|
|
26
27
|
BatchQueueConfig,
|
|
@@ -112,151 +113,170 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
112
113
|
Creates a TaskRecord, resolves timeouts, validates configuration,
|
|
113
114
|
runs the task inside an atomic block with timeout enforcement, and
|
|
114
115
|
persists the outcome.
|
|
116
|
+
|
|
117
|
+
Logging and Sentry context are scoped to the task for the duration of
|
|
118
|
+
the run so that exceptions are attributed to the task name rather than
|
|
119
|
+
to the Lambda function name or, on AWS Batch, to nothing at all.
|
|
115
120
|
"""
|
|
116
121
|
task_logger.message_id = message_id
|
|
117
122
|
|
|
118
123
|
try:
|
|
119
|
-
|
|
124
|
+
with task_scope(
|
|
125
|
+
task_name=self.task_name,
|
|
126
|
+
message_id=message_id,
|
|
127
|
+
n_retries=self.n_retries,
|
|
128
|
+
):
|
|
129
|
+
self._run(message_id=message_id)
|
|
130
|
+
finally:
|
|
131
|
+
task_logger.message_id = None
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
def _run(self, *, message_id: str) -> None:
|
|
134
|
+
"""Resolve, run, and persist the outcome of the task.
|
|
123
135
|
|
|
124
|
-
|
|
136
|
+
Separated from :meth:`execute_immediately` so that the latter owns only
|
|
137
|
+
the logging and Sentry context management; this method holds the
|
|
138
|
+
execution logic and its early returns.
|
|
139
|
+
"""
|
|
140
|
+
task_logger.info(f"Received {self.task_name}")
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
f"import_string('{self.task_name}') returned {type(wrapper)!r}, "
|
|
129
|
-
f"expected LambdaTaskWrapper."
|
|
130
|
-
)
|
|
142
|
+
# Local import to avoid circular dependency
|
|
143
|
+
from lambda_tasks.decorators import LambdaTaskWrapper
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
pk=message_id,
|
|
139
|
-
defaults={
|
|
140
|
-
"task_name": self.task_name,
|
|
141
|
-
"kwargs": self.model_dump(mode="json")["kwargs"],
|
|
142
|
-
"n_retries": self.n_retries,
|
|
143
|
-
"status": TaskStatus.RUNNING,
|
|
144
|
-
"start_time": now(),
|
|
145
|
-
},
|
|
146
|
-
)
|
|
145
|
+
wrapper = import_string(self.task_name)
|
|
146
|
+
|
|
147
|
+
if not isinstance(wrapper, LambdaTaskWrapper):
|
|
148
|
+
raise TypeError(
|
|
149
|
+
f"import_string('{self.task_name}') returned {type(wrapper)!r}, "
|
|
150
|
+
f"expected LambdaTaskWrapper."
|
|
147
151
|
)
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
# Resolve before creating the TaskRecord — ConfigurationError here means
|
|
154
|
+
# misconfigured settings, not a task failure, so no record should be written.
|
|
155
|
+
soft_timeout, hard_timeout = wrapper.resolved_timeouts
|
|
156
|
+
|
|
157
|
+
record, created = (
|
|
158
|
+
TaskRecord.objects.get_or_create( # ty: ignore[unresolved-attribute]
|
|
159
|
+
pk=message_id,
|
|
160
|
+
defaults={
|
|
161
|
+
"task_name": self.task_name,
|
|
162
|
+
"kwargs": self.model_dump(mode="json")["kwargs"],
|
|
163
|
+
"n_retries": self.n_retries,
|
|
164
|
+
"status": TaskStatus.RUNNING,
|
|
165
|
+
"start_time": now(),
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
)
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
if not created:
|
|
171
|
+
task_logger.warning(
|
|
172
|
+
f"Skipping duplicate delivery (existing record status={record.status})"
|
|
173
|
+
)
|
|
174
|
+
return
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
ignored_exception: BaseException | None = None
|
|
177
|
+
ignored_traceback: str | None = None
|
|
178
|
+
result = None
|
|
179
|
+
|
|
180
|
+
if wrapper.singleton:
|
|
181
|
+
cache = caches[LambdaTasksSettings().SINGLETON_CACHE]
|
|
182
|
+
lock_ctx = cache.lock(
|
|
183
|
+
f"lambda_tasks.singleton_lock.{self.task_name}",
|
|
184
|
+
blocking_timeout=0,
|
|
185
|
+
timeout=hard_timeout,
|
|
186
|
+
)
|
|
187
|
+
if wrapper.retry_singleton:
|
|
188
|
+
effective_retry_on = (LockError, *wrapper.retry_on)
|
|
170
189
|
else:
|
|
171
|
-
lock_ctx = contextlib.nullcontext()
|
|
172
190
|
effective_retry_on = wrapper.retry_on
|
|
191
|
+
else:
|
|
192
|
+
lock_ctx = contextlib.nullcontext()
|
|
193
|
+
effective_retry_on = wrapper.retry_on
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
record.traceback = traceback.format_exc()
|
|
194
|
-
record.end_time = now()
|
|
195
|
-
record.save(update_fields=["status", "traceback", "end_time"])
|
|
196
|
-
|
|
197
|
-
task_logger.warning(
|
|
198
|
-
f"Failed due to MaxRetriesExceededError in {record.duration}"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
raise MaxRetriesExceededError(
|
|
202
|
-
task_name=self.task_name, n_retries=self.n_retries
|
|
203
|
-
)
|
|
204
|
-
else:
|
|
205
|
-
record.status = TaskStatus.RETRIED
|
|
206
|
-
record.traceback = traceback.format_exc()
|
|
207
|
-
record.end_time = now()
|
|
208
|
-
record.save(update_fields=["status", "traceback", "end_time"])
|
|
209
|
-
|
|
210
|
-
task_logger.warning(
|
|
211
|
-
f"Retrying (due to {type(error).__name__}) after {record.duration}"
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
delay = min(
|
|
215
|
-
wrapper.retry_delay + round(random.uniform(1, 5)), MAX_DELAY
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
retry_task = SQSLambdaTask(
|
|
219
|
-
message=SQSLambdaTaskMessage(
|
|
220
|
-
task_name=self.task_name,
|
|
221
|
-
kwargs=self.kwargs,
|
|
222
|
-
n_retries=self.n_retries + 1,
|
|
223
|
-
),
|
|
224
|
-
delay=delay,
|
|
225
|
-
queue=wrapper.queue,
|
|
226
|
-
)
|
|
227
|
-
retry_task.execute_on_commit()
|
|
228
|
-
|
|
229
|
-
return
|
|
230
|
-
else:
|
|
231
|
-
task_logger.error(error, exc_info=True)
|
|
232
|
-
|
|
195
|
+
try:
|
|
196
|
+
with lock_ctx:
|
|
197
|
+
with transaction.atomic():
|
|
198
|
+
with TimeoutContext(
|
|
199
|
+
soft_timeout=soft_timeout, hard_timeout=hard_timeout
|
|
200
|
+
):
|
|
201
|
+
result = wrapper(**self.kwargs)
|
|
202
|
+
except Exception as error:
|
|
203
|
+
if (
|
|
204
|
+
wrapper.singleton
|
|
205
|
+
and not wrapper.retry_singleton
|
|
206
|
+
and isinstance(error, LockError)
|
|
207
|
+
):
|
|
208
|
+
ignored_exception = error
|
|
209
|
+
ignored_traceback = traceback.format_exc()
|
|
210
|
+
elif effective_retry_on and isinstance(error, effective_retry_on):
|
|
211
|
+
conf = LambdaTasksSettings()
|
|
212
|
+
if self.n_retries >= conf.MAX_RETRIES:
|
|
213
|
+
capture_task_exception()
|
|
233
214
|
record.status = TaskStatus.FAILED
|
|
234
215
|
record.traceback = traceback.format_exc()
|
|
235
216
|
record.end_time = now()
|
|
236
217
|
record.save(update_fields=["status", "traceback", "end_time"])
|
|
237
218
|
|
|
238
|
-
task_logger.warning(
|
|
239
|
-
|
|
219
|
+
task_logger.warning(
|
|
220
|
+
f"Failed due to MaxRetriesExceededError in {record.duration}"
|
|
221
|
+
)
|
|
240
222
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
223
|
+
raise MaxRetriesExceededError(
|
|
224
|
+
task_name=self.task_name, n_retries=self.n_retries
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
record.status = TaskStatus.RETRIED
|
|
228
|
+
record.traceback = traceback.format_exc()
|
|
229
|
+
record.end_time = now()
|
|
230
|
+
record.save(update_fields=["status", "traceback", "end_time"])
|
|
246
231
|
|
|
247
|
-
|
|
232
|
+
task_logger.warning(
|
|
233
|
+
f"Retrying (due to {type(error).__name__}) after {record.duration}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
delay = min(
|
|
237
|
+
wrapper.retry_delay + round(random.uniform(1, 5)), MAX_DELAY
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
retry_task = SQSLambdaTask(
|
|
241
|
+
message=SQSLambdaTaskMessage(
|
|
242
|
+
task_name=self.task_name,
|
|
243
|
+
kwargs=self.kwargs,
|
|
244
|
+
n_retries=self.n_retries + 1,
|
|
245
|
+
),
|
|
246
|
+
delay=delay,
|
|
247
|
+
queue=wrapper.queue,
|
|
248
|
+
)
|
|
249
|
+
retry_task.execute_on_commit()
|
|
250
|
+
|
|
251
|
+
return
|
|
248
252
|
else:
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
task_logger.error(error, exc_info=True)
|
|
254
|
+
|
|
255
|
+
capture_task_exception()
|
|
256
|
+
record.status = TaskStatus.FAILED
|
|
257
|
+
record.traceback = traceback.format_exc()
|
|
251
258
|
record.end_time = now()
|
|
252
259
|
record.save(update_fields=["status", "traceback", "end_time"])
|
|
253
260
|
|
|
254
|
-
task_logger.
|
|
255
|
-
|
|
256
|
-
)
|
|
261
|
+
task_logger.warning(f"Failed in {record.duration}")
|
|
262
|
+
return
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
if ignored_exception is None:
|
|
265
|
+
record.status = TaskStatus.SUCCEEDED
|
|
266
|
+
record.result = result
|
|
267
|
+
record.end_time = now()
|
|
268
|
+
record.save(update_fields=["status", "result", "end_time"])
|
|
269
|
+
|
|
270
|
+
task_logger.info(f"Succeeded in {record.duration}")
|
|
271
|
+
else:
|
|
272
|
+
record.status = TaskStatus.SUCCEEDED
|
|
273
|
+
record.traceback = ignored_traceback
|
|
274
|
+
record.end_time = now()
|
|
275
|
+
record.save(update_fields=["status", "traceback", "end_time"])
|
|
276
|
+
|
|
277
|
+
task_logger.info(
|
|
278
|
+
f"Succeeded (ignored {type(ignored_exception).__name__}) in {record.duration}"
|
|
279
|
+
)
|
|
260
280
|
|
|
261
281
|
|
|
262
282
|
class SQSLambdaTask(BaseModel):
|