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.
- django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/.config.kiro +1 -0
- django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/design.md +315 -0
- django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/requirements.md +72 -0
- django_lambda_tasks-0.1.5/.kiro/specs/retry-delay/tasks.md +182 -0
- django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/.config.kiro +1 -0
- django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/design.md +198 -0
- django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/requirements.md +64 -0
- django_lambda_tasks-0.1.5/.kiro/specs/singleton-task/tasks.md +104 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/product.md +49 -20
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/structure.md +2 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/PKG-INFO +89 -25
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/README.md +86 -24
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/decorators.py +103 -26
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/handler.py +1 -2
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/models.py +44 -24
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/secret_loader.py +3 -3
- django_lambda_tasks-0.1.5/lambda_tasks/settings.py +42 -0
- django_lambda_tasks-0.1.5/lambda_tasks/tasks.py +23 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/pyproject.toml +3 -1
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_decorator.py +6 -4
- django_lambda_tasks-0.1.5/tests/test_decorators.py +416 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_deferred_enqueue.py +3 -4
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_models.py +657 -65
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_settings.py +20 -0
- django_lambda_tasks-0.1.5/tests/test_tasks.py +101 -0
- django_lambda_tasks-0.1.5/tests/test_timeout_validation.py +170 -0
- django_lambda_tasks-0.1.3/lambda_tasks/settings.py +0 -76
- django_lambda_tasks-0.1.3/tests/test_decorators.py +0 -141
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.gitignore +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/README.md +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/example/manage.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/settings.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.1.3 → django_lambda_tasks-0.1.5}/tests/test_serializer.py +0 -0
- {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"}
|