django-lambda-tasks 0.4.10__tar.gz → 0.4.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/steering/product.md +16 -33
  2. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/PKG-INFO +21 -47
  3. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/README.md +20 -46
  4. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/decorators.py +13 -53
  5. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/models.py +9 -2
  6. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/pyproject.toml +1 -1
  7. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_decorator.py +7 -98
  8. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_decorators.py +76 -57
  9. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_deferred_enqueue.py +5 -5
  10. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_kwargs_only.py +1 -1
  11. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_models.py +51 -279
  12. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.github/workflows/ci.yml +0 -0
  13. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.github/workflows/release.yml +0 -0
  14. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.gitignore +0 -0
  15. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  16. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/async-local-execution/design.md +0 -0
  17. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/async-local-execution/requirements.md +0 -0
  18. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/async-local-execution/tasks.md +0 -0
  19. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  20. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  21. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  22. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  23. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  24. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  25. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  26. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  27. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  28. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  29. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  30. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  31. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  32. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  33. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  34. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  35. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/retry-delay/.config.kiro +0 -0
  36. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/retry-delay/design.md +0 -0
  37. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/retry-delay/requirements.md +0 -0
  38. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/retry-delay/tasks.md +0 -0
  39. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  40. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks/design.md +0 -0
  41. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  42. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  43. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  44. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  45. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  46. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  47. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/singleton-task/.config.kiro +0 -0
  48. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/singleton-task/design.md +0 -0
  49. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/singleton-task/requirements.md +0 -0
  50. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/singleton-task/tasks.md +0 -0
  51. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  52. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  53. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  54. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  55. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/task-retry/.config.kiro +0 -0
  56. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/task-retry/design.md +0 -0
  57. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/task-retry/requirements.md +0 -0
  58. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/specs/task-retry/tasks.md +0 -0
  59. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/steering/structure.md +0 -0
  60. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.kiro/steering/tech.md +0 -0
  61. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.pre-commit-config.yaml +0 -0
  62. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/.vscode/settings.json +0 -0
  63. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/README.md +0 -0
  64. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_app/__init__.py +0 -0
  65. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_app/apps.py +0 -0
  66. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_app/tasks.py +0 -0
  67. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_app/urls.py +0 -0
  68. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_app/views.py +0 -0
  69. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_project/__init__.py +0 -0
  70. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_project/settings.py +0 -0
  71. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_project/urls.py +0 -0
  72. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/example_project/wsgi.py +0 -0
  73. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/example/manage.py +0 -0
  74. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/__init__.py +0 -0
  75. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/admin.py +0 -0
  76. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/apps.py +0 -0
  77. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/environment_loader.py +0 -0
  78. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/handler.py +0 -0
  79. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/local_executor.py +0 -0
  80. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/logging.py +0 -0
  81. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/migrations/0001_initial.py +0 -0
  82. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  83. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/migrations/__init__.py +0 -0
  84. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/secret_loader.py +0 -0
  85. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/settings.py +0 -0
  86. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/tasks.py +0 -0
  87. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/lambda_tasks/timeouts.py +0 -0
  88. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/conftest.py +0 -0
  89. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/settings.py +0 -0
  90. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_admin.py +0 -0
  91. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_environment_loader.py +0 -0
  92. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_handler.py +0 -0
  93. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_local_executor.py +0 -0
  94. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_logging.py +0 -0
  95. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_memory_limit.py +0 -0
  96. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_secret_loader.py +0 -0
  97. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_serializer.py +0 -0
  98. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_settings.py +0 -0
  99. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_tasks.py +0 -0
  100. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_timeout_validation.py +0 -0
  101. {django_lambda_tasks-0.4.10 → django_lambda_tasks-0.4.11}/tests/test_timeouts.py +0 -0
@@ -24,10 +24,9 @@ Key modules:
24
24
 
25
25
  ```python
26
26
  # Always kwargs-only — positional args raise TypeError at decoration time
27
- @lambda_task(delay=0, retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
28
- ignore_errors=(SomeExpectedException,),
27
+ @lambda_task(retry_delay=30, soft_timeout=60, hard_timeout=120, queue="default",
29
28
  retry_on=(TransientError,),
30
- singleton=True)
29
+ singleton=True, retry_singleton=True)
31
30
  def my_task(*, user_id: int, action: str) -> None:
32
31
  ...
33
32
  ```
@@ -39,34 +38,14 @@ def my_task(*, user_id: int, action: str) -> None:
39
38
 
40
39
  ## Enqueuing
41
40
 
42
- `execute_on_commit()` uses the decorator `delay` value by default. Pass `_delay=<seconds>` at call time to override the delay for that specific enqueue:
41
+ `execute_on_commit()` defaults to delay 0. Pass `_delay=<seconds>` at call time to set the SQS delay for that specific enqueue:
43
42
 
44
43
  ```python
45
- my_task.execute_on_commit(user_id=1, action="x") # uses decorator delay
46
- my_task.execute_on_commit(user_id=1, action="x", _delay=60) # overrides with 60s
44
+ my_task.execute_on_commit(user_id=1, action="x") # delay 0
45
+ my_task.execute_on_commit(user_id=1, action="x", _delay=60) # 60s delay
47
46
  ```
48
47
 
49
- The `_delay` override is validated against the same range `[0, 900]` as the decorator `delay`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
50
-
51
- ## ignore_errors
52
-
53
- Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any of those types (or a subclass), the executor treats it as a non-fatal outcome:
54
-
55
- - `TaskRecord.status` is set to `SUCCESS`
56
- - The exception traceback is saved to `TaskRecord.traceback` for observability
57
- - Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
58
- - The `TaskRecord` update itself is committed outside the atomic block
59
-
60
- Exceptions not listed in `ignore_errors` continue to produce `FAILED` with a rollback. Omitting `ignore_errors` (or passing `()`) preserves the existing behaviour.
61
-
62
- ```python
63
- @lambda_task(ignore_errors=(RecordNotFound,))
64
- def sync_user(*, user_id: int) -> None:
65
- # RecordNotFound → SUCCESS + traceback recorded; anything else → FAILED
66
- ...
67
- ```
68
-
69
- `ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
48
+ The `_delay` override is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
70
49
 
71
50
  ## retry_on
72
51
 
@@ -75,8 +54,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
75
54
  - `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
76
55
  - The retry is a new invocation — the current record is terminal at `RETRYING`
77
56
  - Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
78
- - `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
79
- - `retry_on` and `ignore_errors` must not overlap (exact match or subclass relationship); overlapping raises `TypeError` at decoration time
80
57
 
81
58
  ```python
82
59
  @lambda_task(retry_on=(RateLimitError, ConnectionError))
@@ -97,7 +74,8 @@ Pass `singleton=True` on `@lambda_task` to prevent concurrent execution of the s
97
74
 
98
75
  - Lock key format: `lambda_tasks.singleton_lock.{task_name}`
99
76
  - The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
100
- - If the lock cannot be acquired (`LockError`), the executor treats it as a retryable exception — same code path as `retry_on`. The `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
77
+ - If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (default), the executor treats it as a retryable exception — same code path as `retry_on`. The `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
78
+ - If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
101
79
  - If `n_retries` has reached `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
102
80
  - The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
103
81
 
@@ -106,9 +84,14 @@ Pass `singleton=True` on `@lambda_task` to prevent concurrent execution of the s
106
84
  def sync_inventory(*, warehouse_id: int) -> None:
107
85
  # Only one instance runs at a time; LockError → RETRYING + re-enqueued
108
86
  ...
87
+
88
+ @lambda_task(singleton=True, retry_singleton=False)
89
+ def sync_inventory(*, warehouse_id: int) -> None:
90
+ # Only one instance runs at a time; LockError → SUCCESS + traceback (no retry)
91
+ ...
109
92
  ```
110
93
 
111
- `singleton` is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
94
+ `singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
112
95
 
113
96
  ## SQS Message Schema (`SQSLambdaTaskMessage`)
114
97
 
@@ -130,8 +113,8 @@ class SQSLambdaTaskMessage(BaseModel):
130
113
  4. If `wrapper.singleton` is `True`, acquires a Redis lock via `caches[SINGLETON_CACHE].lock(lock_key)` wrapping the atomic block; if `False`, no lock is acquired
131
114
  5. Runs task inside `transaction.atomic()` + `TimeoutContext`
132
115
  6. On success: updates record to `SUCCESS` with result and `end_time`
133
- 7. On ignored exception (type matches `ignore_errors`): rolls back task-side writes, commits record as `SUCCESS` with traceback and `end_time`
134
- 8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks, `n_retries < MAX_RETRIES`): rolls back task-side writes, enqueues retry via `execute_on_commit` with `n_retries + 1`, commits record as `RETRYING` with traceback and `end_time`
116
+ 7. On `LockError` when `singleton=True` and `retry_singleton=False`: rolls back task-side writes, commits record as `SUCCESS` with traceback and `end_time`
117
+ 8. On retryable exception (type matches `retry_on` or `LockError` for singleton tasks with `retry_singleton=True`, `n_retries < MAX_RETRIES`): rolls back task-side writes, enqueues retry via `execute_on_commit` with `n_retries + 1`, commits record as `RETRYING` with traceback and `end_time`
135
118
  9. On retryable exception with `n_retries >= MAX_RETRIES`: commits record as `FAILED` with traceback, raises `MaxRetriesExceededError`
136
119
  10. On any other exception: rolls back atomic block, updates record to `FAILED` with traceback and `end_time`
137
120
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.4.10
3
+ Version: 0.4.11
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -74,7 +74,7 @@ def register(request):
74
74
  You can override the delay for a specific enqueue by passing `_delay`:
75
75
 
76
76
  ```python
77
- # Delay this particular invocation by 60 seconds instead of the decorator default
77
+ # Delay this particular invocation by 60 seconds
78
78
  send_welcome_email.execute_on_commit(user_id=user.id, template="welcome", _delay=60)
79
79
  ```
80
80
 
@@ -207,10 +207,10 @@ Key characteristics:
207
207
  soft_timeout=60, # seconds — overrides global default for this task
208
208
  hard_timeout=90, # seconds — overrides global default for this task
209
209
  queue="default", # named queue from LAMBDA_TASKS_QUEUES
210
- ignore_errors=(), # exception types treated as non-fatal
211
210
  retry_on=(), # exception types that trigger automatic retry
212
211
  retry_delay=0, # base retry delay in seconds (jitter always added, capped at 900)
213
212
  singleton=False, # prevent concurrent execution via Redis lock
213
+ retry_singleton=True, # retry on LockError for singleton tasks (or treat as success if False)
214
214
  )
215
215
  def my_task(*, arg: str) -> None:
216
216
  ...
@@ -218,32 +218,31 @@ def my_task(*, arg: str) -> None:
218
218
 
219
219
  | Parameter | Type | Default | Description |
220
220
  |---|---|---|---|
221
- | `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
222
221
  | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
223
222
  | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
224
223
  | `queue` | `str` | `"default"` | Named queue to route this task to. |
225
- | `ignore_errors` | `tuple[type[BaseException], ...]` | `()` | Exception types to treat as non-fatal (see [Ignored exceptions](#ignored-exceptions)). |
226
224
  | `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
227
225
  | `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
228
226
  | `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
227
+ | `retry_singleton` | `bool` | `True` | When `True`, `LockError` on a singleton task triggers a retry. When `False`, lock contention is treated as a successful no-op (traceback recorded). |
229
228
 
230
229
  ### Per-call delay override
231
230
 
232
- The `delay` decorator parameter sets the default SQS `DelaySeconds` for all invocations of a task. To override it for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
231
+ By default, `execute_on_commit()` uses a delay of 0 seconds. To set the SQS `DelaySeconds` for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
233
232
 
234
233
  ```python
235
- @lambda_task(delay=0)
234
+ @lambda_task
236
235
  def notify_user(*, user_id: int) -> None:
237
236
  ...
238
237
 
239
- # Uses the decorator default (0 seconds)
238
+ # Default (0 seconds delay)
240
239
  notify_user.execute_on_commit(user_id=1)
241
240
 
242
- # Override: delay this specific invocation by 120 seconds
241
+ # Delay this specific invocation by 120 seconds
243
242
  notify_user.execute_on_commit(user_id=1, _delay=120)
244
243
  ```
245
244
 
246
- `_delay` is validated against the same `[0, 900]` range as the decorator `delay`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
245
+ `_delay` is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
247
246
 
248
247
  ---
249
248
 
@@ -386,35 +385,6 @@ fields @timestamp, @message
386
385
 
387
386
  ---
388
387
 
389
- ## Ignored exceptions
390
-
391
- Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor treats it as a non-fatal outcome:
392
-
393
- - `TaskRecord.status` is set to `SUCCESS`
394
- - The exception traceback is saved to `TaskRecord.traceback` for observability
395
- - Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
396
- - The `TaskRecord` update is committed outside the atomic block
397
-
398
- Exceptions not in `ignore_errors` continue to produce `FAILED` with a rollback. The default (`()`) preserves existing behaviour.
399
-
400
- ```python
401
- from lambda_tasks.decorators import lambda_task
402
-
403
- class RecordNotFound(Exception):
404
- pass
405
-
406
- @lambda_task(ignore_errors=(RecordNotFound,))
407
- def sync_user(*, user_id: int) -> None:
408
- user = fetch_user(user_id) # raises RecordNotFound if already deleted
409
- update_profile(user)
410
- ```
411
-
412
- If `sync_user` raises `RecordNotFound`, the `TaskRecord` will have `status=SUCCESS` and the traceback recorded in `traceback`. Any ORM writes made before the exception are rolled back.
413
-
414
- `ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
415
-
416
- ---
417
-
418
388
  ## Automatic retries
419
389
 
420
390
  Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor re-enqueues the task with an incremented retry counter:
@@ -422,8 +392,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
422
392
  - `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
423
393
  - The retry is a new invocation — the current record is terminal at `RETRYING`
424
394
  - Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES` (default `2880`), at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
425
- - `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
426
- - `retry_on` and `ignore_errors` must not overlap; overlapping raises `TypeError` at decoration time
427
395
 
428
396
  ```python
429
397
  from lambda_tasks.decorators import lambda_task
@@ -450,19 +418,27 @@ from lambda_tasks.decorators import lambda_task
450
418
 
451
419
  @lambda_task(singleton=True)
452
420
  def sync_inventory(*, warehouse_id: int) -> None:
453
- # Only one instance runs at a time
421
+ # Only one instance runs at a time; LockError → RETRYING + re-enqueued
454
422
  ...
455
423
  ```
456
424
 
457
425
  - Lock key format: `lambda_tasks.singleton_lock.{task_name}`
458
426
  - The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
459
- - If the lock cannot be acquired (`LockError`), the task is retried via the existing retry mechanism — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
427
+ - If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (the default), the task is retried — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
428
+ - If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
460
429
  - If `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
461
430
  - The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
462
431
 
463
- `LockError` is retried automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time). You may include `LockError` in `ignore_errors` if you want lock contention to be treated as a non-fatal outcome instead of triggering a retry.
432
+ `LockError` is handled automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time).
433
+
434
+ `singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
464
435
 
465
- `singleton` is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
436
+ ```python
437
+ @lambda_task(singleton=True, retry_singleton=False)
438
+ def sync_inventory(*, warehouse_id: int) -> None:
439
+ # Lock contention → SUCCESS with traceback (no retry)
440
+ ...
441
+ ```
466
442
 
467
443
  ---
468
444
 
@@ -470,8 +446,6 @@ def sync_inventory(*, warehouse_id: int) -> None:
470
446
 
471
447
  Each task runs inside a `django.db.transaction.atomic` block. If the task raises an unhandled exception, all ORM writes made inside the task are rolled back. The `TaskRecord` failure update is always written outside the atomic block so it survives the rollback.
472
448
 
473
- When an exception matches `ignore_errors`, the same rollback applies to task-side writes — the atomic block still exits via exception. Only the `TaskRecord` update (written outside the block) is committed, recording the `SUCCESS` outcome and traceback.
474
-
475
449
  ---
476
450
 
477
451
  ## Error handling
@@ -62,7 +62,7 @@ def register(request):
62
62
  You can override the delay for a specific enqueue by passing `_delay`:
63
63
 
64
64
  ```python
65
- # Delay this particular invocation by 60 seconds instead of the decorator default
65
+ # Delay this particular invocation by 60 seconds
66
66
  send_welcome_email.execute_on_commit(user_id=user.id, template="welcome", _delay=60)
67
67
  ```
68
68
 
@@ -195,10 +195,10 @@ Key characteristics:
195
195
  soft_timeout=60, # seconds — overrides global default for this task
196
196
  hard_timeout=90, # seconds — overrides global default for this task
197
197
  queue="default", # named queue from LAMBDA_TASKS_QUEUES
198
- ignore_errors=(), # exception types treated as non-fatal
199
198
  retry_on=(), # exception types that trigger automatic retry
200
199
  retry_delay=0, # base retry delay in seconds (jitter always added, capped at 900)
201
200
  singleton=False, # prevent concurrent execution via Redis lock
201
+ retry_singleton=True, # retry on LockError for singleton tasks (or treat as success if False)
202
202
  )
203
203
  def my_task(*, arg: str) -> None:
204
204
  ...
@@ -206,32 +206,31 @@ def my_task(*, arg: str) -> None:
206
206
 
207
207
  | Parameter | Type | Default | Description |
208
208
  |---|---|---|---|
209
- | `delay` | `int` | `0` | Seconds to delay the SQS message before it becomes visible to consumers (max 900). |
210
209
  | `soft_timeout` | `int \| None` | `None` (uses global default) | Per-task soft timeout in seconds (max 900). |
211
210
  | `hard_timeout` | `int \| None` | `None` (uses global default) | Per-task hard timeout in seconds (max 900). |
212
211
  | `queue` | `str` | `"default"` | Named queue to route this task to. |
213
- | `ignore_errors` | `tuple[type[BaseException], ...]` | `()` | Exception types to treat as non-fatal (see [Ignored exceptions](#ignored-exceptions)). |
214
212
  | `retry_on` | `tuple[type[BaseException], ...]` | `()` | Exception types that trigger an automatic retry (see [Automatic retries](#automatic-retries)). |
215
213
  | `retry_delay` | `int` | `0` | Base delay in seconds when enqueuing a retry. Jitter (1–5s) is always added; result capped at 900. Requires `retry_on` to be non-empty. |
216
214
  | `singleton` | `bool` | `False` | Prevent concurrent execution via a Redis lock (see [Singleton tasks](#singleton-tasks)). |
215
+ | `retry_singleton` | `bool` | `True` | When `True`, `LockError` on a singleton task triggers a retry. When `False`, lock contention is treated as a successful no-op (traceback recorded). |
217
216
 
218
217
  ### Per-call delay override
219
218
 
220
- The `delay` decorator parameter sets the default SQS `DelaySeconds` for all invocations of a task. To override it for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
219
+ By default, `execute_on_commit()` uses a delay of 0 seconds. To set the SQS `DelaySeconds` for a specific call, pass `_delay` to `execute_on_commit()` or `serialize()`:
221
220
 
222
221
  ```python
223
- @lambda_task(delay=0)
222
+ @lambda_task
224
223
  def notify_user(*, user_id: int) -> None:
225
224
  ...
226
225
 
227
- # Uses the decorator default (0 seconds)
226
+ # Default (0 seconds delay)
228
227
  notify_user.execute_on_commit(user_id=1)
229
228
 
230
- # Override: delay this specific invocation by 120 seconds
229
+ # Delay this specific invocation by 120 seconds
231
230
  notify_user.execute_on_commit(user_id=1, _delay=120)
232
231
  ```
233
232
 
234
- `_delay` is validated against the same `[0, 900]` range as the decorator `delay`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
233
+ `_delay` is validated against the range `[0, 900]`. It only affects the SQS `DelaySeconds` — it has no effect in eager or async-local mode.
235
234
 
236
235
  ---
237
236
 
@@ -374,35 +373,6 @@ fields @timestamp, @message
374
373
 
375
374
  ---
376
375
 
377
- ## Ignored exceptions
378
-
379
- Pass a tuple of exception types to `ignore_errors` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor treats it as a non-fatal outcome:
380
-
381
- - `TaskRecord.status` is set to `SUCCESS`
382
- - The exception traceback is saved to `TaskRecord.traceback` for observability
383
- - Task-side ORM writes inside the `transaction.atomic()` block are still rolled back
384
- - The `TaskRecord` update is committed outside the atomic block
385
-
386
- Exceptions not in `ignore_errors` continue to produce `FAILED` with a rollback. The default (`()`) preserves existing behaviour.
387
-
388
- ```python
389
- from lambda_tasks.decorators import lambda_task
390
-
391
- class RecordNotFound(Exception):
392
- pass
393
-
394
- @lambda_task(ignore_errors=(RecordNotFound,))
395
- def sync_user(*, user_id: int) -> None:
396
- user = fetch_user(user_id) # raises RecordNotFound if already deleted
397
- update_profile(user)
398
- ```
399
-
400
- If `sync_user` raises `RecordNotFound`, the `TaskRecord` will have `status=SUCCESS` and the traceback recorded in `traceback`. Any ORM writes made before the exception are rolled back.
401
-
402
- `ignore_errors` is validated at decoration time — passing a non-exception type raises `TypeError` immediately. It is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
403
-
404
- ---
405
-
406
376
  ## Automatic retries
407
377
 
408
378
  Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task raises an instance of any listed type (or a subclass), the executor re-enqueues the task with an incremented retry counter:
@@ -410,8 +380,6 @@ Pass a tuple of exception types to `retry_on` on `@lambda_task`. If the task rai
410
380
  - `TaskRecord.status` is set to `RETRYING` and the traceback is recorded
411
381
  - The retry is a new invocation — the current record is terminal at `RETRYING`
412
382
  - Retries continue until `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES` (default `2880`), at which point `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
413
- - `ignore_errors` is checked first — a type in both `ignore_errors` and `retry_on` is treated as ignored (SUCCESS), not retried
414
- - `retry_on` and `ignore_errors` must not overlap; overlapping raises `TypeError` at decoration time
415
383
 
416
384
  ```python
417
385
  from lambda_tasks.decorators import lambda_task
@@ -438,19 +406,27 @@ from lambda_tasks.decorators import lambda_task
438
406
 
439
407
  @lambda_task(singleton=True)
440
408
  def sync_inventory(*, warehouse_id: int) -> None:
441
- # Only one instance runs at a time
409
+ # Only one instance runs at a time; LockError → RETRYING + re-enqueued
442
410
  ...
443
411
  ```
444
412
 
445
413
  - Lock key format: `lambda_tasks.singleton_lock.{task_name}`
446
414
  - The lock is acquired with `blocking_timeout=0` (fail immediately if held) and `timeout=hard_timeout` (auto-expire if the worker crashes)
447
- - If the lock cannot be acquired (`LockError`), the task is retried via the existing retry mechanism — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
415
+ - If the lock cannot be acquired (`LockError`) and `retry_singleton=True` (the default), the task is retried — `TaskRecord` is set to `RETRYING`, the traceback is recorded, and the task is re-enqueued with `n_retries + 1`
416
+ - If `retry_singleton=False`, lock contention is treated as a successful no-op — `TaskRecord` is set to `SUCCESS` with the traceback recorded, and no retry is enqueued
448
417
  - If `n_retries` reaches `LAMBDA_TASKS_MAX_RETRIES`, `MaxRetriesExceededError` is raised and the record is saved as `FAILED`
449
418
  - The cache backend used for locks is controlled by `LAMBDA_TASKS_SINGLETON_CACHE` (default `"default"`)
450
419
 
451
- `LockError` is retried automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time). You may include `LockError` in `ignore_errors` if you want lock contention to be treated as a non-fatal outcome instead of triggering a retry.
420
+ `LockError` is handled automatically for singleton tasks — do not include it in `retry_on` (doing so raises `TypeError` at decoration time).
421
+
422
+ `singleton` and `retry_singleton` are stored on `LambdaTaskWrapper` and read by the executor at execution time; they are never serialised into the SQS message.
452
423
 
453
- `singleton` is stored on `LambdaTaskWrapper` and read by the executor at execution time; it is never serialised into the SQS message.
424
+ ```python
425
+ @lambda_task(singleton=True, retry_singleton=False)
426
+ def sync_inventory(*, warehouse_id: int) -> None:
427
+ # Lock contention → SUCCESS with traceback (no retry)
428
+ ...
429
+ ```
454
430
 
455
431
  ---
456
432
 
@@ -458,8 +434,6 @@ def sync_inventory(*, warehouse_id: int) -> None:
458
434
 
459
435
  Each task runs inside a `django.db.transaction.atomic` block. If the task raises an unhandled exception, all ORM writes made inside the task are rolled back. The `TaskRecord` failure update is always written outside the atomic block so it survives the rollback.
460
436
 
461
- When an exception matches `ignore_errors`, the same rollback applies to task-side writes — the atomic block still exits via exception. Only the `TaskRecord` update (written outside the block) is committed, recording the `SUCCESS` outcome and traceback.
462
-
463
437
  ---
464
438
 
465
439
  ## Error handling
@@ -11,7 +11,6 @@ Also provides the lambda_task decorator factory.
11
11
  import functools
12
12
  import inspect
13
13
  import types
14
- import uuid
15
14
  from collections.abc import Callable
16
15
  from typing import Any, overload
17
16
 
@@ -65,35 +64,30 @@ class LambdaTaskWrapper:
65
64
  self,
66
65
  func: types.FunctionType,
67
66
  *,
68
- delay: int = 0,
69
67
  retry_delay: int = 0,
70
68
  soft_timeout: int | None = None,
71
69
  hard_timeout: int | None = None,
72
70
  queue: str = "default",
73
- ignore_errors: tuple[type[BaseException], ...] = (),
74
71
  retry_on: tuple[type[BaseException], ...] = (),
75
72
  singleton: bool = False,
73
+ retry_singleton: bool = True,
76
74
  ) -> None:
77
75
  self._validate_func(func=func)
78
76
  self._validate_timeouts(soft_timeout=soft_timeout, hard_timeout=hard_timeout)
79
- self._validate_delay(delay=delay)
80
77
  self._validate_retry_delay(retry_delay=retry_delay, retry_on=retry_on)
81
- self._validate_ignore_errors(ignore_errors=ignore_errors)
82
78
  self._validate_retry_on(retry_on=retry_on)
83
- self._validate_no_overlap(retry_on=retry_on, ignore_errors=ignore_errors)
84
79
  self._validate_singleton(singleton=singleton, retry_on=retry_on)
85
80
 
86
81
  functools.update_wrapper(self, func)
87
82
 
88
83
  self._func: types.FunctionType = func
89
- self._delay = delay
90
84
  self._retry_delay = retry_delay
91
85
  self._soft_timeout = soft_timeout
92
86
  self._hard_timeout = hard_timeout
93
87
  self._queue = queue
94
- self._ignore_errors = ignore_errors
95
88
  self._retry_on = retry_on
96
89
  self._singleton = singleton
90
+ self._retry_singleton = retry_singleton
97
91
  self._kwargs_model: type[pydantic.BaseModel] = _build_kwargs_model(func)
98
92
 
99
93
  @property
@@ -162,7 +156,7 @@ class LambdaTaskWrapper:
162
156
  ValueError: if ``_delay`` is outside the allowed range [0, 900].
163
157
  """
164
158
  n_retries = kwargs.pop("_n_retries", 0)
165
- delay = kwargs.pop("_delay", self._delay)
159
+ delay = kwargs.pop("_delay", 0)
166
160
 
167
161
  self._validate_delay(delay=delay)
168
162
 
@@ -240,11 +234,6 @@ class LambdaTaskWrapper:
240
234
  """The SQS queue name this task is routed to."""
241
235
  return self._queue
242
236
 
243
- @property
244
- def ignore_errors(self) -> tuple[type[BaseException], ...]:
245
- """Exception types that are treated as non-fatal during task execution."""
246
- return self._ignore_errors
247
-
248
237
  @property
249
238
  def retry_on(self) -> tuple[type[BaseException], ...]:
250
239
  """Exception types that trigger an automatic retry."""
@@ -255,6 +244,11 @@ class LambdaTaskWrapper:
255
244
  """Whether this task enforces single-concurrency via a Redis lock."""
256
245
  return self._singleton
257
246
 
247
+ @property
248
+ def retry_singleton(self) -> bool:
249
+ """Whether LockError on a singleton task triggers an automatic retry."""
250
+ return self._retry_singleton
251
+
258
252
  @staticmethod
259
253
  def _validate_delay(*, delay: int) -> None:
260
254
  """Raise ValueError if delay is outside the allowed range [0, MAX_DELAY]."""
@@ -279,18 +273,6 @@ class LambdaTaskWrapper:
279
273
  f"retry_delay only applies when the task is configured to retry."
280
274
  )
281
275
 
282
- @staticmethod
283
- def _validate_ignore_errors(
284
- *, ignore_errors: tuple[type[BaseException], ...]
285
- ) -> None:
286
- """Raise TypeError if any element is not a subclass of BaseException."""
287
- for item in ignore_errors:
288
- if not (isinstance(item, type) and issubclass(item, BaseException)):
289
- raise TypeError(
290
- f"ignore_errors must contain only exception types (subclasses of "
291
- f"BaseException); got {item!r}."
292
- )
293
-
294
276
  @staticmethod
295
277
  def _validate_retry_on(*, retry_on: tuple[type[BaseException], ...]) -> None:
296
278
  """Raise TypeError if any element is not a subclass of BaseException."""
@@ -301,24 +283,6 @@ class LambdaTaskWrapper:
301
283
  f"BaseException); got {item!r}."
302
284
  )
303
285
 
304
- @staticmethod
305
- def _validate_no_overlap(
306
- *,
307
- retry_on: tuple[type[BaseException], ...],
308
- ignore_errors: tuple[type[BaseException], ...],
309
- ) -> None:
310
- """Raise TypeError if any type in retry_on and ignore_errors overlap via subclass."""
311
- for retry_type in retry_on:
312
- for ignore_type in ignore_errors:
313
- if issubclass(retry_type, ignore_type) or issubclass(
314
- ignore_type, retry_type
315
- ):
316
- raise TypeError(
317
- f"retry_on and ignore_errors must not overlap: "
318
- f"{retry_type.__name__!r} and {ignore_type.__name__!r} conflict "
319
- f"(one is a subclass of the other)."
320
- )
321
-
322
286
  @staticmethod
323
287
  def _validate_timeouts(
324
288
  *, soft_timeout: int | None, hard_timeout: int | None
@@ -368,14 +332,13 @@ class LambdaTaskWrapper:
368
332
  def lambda_task(
369
333
  func: types.FunctionType,
370
334
  *,
371
- delay: int = ...,
372
335
  retry_delay: int = ...,
373
336
  soft_timeout: int | None = ...,
374
337
  hard_timeout: int | None = ...,
375
338
  queue: str = ...,
376
- ignore_errors: tuple[type[BaseException], ...] = ...,
377
339
  retry_on: tuple[type[BaseException], ...] = ...,
378
340
  singleton: bool = ...,
341
+ retry_singleton: bool = ...,
379
342
  ) -> LambdaTaskWrapper: ...
380
343
 
381
344
 
@@ -383,28 +346,26 @@ def lambda_task(
383
346
  def lambda_task(
384
347
  func: None = None,
385
348
  *,
386
- delay: int = ...,
387
349
  retry_delay: int = ...,
388
350
  soft_timeout: int | None = ...,
389
351
  hard_timeout: int | None = ...,
390
352
  queue: str = ...,
391
- ignore_errors: tuple[type[BaseException], ...] = ...,
392
353
  retry_on: tuple[type[BaseException], ...] = ...,
393
354
  singleton: bool = ...,
355
+ retry_singleton: bool = ...,
394
356
  ) -> Callable[[types.FunctionType], LambdaTaskWrapper]: ...
395
357
 
396
358
 
397
359
  def lambda_task(
398
360
  func: types.FunctionType | None = None,
399
361
  *,
400
- delay: int = 0,
401
362
  retry_delay: int = 0,
402
363
  soft_timeout: int | None = None,
403
364
  hard_timeout: int | None = None,
404
365
  queue: str = "default",
405
- ignore_errors: tuple[type[BaseException], ...] = (),
406
366
  retry_on: tuple[type[BaseException], ...] = (),
407
367
  singleton: bool = False,
368
+ retry_singleton: bool = True,
408
369
  ) -> LambdaTaskWrapper | Callable[[types.FunctionType], LambdaTaskWrapper]:
409
370
  """Decorator factory that registers a function as a background task.
410
371
 
@@ -413,21 +374,20 @@ def lambda_task(
413
374
  @lambda_task
414
375
  def my_task(*, x: int): ...
415
376
 
416
- @lambda_task(delay=5, soft_timeout=60, hard_timeout=120)
377
+ @lambda_task(soft_timeout=60, hard_timeout=120)
417
378
  def my_task(*, x: int): ...
418
379
  """
419
380
 
420
381
  def _decorate(f: types.FunctionType) -> LambdaTaskWrapper:
421
382
  wrapper = LambdaTaskWrapper(
422
383
  f,
423
- delay=delay,
424
384
  retry_delay=retry_delay,
425
385
  soft_timeout=soft_timeout,
426
386
  hard_timeout=hard_timeout,
427
387
  queue=queue,
428
- ignore_errors=ignore_errors,
429
388
  retry_on=retry_on,
430
389
  singleton=singleton,
390
+ retry_singleton=retry_singleton,
431
391
  )
432
392
  return wrapper
433
393
 
@@ -139,7 +139,10 @@ class SQSLambdaTaskMessage(BaseModel):
139
139
  blocking_timeout=0,
140
140
  timeout=hard_timeout,
141
141
  )
142
- effective_retry_on = (LockError, *wrapper.retry_on)
142
+ if wrapper.retry_singleton:
143
+ effective_retry_on = (LockError, *wrapper.retry_on)
144
+ else:
145
+ effective_retry_on = wrapper.retry_on
143
146
  else:
144
147
  lock_ctx = contextlib.nullcontext()
145
148
  effective_retry_on = wrapper.retry_on
@@ -152,7 +155,11 @@ class SQSLambdaTaskMessage(BaseModel):
152
155
  ):
153
156
  result = wrapper(**self.kwargs)
154
157
  except Exception as error:
155
- if wrapper.ignore_errors and isinstance(error, wrapper.ignore_errors):
158
+ if (
159
+ wrapper.singleton
160
+ and not wrapper.retry_singleton
161
+ and isinstance(error, LockError)
162
+ ):
156
163
  ignored_exception = error
157
164
  ignored_traceback = traceback.format_exc()
158
165
  elif effective_retry_on and isinstance(error, effective_retry_on):
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.4.10"
10
+ version = "0.4.11"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"