django-lambda-tasks 0.1.3__tar.gz → 0.1.5__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 (89) hide show
  1. django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/.config.kiro +1 -0
  2. django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/design.md +315 -0
  3. django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/requirements.md +72 -0
  4. django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/tasks.md +182 -0
  5. django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/.config.kiro +1 -0
  6. django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/design.md +198 -0
  7. django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/requirements.md +64 -0
  8. django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/tasks.md +104 -0
  9. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/product.md +49 -20
  10. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/structure.md +2 -0
  11. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/PKG-INFO +89 -25
  12. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/README.md +86 -24
  13. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/decorators.py +103 -26
  14. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/handler.py +1 -2
  15. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/models.py +44 -24
  16. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/secret_loader.py +3 -3
  17. django_lambda_tasks-0.1.5/lambda_tasks/settings.py +42 -0
  18. django_lambda_tasks-0.1.5/lambda_tasks/tasks.py +23 -0
  19. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/pyproject.toml +3 -1
  20. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_decorator.py +6 -4
  21. django_lambda_tasks-0.1.5/tests/test_decorators.py +416 -0
  22. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_deferred_enqueue.py +3 -4
  23. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_models.py +657 -65
  24. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_settings.py +20 -0
  25. django_lambda_tasks-0.1.5/tests/test_tasks.py +101 -0
  26. django_lambda_tasks-0.1.5/tests/test_timeout_validation.py +170 -0
  27. django_lambda_tasks-0.1.3/lambda_tasks/settings.py +0 -76
  28. django_lambda_tasks-0.1.3/tests/test_decorators.py +0 -141
  29. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.github/workflows/ci.yml +0 -0
  30. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.github/workflows/release.yml +0 -0
  31. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.gitignore +0 -0
  32. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  33. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  34. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  35. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  36. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  37. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  38. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  39. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  40. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  41. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  42. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  43. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  44. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  45. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  46. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  47. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  48. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  49. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/design.md +0 -0
  50. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  51. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  52. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  53. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  54. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  55. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  56. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/.config.kiro +0 -0
  57. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/design.md +0 -0
  58. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/requirements.md +0 -0
  59. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/tasks.md +0 -0
  60. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/tech.md +0 -0
  61. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.pre-commit-config.yaml +0 -0
  62. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.vscode/settings.json +0 -0
  63. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/README.md +0 -0
  64. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/__init__.py +0 -0
  65. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/apps.py +0 -0
  66. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/tasks.py +0 -0
  67. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/urls.py +0 -0
  68. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/views.py +0 -0
  69. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/__init__.py +0 -0
  70. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/settings.py +0 -0
  71. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/urls.py +0 -0
  72. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/wsgi.py +0 -0
  73. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/manage.py +0 -0
  74. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/__init__.py +0 -0
  75. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/admin.py +0 -0
  76. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/apps.py +0 -0
  77. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/logging.py +0 -0
  78. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/0001_initial.py +0 -0
  79. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/__init__.py +0 -0
  80. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/timeouts.py +0 -0
  81. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/conftest.py +0 -0
  82. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/settings.py +0 -0
  83. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_admin.py +0 -0
  84. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_handler.py +0 -0
  85. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_kwargs_only.py +0 -0
  86. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_logging.py +0 -0
  87. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_secret_loader.py +0 -0
  88. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_serializer.py +0 -0
  89. {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_timeouts.py +0 -0
@@ -0,0 +1 @@
1
+ {"specId": "d39d74d4-303d-471c-b297-79795f66ea87", "workflowType": "requirements-first", "specType": "feature"}
@@ -0,0 +1,315 @@
1
+ # Design Document: retry-delay
2
+
3
+ ## Overview
4
+
5
+ This feature adds a `retry_delay` parameter to the `@lambda_task` decorator, giving task authors explicit control over the SQS `DelaySeconds` used when a task is automatically re-enqueued after a retryable failure.
6
+
7
+ As part of this change, the call-time `_delay` override kwarg accepted by `execute_on_commit()` is removed. Delay configuration is centralised on the decorator — there are no per-call overrides.
8
+
9
+ The two delay resolution paths remain entirely separate after this change:
10
+
11
+ - **Normal enqueue** (`execute_on_commit` called directly by application code): uses `self._delay` from the decorator.
12
+ - **Retry enqueue** (triggered by a retryable exception in the executor): uses `min(wrapper.retry_delay + round(random.uniform(1, 5)), 900)` — jitter is always added, capped at 900.
13
+
14
+ Both `delay` and `retry_delay` are validated at decoration time against the SQS maximum `DelaySeconds` of 900 seconds. `retry_delay` is additionally validated to require a non-empty `retry_on` tuple when non-zero.
15
+
16
+ ## Architecture
17
+
18
+ No new modules are introduced. Changes are confined to three files:
19
+
20
+ ```
21
+ lambda_tasks/
22
+ decorators.py — add retry_delay param, validation, property; remove _delay pop in _build_task
23
+ models.py — update retry path to use wrapper.retry_delay; remove _delay kwarg from retry enqueue
24
+ .kiro/steering/
25
+ product.md — update Enqueuing section and retry_on retry delay description
26
+ ```
27
+
28
+ The flow after this change:
29
+
30
+ ```
31
+ execute_on_commit(**kwargs)
32
+ → _build_task(kwargs) # pops only _n_retries; uses self._delay directly
33
+ → SQSLambdaTask(delay=self._delay, ...)
34
+
35
+ execute_immediately() retry path
36
+ → delay = wrapper.retry_delay if wrapper.retry_delay != 0 else round(random.uniform(1, 5))
37
+ → SQSLambdaTask(message=..., delay=delay, queue=wrapper.queue).execute_on_commit()
38
+ ```
39
+
40
+ ## Components and Interfaces
41
+
42
+ ### `LambdaTaskWrapper.__init__` (decorators.py)
43
+
44
+ Add `retry_delay: int = 0` to the parameter list. Call two new validation methods before `functools.update_wrapper`. Store as `self._retry_delay`.
45
+
46
+ ```python
47
+ def __init__(
48
+ self,
49
+ func: Callable[..., Any],
50
+ *,
51
+ delay: int = 0,
52
+ retry_delay: int = 0,
53
+ soft_timeout: int | None = None,
54
+ hard_timeout: int | None = None,
55
+ queue: str = "default",
56
+ ignore_errors: tuple[type[BaseException], ...] = (),
57
+ retry_on: tuple[type[BaseException], ...] = (),
58
+ ) -> None:
59
+ self._validate_func(func=func)
60
+ self._validate_timeouts(soft_timeout=soft_timeout, hard_timeout=hard_timeout)
61
+ self._validate_delay(delay=delay)
62
+ self._validate_retry_delay(retry_delay=retry_delay, retry_on=retry_on)
63
+ self._validate_ignore_errors(ignore_errors=ignore_errors)
64
+ self._validate_retry_on(retry_on=retry_on)
65
+ self._validate_no_overlap(retry_on=retry_on, ignore_errors=ignore_errors)
66
+ ...
67
+ self._retry_delay = retry_delay
68
+ ```
69
+
70
+ ### `LambdaTaskWrapper.retry_delay` property (decorators.py)
71
+
72
+ Expose `_retry_delay` as a read-only property, mirroring the existing `retry_on` and `ignore_errors` properties:
73
+
74
+ ```python
75
+ @property
76
+ def retry_delay(self) -> int:
77
+ """Delay in seconds used when enqueuing a retry. 0 means use jitter."""
78
+ return self._retry_delay
79
+ ```
80
+
81
+ ### `LambdaTaskWrapper._validate_delay` (decorators.py)
82
+
83
+ New static method. Validates `delay` is in `[0, 900]`:
84
+
85
+ ```python
86
+ @staticmethod
87
+ def _validate_delay(*, delay: int) -> None:
88
+ if delay < 0 or delay > 900:
89
+ raise ValueError(
90
+ f"delay ({delay}) must be in the range [0, 900] (SQS maximum DelaySeconds)."
91
+ )
92
+ ```
93
+
94
+ ### `LambdaTaskWrapper._validate_retry_delay` (decorators.py)
95
+
96
+ New static method. Validates `retry_delay` is in `[0, 900]` and, if non-zero, that `retry_on` is non-empty:
97
+
98
+ ```python
99
+ @staticmethod
100
+ def _validate_retry_delay(
101
+ *, retry_delay: int, retry_on: tuple[type[BaseException], ...]
102
+ ) -> None:
103
+ if retry_delay < 0 or retry_delay > 900:
104
+ raise ValueError(
105
+ f"retry_delay ({retry_delay}) must be in the range [0, 900] (SQS maximum DelaySeconds)."
106
+ )
107
+ if retry_delay != 0 and not retry_on:
108
+ raise TypeError(
109
+ "retry_delay is only meaningful when retry_on is non-empty. "
110
+ "Either set retry_on or remove retry_delay."
111
+ )
112
+ ```
113
+
114
+ ### `LambdaTaskWrapper._build_task` (decorators.py)
115
+
116
+ Remove the `_delay` pop. Use `self._delay` directly. Only `_n_retries` is still popped from kwargs:
117
+
118
+ ```python
119
+ def _build_task(self, *, kwargs: dict[str, Any]) -> SQSLambdaTask:
120
+ n_retries = kwargs.pop("_n_retries", 0)
121
+
122
+ self._kwargs_model.model_validate(kwargs)
123
+
124
+ message = SQSLambdaTaskMessage(
125
+ task_name=f"{self._func.__module__}.{self._func.__qualname__}",
126
+ kwargs=dict(kwargs),
127
+ n_retries=n_retries,
128
+ )
129
+
130
+ return SQSLambdaTask(
131
+ message=message,
132
+ delay=self._delay,
133
+ queue=self._queue,
134
+ )
135
+ ```
136
+
137
+ The docstring for `_build_task`, `serialize`, and `execute_on_commit` must also drop all references to `_delay`.
138
+
139
+ ### `lambda_task` decorator factory (decorators.py)
140
+
141
+ Add `retry_delay: int = 0` to both `@overload` signatures and the implementation. Pass it through to `LambdaTaskWrapper`:
142
+
143
+ ```python
144
+ def lambda_task(
145
+ func: Callable[..., Any] | None = None,
146
+ *,
147
+ delay: int = 0,
148
+ retry_delay: int = 0,
149
+ soft_timeout: int | None = None,
150
+ hard_timeout: int | None = None,
151
+ queue: str = "default",
152
+ ignore_errors: tuple[type[BaseException], ...] = (),
153
+ retry_on: tuple[type[BaseException], ...] = (),
154
+ ) -> LambdaTaskWrapper | Callable[[Callable[..., Any]], LambdaTaskWrapper]:
155
+ def _decorate(f: Callable[..., Any]) -> LambdaTaskWrapper:
156
+ return LambdaTaskWrapper(
157
+ f,
158
+ delay=delay,
159
+ retry_delay=retry_delay,
160
+ ...
161
+ )
162
+ ...
163
+ ```
164
+
165
+ ### `SQSLambdaTaskMessage.execute_immediately` retry path (models.py)
166
+
167
+ Replace the `wrapper._delay` reference with `wrapper.retry_delay`. Remove `_delay=delay` from the `execute_on_commit` call — `_build_task` no longer accepts it:
168
+
169
+ ```python
170
+ # Before:
171
+ delay = wrapper._delay if wrapper._delay != 0 else round(random.uniform(1, 5))
172
+ wrapper.execute_on_commit(**self.kwargs, _delay=delay, _n_retries=self.n_retries + 1)
173
+
174
+ # After:
175
+ delay = wrapper.retry_delay if wrapper.retry_delay != 0 else round(random.uniform(1, 5))
176
+ wrapper.execute_on_commit(**self.kwargs, _n_retries=self.n_retries + 1)
177
+ ```
178
+
179
+ The `delay` local variable is still computed and used — it must be passed to `_build_task` via the retry enqueue. Since `_build_task` now uses `self._delay` directly for normal enqueues, the retry path needs a different mechanism to inject the computed delay.
180
+
181
+ **Revised approach:** The retry enqueue cannot use `execute_on_commit` directly if `_delay` is removed, because `execute_on_commit` always uses `self._delay`. Instead, the retry path in `execute_immediately` should build the `SQSLambdaTask` directly and call `execute_on_commit` on it:
182
+
183
+ ```python
184
+ delay = wrapper.retry_delay if wrapper.retry_delay != 0 else round(random.uniform(1, 5))
185
+ retry_task = SQSLambdaTask(
186
+ message=SQSLambdaTaskMessage(
187
+ task_name=self.task_name,
188
+ kwargs=self.kwargs,
189
+ n_retries=self.n_retries + 1,
190
+ ),
191
+ delay=delay,
192
+ queue=wrapper._queue,
193
+ )
194
+ retry_task.execute_on_commit()
195
+ ```
196
+
197
+ This avoids any need for a `_delay` override kwarg and keeps `_build_task` clean. The `wrapper._queue` access uses the private attribute — this is acceptable since `execute_immediately` is part of the same library and `_queue` has no public property. Alternatively, expose a `queue` property on `LambdaTaskWrapper` (preferred for clarity):
198
+
199
+ ```python
200
+ @property
201
+ def queue(self) -> str:
202
+ """The SQS queue name this task is routed to."""
203
+ return self._queue
204
+ ```
205
+
206
+ Then the retry path becomes:
207
+
208
+ ```python
209
+ delay = wrapper.retry_delay if wrapper.retry_delay != 0 else round(random.uniform(1, 5))
210
+ retry_task = SQSLambdaTask(
211
+ message=SQSLambdaTaskMessage(
212
+ task_name=self.task_name,
213
+ kwargs=self.kwargs,
214
+ n_retries=self.n_retries + 1,
215
+ ),
216
+ delay=delay,
217
+ queue=wrapper.queue,
218
+ )
219
+ retry_task.execute_on_commit()
220
+ ```
221
+
222
+ ## Data Models
223
+
224
+ No changes to `SQSLambdaTaskMessage`, `SQSLambdaTask`, or `TaskRecord` schemas. `retry_delay` is a decorator-level configuration value stored on `LambdaTaskWrapper` — it is never serialised into the SQS message.
225
+
226
+ `LambdaTaskWrapper` gains one new stored attribute:
227
+
228
+ | Attribute | Type | Description |
229
+ |---|---|---|
230
+ | `_retry_delay` | `int` | Seconds to delay a retry enqueue. 0 = use jitter. |
231
+
232
+ Exposed via the `retry_delay` property.
233
+
234
+ ## Correctness Properties
235
+
236
+ *A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
237
+
238
+ ### Property 1: retry_delay storage round-trip
239
+
240
+ *For any* integer `retry_delay` in `[0, 900]` (with `retry_on` non-empty when `retry_delay > 0`), constructing a `LambdaTaskWrapper` and reading `wrapper.retry_delay` SHALL return the same value that was passed in.
241
+
242
+ **Validates: Requirements 1.2**
243
+
244
+ ### Property 2: retry_delay requires retry_on
245
+
246
+ *For any* integer `retry_delay` in `[1, 900]`, constructing a `LambdaTaskWrapper` with an empty `retry_on` tuple SHALL raise `TypeError`.
247
+
248
+ **Validates: Requirements 1.4**
249
+
250
+ ### Property 3: Out-of-range delay and retry_delay raise ValueError
251
+
252
+ *For any* integer `delay` outside `[0, 900]`, constructing a `LambdaTaskWrapper` SHALL raise `ValueError`. Likewise, *for any* integer `retry_delay` outside `[0, 900]`, constructing a `LambdaTaskWrapper` SHALL raise `ValueError`.
253
+
254
+ **Validates: Requirements 2.1, 2.2**
255
+
256
+ ### Property 4: Non-zero retry_delay is used in the retry enqueue
257
+
258
+ *For any* `retry_delay` in `[1, 900]` and any retryable exception, the `SQSLambdaTask` enqueued by the retry path SHALL have `delay == retry_delay`.
259
+
260
+ **Validates: Requirements 3.1**
261
+
262
+ ### Property 5: Zero retry_delay produces jitter in [1, 5]
263
+
264
+ *For any* execution where `retry_delay` is `0` and a retryable exception is raised, the `delay` on the enqueued retry `SQSLambdaTask` SHALL be an integer in the inclusive range `[1, 5]`.
265
+
266
+ **Validates: Requirements 3.2, 3.3**
267
+
268
+ ### Property 6: Normal enqueue uses decorator delay
269
+
270
+ *For any* `delay` in `[0, 900]`, calling `execute_on_commit` directly (not via the retry path) SHALL produce a `SQSLambdaTask` with `delay` equal to the decorator-configured value.
271
+
272
+ **Validates: Requirements 4.3, 4.4**
273
+
274
+ ## Error Handling
275
+
276
+ | Condition | Error | Raised at |
277
+ |---|---|---|
278
+ | `delay < 0` or `delay > 900` | `ValueError` | Decoration time |
279
+ | `retry_delay < 0` or `retry_delay > 900` | `ValueError` | Decoration time |
280
+ | `retry_delay != 0` and `retry_on == ()` | `TypeError` | Decoration time |
281
+ | `_delay` passed to `execute_on_commit()` | `TypeError` (from pydantic `extra="forbid"` on the kwargs model, or from `_build_task` rejecting it) | Call time |
282
+
283
+ The `_delay` rejection at call time is a natural consequence of removing the `kwargs.pop("_delay", ...)` line from `_build_task`. If `_delay` is passed, it will reach `self._kwargs_model.model_validate(kwargs)` and be rejected by the `extra="forbid"` Pydantic config with a `ValidationError`. This is acceptable — the public contract says `_delay` is no longer supported, and the error message from Pydantic is clear enough. No special handling is needed.
284
+
285
+ ## Testing Strategy
286
+
287
+ Tests use `pytest` and `hypothesis` (already present in the project).
288
+
289
+ **Unit tests** (`tests/test_decorators.py`):
290
+ - `retry_delay` defaults to `0`
291
+ - `retry_delay` is stored and returned via the property
292
+ - `retry_delay=0` with empty `retry_on` is accepted (default case)
293
+ - `retry_delay > 0` with non-empty `retry_on` is accepted
294
+ - `retry_delay > 0` with empty `retry_on` raises `TypeError`
295
+ - `delay < 0` raises `ValueError`; `delay > 900` raises `ValueError`
296
+ - `retry_delay < 0` raises `ValueError`; `retry_delay > 900` raises `ValueError`
297
+ - `delay=0` and `delay=900` are accepted (boundary values)
298
+ - `_delay` passed to `execute_on_commit` raises (via Pydantic `ValidationError`)
299
+ - `lambda_task` decorator factory passes `retry_delay` through correctly
300
+
301
+ **Unit tests** (`tests/test_models.py`):
302
+ - Retry path with non-zero `retry_delay` enqueues with that exact delay
303
+ - Retry path with `retry_delay=0` enqueues with a delay in `[1, 5]`
304
+ - Normal `execute_on_commit` uses decorator `delay`, not `retry_delay`
305
+
306
+ **Property-based tests** (using `hypothesis`, in `tests/test_decorators.py` and `tests/test_models.py`):
307
+
308
+ Each property test runs a minimum of 100 iterations.
309
+
310
+ - **Feature: retry-delay, Property 1**: `@given(st.integers(min_value=0, max_value=900))` — verify `wrapper.retry_delay == input`
311
+ - **Feature: retry-delay, Property 2**: `@given(st.integers(min_value=1, max_value=900))` — verify `TypeError` raised with empty `retry_on`
312
+ - **Feature: retry-delay, Property 3**: `@given(st.integers().filter(lambda x: x < 0 or x > 900))` — verify `ValueError` for both `delay` and `retry_delay`
313
+ - **Feature: retry-delay, Property 4**: `@given(st.integers(min_value=1, max_value=900))` — mock retry enqueue, verify `task.delay == retry_delay`
314
+ - **Feature: retry-delay, Property 5**: `@given(st.integers(min_value=0, max_value=0))` with repeated sampling — verify jitter delay in `[1, 5]`
315
+ - **Feature: retry-delay, Property 6**: `@given(st.integers(min_value=0, max_value=900))` — mock enqueue, verify `task.delay == decorator delay`
@@ -0,0 +1,72 @@
1
+ # Requirements Document
2
+
3
+ ## Introduction
4
+
5
+ Add a `retry_delay` parameter to the `@lambda_task` decorator that gives callers explicit control over the delay (in seconds) used when a task is automatically re-enqueued after a retryable failure.
6
+
7
+ As part of this feature, the call-time `_delay` override kwarg accepted by `execute_on_commit()` is being removed. The `product.md` steering doc currently documents `execute_on_commit()` as accepting `_delay` as a per-call override — this behaviour is being removed.
8
+
9
+ The two delay resolution paths are entirely separate:
10
+
11
+ - **Normal enqueue** (`execute_on_commit` called directly): decorator `delay` only (no call-time override)
12
+ - **Retry enqueue** (triggered by a retryable exception): `min(retry_delay + round(random.uniform(1, 5)), 900)` — jitter is always added, capped at 900
13
+
14
+ `retry_delay` is only meaningful when `retry_on` is also configured. Setting `retry_delay` without `retry_on` raises `TypeError` at decoration time.
15
+
16
+ Both `delay` and `retry_delay` are validated at decoration time against the SQS maximum `DelaySeconds` of 900 seconds.
17
+
18
+ ## Glossary
19
+
20
+ - **Decorator**: The `@lambda_task` decorator factory defined in `decorators.py`.
21
+ - **LambdaTaskWrapper**: The object produced by applying `@lambda_task` to a function; stores all decorator-level configuration.
22
+ - **Executor**: `SQSLambdaTaskMessage.execute_immediately()` in `models.py`; runs the task and handles retries.
23
+ - **retry_delay**: The new decorator parameter — a non-negative integer (seconds) used as the base SQS `DelaySeconds` when enqueuing a retry. Jitter is always added on top. Not used for normal enqueues.
24
+ - **delay**: The existing decorator parameter — a non-negative integer (seconds) used as the SQS `DelaySeconds` for normal (non-retry) enqueues only.
25
+ - **Jitter**: Random delay of `round(random.uniform(1, 5))` seconds, always added to `retry_delay` when enqueuing a retry. The total is capped at 900.
26
+ - **retry_on**: The existing decorator parameter — a tuple of exception types that trigger automatic retry.
27
+ - **SQS_MAX_DELAY**: The SQS maximum allowed `DelaySeconds` value: 900 seconds.
28
+
29
+ ## Requirements
30
+
31
+ ### Requirement 1: retry_delay Decorator Parameter
32
+
33
+ **User Story:** As a task author, I want to set a dedicated retry delay on my task decorator, so that retries use a predictable delay independent of the normal enqueue delay.
34
+
35
+ #### Acceptance Criteria
36
+
37
+ 1. THE Decorator SHALL accept a `retry_delay` keyword argument of type `int` with a default value of `0`.
38
+ 2. THE LambdaTaskWrapper SHALL store the `retry_delay` value and expose it via a `retry_delay` property.
39
+ 3. WHEN `retry_delay` is set to a non-zero value and `retry_on` is a non-empty tuple, THE Decorator SHALL construct the LambdaTaskWrapper without raising an exception.
40
+ 4. IF `retry_delay` is non-zero and `retry_on` is an empty tuple or not provided, THEN THE Decorator SHALL raise a `TypeError` at decoration time.
41
+
42
+ ### Requirement 2: Validation of delay and retry_delay at Decoration Time
43
+
44
+ **User Story:** As a task author, I want invalid delay values to be caught immediately when I define my task, so that misconfiguration is surfaced before any task is ever enqueued.
45
+
46
+ #### Acceptance Criteria
47
+
48
+ 1. IF `delay` is less than `0` or greater than `900`, THEN THE Decorator SHALL raise a `ValueError` at decoration time.
49
+ 2. IF `retry_delay` is less than `0` or greater than `900`, THEN THE Decorator SHALL raise a `ValueError` at decoration time.
50
+ 3. WHEN `delay` is an integer in the inclusive range `[0, 900]`, THE Decorator SHALL accept it without raising an exception.
51
+ 4. WHEN `retry_delay` is an integer in the inclusive range `[0, 900]`, THE Decorator SHALL accept it without raising an exception.
52
+
53
+ ### Requirement 3: Retry Delay Resolution
54
+
55
+ **User Story:** As a task author, I want retries to use `retry_delay` when set, and fall back to jitter otherwise, so that retry timing is predictable when configured and safe when not.
56
+
57
+ #### Acceptance Criteria
58
+
59
+ 1. WHEN a retryable exception is raised and `retry_delay` is non-zero, THE Executor SHALL enqueue the retry with `DelaySeconds` set to `retry_delay`.
60
+ 2. WHEN a retryable exception is raised and `retry_delay` is zero, THE Executor SHALL enqueue the retry with `DelaySeconds` set to `round(random.uniform(1, 5))`.
61
+ 3. WHEN a retryable exception is raised and `retry_delay` is zero, THE Executor SHALL produce a `DelaySeconds` value in the inclusive integer range `[1, 5]`.
62
+
63
+ ### Requirement 4: Remove Call-Time _delay Override
64
+
65
+ **User Story:** As a library maintainer, I want to remove the `_delay` per-call override from `execute_on_commit()`, so that delay configuration is centralised on the decorator and the public API is simpler.
66
+
67
+ #### Acceptance Criteria
68
+
69
+ 1. THE LambdaTaskWrapper SHALL NOT accept `_delay` as a kwarg in `execute_on_commit()`.
70
+ 2. IF `_delay` is passed to `execute_on_commit()`, THEN THE LambdaTaskWrapper SHALL raise a `TypeError`.
71
+ 3. WHEN `execute_on_commit` is called directly (not as a retry), THE LambdaTaskWrapper SHALL resolve `DelaySeconds` using only the decorator `delay` value.
72
+ 4. THE LambdaTaskWrapper SHALL NOT use `retry_delay` when building a task for a non-retry enqueue.
@@ -0,0 +1,182 @@
1
+ # Implementation Plan: retry-delay
2
+
3
+ ## Overview
4
+
5
+ Add `retry_delay` to `@lambda_task`, remove the `_delay` call-time override from `execute_on_commit`, and update the retry path in `execute_immediately` to use `wrapper.retry_delay` directly via a `SQSLambdaTask` built in-place. Follow red/green TDD: tests are written before each implementation step.
6
+
7
+ ## Tasks
8
+
9
+ - [x] 1. Add `_validate_delay` to `LambdaTaskWrapper`
10
+ - [x] 1.1 Write failing tests for `_validate_delay`
11
+ - In `tests/test_decorators.py`, add unit tests asserting:
12
+ - `delay < 0` raises `ValueError`
13
+ - `delay > 900` raises `ValueError`
14
+ - `delay=0` and `delay=900` are accepted (boundary values)
15
+ - Tests must fail before implementation exists
16
+ - _Requirements: 2.1, 2.3_
17
+ - [x] 1.2 Implement `_validate_delay` static method on `LambdaTaskWrapper`
18
+ - Add `@staticmethod _validate_delay(*, delay: int) -> None` to `LambdaTaskWrapper` in `decorators.py`
19
+ - Raises `ValueError` if `delay < 0` or `delay > 900`
20
+ - Call it from `__init__` (after `_validate_timeouts`)
21
+ - _Requirements: 2.1, 2.3_
22
+
23
+ - [x] 2. Add `retry_delay` parameter and `_validate_retry_delay` to `LambdaTaskWrapper`
24
+ - [x] 2.1 Write failing tests for `retry_delay` init and `_validate_retry_delay`
25
+ - In `tests/test_decorators.py`, add unit tests asserting:
26
+ - `retry_delay` defaults to `0`
27
+ - `retry_delay=0` with empty `retry_on` is accepted
28
+ - `retry_delay > 0` with non-empty `retry_on` is accepted
29
+ - `retry_delay > 0` with empty `retry_on` raises `TypeError`
30
+ - `retry_delay < 0` raises `ValueError`
31
+ - `retry_delay > 900` raises `ValueError`
32
+ - Tests must fail before implementation exists
33
+ - _Requirements: 1.1, 1.3, 1.4, 2.2, 2.4_
34
+ - [x] 2.2 Implement `retry_delay` parameter and `_validate_retry_delay`
35
+ - Add `retry_delay: int = 0` to `LambdaTaskWrapper.__init__` signature
36
+ - Add `@staticmethod _validate_retry_delay(*, retry_delay: int, retry_on: tuple[type[BaseException], ...]) -> None`
37
+ - Raises `ValueError` if `retry_delay < 0` or `retry_delay > 900`
38
+ - Raises `TypeError` if `retry_delay != 0` and `retry_on` is empty
39
+ - Call it from `__init__` (after `_validate_delay`)
40
+ - Store as `self._retry_delay = retry_delay`
41
+ - _Requirements: 1.1, 1.3, 1.4, 2.2, 2.4_
42
+
43
+ - [x] 3. Add `retry_delay` property to `LambdaTaskWrapper`
44
+ - [x] 3.1 Write failing tests for `retry_delay` property
45
+ - In `tests/test_decorators.py`, add unit tests asserting:
46
+ - `wrapper.retry_delay` returns the value passed at construction
47
+ - Default is `0`
48
+ - Tests must fail before implementation exists
49
+ - _Requirements: 1.2_
50
+ - [x] 3.2 Implement `retry_delay` property
51
+ - Add `@property retry_delay(self) -> int` to `LambdaTaskWrapper`, returning `self._retry_delay`
52
+ - Mirror the existing `retry_on` and `ignore_errors` property pattern
53
+ - _Requirements: 1.2_
54
+
55
+ - [x] 4. Add `queue` property to `LambdaTaskWrapper`
56
+ - [x] 4.1 Write failing tests for `queue` property
57
+ - In `tests/test_decorators.py`, add unit tests asserting:
58
+ - `wrapper.queue` returns the queue name passed at construction
59
+ - Default is `"default"`
60
+ - Tests must fail before implementation exists
61
+ - [x] 4.2 Implement `queue` property
62
+ - Add `@property queue(self) -> str` to `LambdaTaskWrapper`, returning `self._queue`
63
+ - _Requirements: 3.1 (needed by retry path in models.py)_
64
+
65
+ - [x] 5. Update `_build_task` to remove `_delay` pop and use `self._delay` directly
66
+ - [x] 5.1 Write failing tests for updated `_build_task` behaviour
67
+ - In `tests/test_decorators.py`, add unit tests asserting:
68
+ - Passing `_delay` as a kwarg to `execute_on_commit` raises (Pydantic `ValidationError` from `extra="forbid"`)
69
+ - Normal `execute_on_commit` without `_delay` still works and uses the decorator `delay`
70
+ - Tests must fail before implementation exists
71
+ - _Requirements: 4.1, 4.2, 4.3_
72
+ - [x] 5.2 Remove `_delay` pop from `_build_task`; use `self._delay` directly
73
+ - In `decorators.py`, remove `delay = kwargs.pop("_delay", self._delay)` from `_build_task`
74
+ - Replace with `delay = self._delay` (no pop)
75
+ - Only `_n_retries` is still popped from kwargs
76
+ - Update `_build_task` docstring to remove all `_delay` references
77
+ - _Requirements: 4.1, 4.2, 4.3, 4.4_
78
+
79
+ - [x] 6. Update `execute_on_commit` and `serialize` docstrings
80
+ - Remove all references to `_delay` from the docstrings of `execute_on_commit` and `serialize` in `decorators.py`
81
+ - No behaviour change — docstring-only update
82
+ - _Requirements: 4.1_
83
+
84
+ - [x] 7. Add `retry_delay` to the `lambda_task` decorator factory
85
+ - [x] 7.1 Write failing tests for `lambda_task` forwarding `retry_delay`
86
+ - In `tests/test_decorators.py`, add unit tests asserting:
87
+ - `@lambda_task(retry_delay=30, retry_on=(ValueError,))` produces `wrapper.retry_delay == 30`
88
+ - `@lambda_task` without `retry_delay` produces `wrapper.retry_delay == 0`
89
+ - Tests must fail before implementation exists
90
+ - _Requirements: 1.1, 1.2_
91
+ - [x] 7.2 Add `retry_delay` to both `@overload` signatures and the `lambda_task` implementation
92
+ - Add `retry_delay: int = 0` to both `@overload` stubs and the concrete `lambda_task` function in `decorators.py`
93
+ - Pass `retry_delay=retry_delay` through to `LambdaTaskWrapper(...)` inside `_decorate`
94
+ - _Requirements: 1.1_
95
+
96
+ - [x] 8. Checkpoint — ensure all decorator tests pass
97
+ - Ensure all tests pass, ask the user if questions arise.
98
+
99
+ - [x] 9. Update `execute_immediately` retry path in `models.py`
100
+ - [x] 9.1 Write failing tests for the updated retry path
101
+ - In `tests/test_models.py`, add unit tests asserting:
102
+ - Retry path with non-zero `retry_delay` enqueues a `SQSLambdaTask` with `delay == retry_delay`
103
+ - Retry path with `retry_delay=0` enqueues a `SQSLambdaTask` with `delay` in `[1, 5]`
104
+ - Normal `execute_on_commit` (non-retry) uses the decorator `delay`, not `retry_delay`
105
+ - Passing `_delay` to `execute_on_commit` raises `ValidationError`
106
+ - Tests must fail before implementation exists
107
+ - _Requirements: 3.1, 3.2, 3.3, 4.1, 4.2_
108
+ - [x] 9.2 Update the retry path in `SQSLambdaTaskMessage.execute_immediately`
109
+ - Replace `wrapper._delay` with `wrapper.retry_delay` for the delay resolution
110
+ - Replace the `wrapper.execute_on_commit(**self.kwargs, _delay=delay, _n_retries=...)` call with a direct `SQSLambdaTask` construction and `execute_on_commit()`:
111
+ ```python
112
+ delay = wrapper.retry_delay if wrapper.retry_delay != 0 else round(random.uniform(1, 5))
113
+ retry_task = SQSLambdaTask(
114
+ message=SQSLambdaTaskMessage(
115
+ task_name=self.task_name,
116
+ kwargs=self.kwargs,
117
+ n_retries=self.n_retries + 1,
118
+ ),
119
+ delay=delay,
120
+ queue=wrapper.queue,
121
+ )
122
+ retry_task.execute_on_commit()
123
+ ```
124
+ - _Requirements: 3.1, 3.2, 3.3_
125
+
126
+ - [x] 10. Update `product.md` steering doc
127
+ - In `.kiro/steering/product.md`:
128
+ - Update the `## Enqueuing` section: remove `_delay` from the list of per-call overrides; state that `execute_on_commit()` uses only the decorator `delay` value
129
+ - Update the `## retry_on` section: replace the "Retry delay" paragraph to describe `retry_delay` (non-zero → use `retry_delay`; zero → jitter `round(random.uniform(1, 5))`)
130
+ - Add `retry_delay` to the `## Task Definition` example snippet
131
+ - _Requirements: 4.1_
132
+
133
+ - [x] 11. Checkpoint — ensure all tests pass
134
+ - Ensure all tests pass, ask the user if questions arise.
135
+
136
+ - [x] 12. Property-based tests (hypothesis) for the 6 correctness properties
137
+ - [x] 12.1 Property 1: `retry_delay` storage round-trip
138
+ - In `tests/test_decorators.py`, add `@given(st.integers(min_value=0, max_value=900))`
139
+ - For `retry_delay=0`: construct with empty `retry_on`; for `retry_delay > 0`: construct with `retry_on=(ValueError,)`
140
+ - Assert `wrapper.retry_delay == input`
141
+ - **Property 1: retry_delay storage round-trip**
142
+ - **Validates: Requirements 1.2**
143
+ - [x] 12.2 Property 2: `retry_delay` requires `retry_on`
144
+ - In `tests/test_decorators.py`, add `@given(st.integers(min_value=1, max_value=900))`
145
+ - Construct `LambdaTaskWrapper` with `retry_delay=value` and empty `retry_on`
146
+ - Assert `TypeError` is raised
147
+ - **Property 2: retry_delay requires retry_on**
148
+ - **Validates: Requirements 1.4**
149
+ - [x] 12.3 Property 3: out-of-range `delay` and `retry_delay` raise `ValueError`
150
+ - In `tests/test_decorators.py`, add `@given(st.integers().filter(lambda x: x < 0 or x > 900))`
151
+ - Test both `delay=value` and `retry_delay=value` (with `retry_on=(ValueError,)` for the latter)
152
+ - Assert `ValueError` is raised in both cases
153
+ - **Property 3: out-of-range delay and retry_delay raise ValueError**
154
+ - **Validates: Requirements 2.1, 2.2**
155
+ - [x] 12.4 Property 4: non-zero `retry_delay` is used in the retry enqueue
156
+ - In `tests/test_models.py`, add `@given(st.integers(min_value=1, max_value=900))`
157
+ - Mock `SQSLambdaTask.execute_on_commit` or capture the constructed task; trigger a retryable exception
158
+ - Assert the enqueued `SQSLambdaTask.delay == retry_delay`
159
+ - **Property 4: non-zero retry_delay is used in the retry enqueue**
160
+ - **Validates: Requirements 3.1**
161
+ - [x] 12.5 Property 5: zero `retry_delay` produces jitter in `[1, 5]`
162
+ - In `tests/test_models.py`, use repeated sampling (e.g. 100 iterations) with `retry_delay=0`
163
+ - Assert every enqueued delay is an integer in `[1, 5]`
164
+ - **Property 5: zero retry_delay produces jitter in [1, 5]**
165
+ - **Validates: Requirements 3.2, 3.3**
166
+ - [x] 12.6 Property 6: normal enqueue uses decorator `delay`
167
+ - In `tests/test_decorators.py` or `tests/test_models.py`, add `@given(st.integers(min_value=0, max_value=900))`
168
+ - Call `execute_on_commit` directly (not via retry path); capture the `SQSLambdaTask`
169
+ - Assert `task.delay == decorator delay`
170
+ - **Property 6: normal enqueue uses decorator delay**
171
+ - **Validates: Requirements 4.3, 4.4**
172
+
173
+ - [x] 13. Final checkpoint — ensure all tests pass
174
+ - Ensure all tests pass, ask the user if questions arise.
175
+
176
+ ## Notes
177
+
178
+ - Tasks marked with `*` are optional and can be skipped for faster MVP
179
+ - TDD order is strict: each `x.1` test task must be completed before its `x.2` implementation task
180
+ - Existing tests for `_delay` call-time override (e.g. `test_property_on_commit_delay_embedded_in_sqs_message` and `test_property_10_non_zero_delay_used_as_retry_delay`) will need to be updated or removed as part of tasks 5 and 9 — they test behaviour that is being removed
181
+ - `retry_delay` is never serialised into the SQS message; it lives only on `LambdaTaskWrapper`
182
+ - Property tests use `min_examples=100` per the design doc
@@ -0,0 +1 @@
1
+ {"specId": "d39d74d4-303d-471c-b297-79795f66ea87", "workflowType": "requirements-first", "specType": "feature"}