Skip to content

Commit 8691b03

Browse files
dgarrosclaude
andcommitted
ci: fix markdownlint and vale errors in ihs-249 spec docs
markdown-lint: pad table delimiter rows (MD060 compact style), add blank lines around headings/tables/lists (MD022/MD032/MD058), and replace emphasis-as-heading with plain text (MD036) in the generated spec artifacts under dev/specs/ihs-249-sdk-429-retry/. vale: add "backoff" to the spelling exception vocabulary so the generated config.mdx retry docs pass Infrahub.spelling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b7edb68 commit 8691b03

5 files changed

Lines changed: 41 additions & 23 deletions

File tree

.vale/styles/spelling-exceptions.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ artifact_definitions
88
artifact_name
99
async
1010
Authentik
11+
backoff
1112
boolean
1213
check_definitions
1314
class_name

dev/specs/ihs-249-sdk-429-retry/alignment-check.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ GitHub issue: opsmill/infrahub-sdk-python#1124. No secondary URLs to fetch.
1414

1515
## 2. Verdict
1616

17-
**✅ ALIGNED**
17+
Result: ✅ ALIGNED
1818

1919
`spec.md` faithfully carries every PRD requirement, acceptance criterion, and scope boundary.
2020
The only additions are an expansion of an existing requirement and the authorized resolution of
@@ -23,7 +23,7 @@ the PRD's explicit open question — neither is drift under the check's definiti
2323
## 3. Findings
2424

2525
| Severity | Category | PRD reference | Spec reference | Description |
26-
|----------|----------|---------------|----------------|-------------|
26+
| ---------- | ---------- | --------------- | ---------------- | ------------- |
2727
| ✅ none | missing | FR-001…009 | FR-001…009 | All nine functional requirements present, none dropped or softened (attempt cap, jittered+clamped backoff, Retry-After both forms, malformed fallback, RateLimitError with url/attempts/last-Retry-After, all request paths, per-retry logging, async/sync parity, tune+disable). |
2828
| ✅ none | missing | Journeys P1–P3, User Stories 1–9 | US1–US4, Edge Cases | P1/P2/P3 journeys map to US1/US2/US3; PRD user story 8 (tune/disable) surfaced as US4. All acceptance scenarios preserved. |
2929
| ✅ none | missing | SC-001…005 | SC-001…005 | Success criteria carried over with equivalent semantics. |

dev/specs/ihs-249-sdk-429-retry/critiques/critique-20260707-111502.md

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,84 +24,96 @@ document worst-case cumulative wait, add a multipart-body regression test) appli
2424
## Product Lens Findings 🎯
2525

2626
### Problem Validation
27+
2728
| ID | Severity | Finding | Suggestion |
28-
|----|----------|---------|------------|
29+
| ---- | ---------- | --------- | ------------ |
2930
| P1 || Problem is clear and evidenced (background workloads are the traffic most likely rate-limited; callers currently hand-roll retries). No gap. | None. |
3031

3132
### User Value Assessment
33+
3234
| ID | Severity | Finding | Suggestion |
33-
|----|----------|---------|------------|
35+
| ---- | ---------- | --------- | ------------ |
3436
| P2 || Every user story maps to value; MVP is cleanly P1 (transparent retry-and-succeed). | None. |
3537

3638
### Alternative Approaches
39+
3740
| ID | Severity | Finding | Suggestion |
38-
|----|----------|---------|------------|
41+
| ---- | ---------- | --------- | ------------ |
3942
| P3 | 💡 | The plan doesn't record *why* a custom handler beats off-the-shelf options (`tenacity`, httpx transport-level `retries`). httpx transport retries are connection-level only (not status-code aware) and `tenacity` is a new dependency (out of scope). | Add one line to research.md for the record so reviewers don't re-litigate. (Applied.) |
4043

4144
### Edge Cases & UX
45+
4246
| ID | Severity | Finding | Suggestion |
43-
|----|----------|---------|------------|
47+
| ---- | ---------- | --------- | ------------ |
4448
| P4 | 💡 | Worst-case cumulative blocking time (~`max_retries × backoff_max` ≈ 300 s with defaults) is bounded but undocumented; an interactive caller could block ~5 min. | Document the worst-case total wait and note interactive callers can lower `rate_limit_max_retries`/`rate_limit_backoff_max` or disable. (Applied to plan.) |
4549

4650
### Success Measurement
51+
4752
| ID | Severity | Finding | Suggestion |
48-
|----|----------|---------|------------|
53+
| ---- | ---------- | --------- | ------------ |
4954
| P5 || SC-001..006 are measurable and mapped to acceptance scenarios and quickstart validations. | None. |
5055

5156
---
5257

5358
## Engineering Lens Findings 🔬
5459

5560
### Architecture Soundness
61+
5662
| ID | Severity | Finding | Suggestion |
57-
|----|----------|---------|------------|
63+
| ---- | ---------- | --------- | ------------ |
5864
| E1 || Pure handler + thin async/sync drivers is the right shape; single logic contract satisfies FR-008. Multi-site chokepoint already documented (research R1). | None. |
5965

6066
### Failure Mode Analysis
67+
6168
| ID | Severity | Finding | Suggestion |
62-
|----|----------|---------|------------|
69+
| ---- | ---------- | --------- | ------------ |
6370
| E2 | 🎯 | **Multipart retry can re-send a consumed body.** `_request_multipart` receives a `files` dict that may hold open file handles / streams. On the first send httpx reads them to EOF; a retry re-sends the *same* handles, uploading empty or truncated data — a silent data-corruption bug that only manifests under rate-limiting. | The multipart send site MUST rewind (`seek(0)`) or re-materialize the payload before each retry, or the driver must accept a payload *factory* that produces a fresh body per attempt. Add this constraint to plan/data-model and a dedicated task + regression test. (Applied.) |
6471
| E3 | 🤔 | Retrying mutations relies on the PRD assumption that a 429 is always a pre-processing rejection (no partial write). If the server ever emits 429 after partial processing, a retried POST double-writes. | Accept the PRD's explicit assumption (rate-limit 429 = pre-processing) for this scope; recorded as an assumption in spec.md. Revisit only if server semantics change. (Resolved — no change.) |
6572

6673
### Security & Privacy
74+
6775
| ID | Severity | Finding | Suggestion |
68-
|----|----------|---------|------------|
76+
| ---- | ---------- | --------- | ------------ |
6977
| E4 | 💡 | Retry logging must not leak secrets. The login/refresh paths carry `Authorization: Bearer …` headers and username/password payloads; logging is spec'd to include only URL/attempt/delay, but this should be stated as an explicit constraint so it isn't broadened later. | State in research R8 that retry logs MUST include only URL, attempt number, and delay — never headers or payload. (Applied.) |
7078

7179
### Performance & Scalability
80+
7281
| ID | Severity | Finding | Suggestion |
73-
|----|----------|---------|------------|
82+
| ---- | ---------- | --------- | ------------ |
7483
| E5 || Full jitter de-correlates concurrent clients (thundering-herd mitigation). No hot paths introduced. | None. |
7584

7685
### Testing Strategy
86+
7787
| ID | Severity | Finding | Suggestion |
78-
|----|----------|---------|------------|
88+
| ---- | ---------- | --------- | ------------ |
7989
| E6 | 💡 | Test matrix is comprehensive but has no guard for E2 (multipart body re-read). | Add a client-level test: a multipart upload that gets 429→200 must re-send the *full* body on the retry (assert bytes received on attempt 2 equal attempt 1). (Applied to quickstart + will be a task.) |
8090

8191
### Operational Readiness
92+
8293
| ID | Severity | Finding | Suggestion |
83-
|----|----------|---------|------------|
94+
| ---- | ---------- | --------- | ------------ |
8495
| E7 || WARNING-level per-retry logging via the existing module logger is appropriate for a library; host apps control handlers. | None. |
8596

8697
### Dependencies & Integration
98+
8799
| ID | Severity | Finding | Suggestion |
88-
|----|----------|---------|------------|
100+
| ---- | ---------- | --------- | ------------ |
89101
| E8 || No new dependencies (httpx + stdlib). `retry_on_failure` left untouched (out of scope). Additive Config + one exception; behavioural break documented for changelog. | None. |
90102

91103
---
92104

93105
## Cross-Lens Insights 🔗
94106

95107
| ID | Finding | Product Impact | Engineering Impact | Suggestion |
96-
|----|---------|---------------|-------------------|------------|
108+
| ---- | --------- | --------------- | ------------------- | ------------ |
97109
| X1 | Multipart body re-read (E2) | A "successful" upload that silently uploaded nothing is worse than a visible failure — directly harms the P1 "transparent success" promise. | Silent data corruption under load; hard to diagnose. | Make re-readable payload a hard requirement before implementation. (Applied.) |
98110

99111
---
100112

101113
## Findings Summary
102114

103115
| Metric | Count |
104-
|--------|-------|
116+
| -------- | ------- |
105117
| 🎯 Must-Address | 1 |
106118
| 💡 Recommendations | 4 |
107119
| 🤔 Questions | 1 (resolved inline) |
@@ -114,7 +126,7 @@ document worst-case cumulative wait, add a multipart-body regression test) appli
114126
## Consolidated Findings Table
115127

116128
| ID | Lens | Severity | Category | Finding | Suggestion |
117-
|----|------|----------|----------|---------|------------|
129+
| ---- | ------ | ---------- | ---------- | --------- | ------------ |
118130
| E2/X1 | Both | 🎯 | Failure Modes × UX | Multipart retry re-sends consumed body | Require rewind/re-materialize payload per attempt + regression test |
119131
| P3 | Product | 💡 | Alternatives | No record of why custom vs tenacity/httpx retries | One line in research.md |
120132
| P4 | Product | 💡 | Edge/UX | Worst-case cumulative wait undocumented | Document ~max_retries×backoff_max; tuning guidance |
@@ -127,22 +139,26 @@ document worst-case cumulative wait, add a multipart-body regression test) appli
127139
## Recommended Actions
128140

129141
### 🎯 Must-Address (Before Proceeding)
142+
130143
1. **E2/X1**: Add to `plan.md` (Key design decisions) and `data-model.md` (retry driver) the
131144
requirement that the multipart send site rewinds or re-materializes its payload before each
132145
retry attempt; ensure `tasks.md` includes a task and a regression test for it.
133146

134147
### 💡 Recommendations (Strongly Suggested)
148+
135149
1. **P3**: Record the build-vs-buy rationale in `research.md`.
136150
2. **P4**: Document the worst-case cumulative wait and tuning guidance in `plan.md`.
137151
3. **E4**: State the log-content constraint (no headers/payload) in `research.md` R8.
138152
4. **E6**: Add the multipart full-body-on-retry validation to `quickstart.md`.
139153

140154
### 🤔 Questions (Need Stakeholder Input)
155+
141156
1. **E3**: Confirmed resolved by accepting the PRD's explicit "429 is pre-processing" assumption; no blocker.
142157

143158
---
144159

145160
**Severity Legend**:
161+
146162
- 🎯 **Must-Address**: Blocks proceeding to implementation
147163
- 💡 **Recommendation**: Strongly suggested improvement
148164
- 🤔 **Question**: Needs stakeholder input to resolve

dev/specs/ihs-249-sdk-429-retry/data-model.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ fields, the pure decision helper, and the exception.
66
## Config fields (added to `ConfigBase`)
77

88
| Field | Type | Default | Meaning | Validation |
9-
|-------|------|---------|---------|------------|
9+
| ------- | ------ | --------- | --------- | ------------ |
1010
| `rate_limit_retry_enabled` | `bool` | `True` | Master on/off switch for 429 retry (FR-009). ||
1111
| `rate_limit_max_retries` | `int` | `5` | Max number of *retries* after the initial attempt; total sends = value + 1 (FR-001, SC-004). | `>= 0` |
1212
| `rate_limit_backoff_base` | `float` | `0.5` | Base interval (seconds) for exponential backoff (FR-002). | `> 0` |
1313
| `rate_limit_backoff_max` | `float` | `60.0` | Ceiling (seconds) for any single wait, incl. `Retry-After` (FR-002, FR-003). | `> 0` |
1414

1515
Notes:
16+
1617
- Fields live on `ConfigBase` so `Config` and any subclass inherit them.
1718
- `rate_limit_max_retries = 0` with retry enabled means: one attempt, and a 429 immediately
1819
raises `RateLimitError` (0 retries) — distinct from disabled, which raises the raw error.
@@ -29,7 +30,7 @@ Owns all decision logic; performs no sleeping and no network I/O.
2930
**Behaviour**:
3031

3132
| Method | Signature (conceptual) | Returns | Rules |
32-
|--------|------------------------|---------|-------|
33+
| -------- | ------------------------ | --------- | ------- |
3334
| `parse_retry_after` | `(header: str \| None, *, now=…) -> float \| None` | seconds, or `None` | delta-seconds → `int`; HTTP-date → `(date-now).total_seconds()` floored at 0; malformed/absent → `None` (FR-003, FR-004, past-date edge case). |
3435
| `compute_backoff` | `(attempt: int) -> float` | ceiling seconds | `min(backoff_max, backoff_base * 2**attempt)` — the deterministic exponential ceiling (used for assertions in SC-003). |
3536
| `jittered_delay` | `(ceiling: float) -> float` | seconds | `random.uniform(0, ceiling)` — full jitter (FR-002, SC-003). |
@@ -43,7 +44,7 @@ Owns all decision logic; performs no sleeping and no network I/O.
4344
Subclass of the existing base `Error`.
4445

4546
| Attribute | Type | Meaning |
46-
|-----------|------|---------|
47+
| ----------- | ------ | --------- |
4748
| `url` | `str` | The request URL that kept getting rate-limited (FR-005). |
4849
| `attempts` | `int` | Total attempts made before giving up (= `max_retries + 1`). |
4950
| `retry_after` | `float \| None` | The last `Retry-After` value observed (parsed seconds), or `None`. |

dev/specs/ihs-249-sdk-429-retry/opsmill-implement-report.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
## 2. Chunk-by-chunk ledger
1515

1616
| # | Chunk (phase) | Tasks || ⚠️ || Commit | Notes flagged upward |
17-
|---|---------------|-------|----|----|----|--------|----------------------|
17+
| --- | --------------- | ------- | ---- | ---- | ---- | -------- | ---------------------- |
1818
| 1 | Phase 1+2 Setup + Foundational | T001–T010 (10) | 10 | 0 | 0 | `c71238c` | Streaming retry uses `ExitStack`/`AsyncExitStack`; failed 429 stream read+closed, successful stream left open until caller done. Logs only URL/attempt/delay (no secrets). No client tests here (deferred to later chunks). |
1919
| 2 | Phase 3 US1 | T011, T012 (2) | 2 | 0 | 0 | `ed0457c` | T012 needed no `client.py` change — the `_request` retry + non-429 passthrough already correct. |
2020
| 3 | Phase 4 US2 | T013 (1) | 1 | 0 | 0 | `32abfd5` | HTTP-date case asserted with an inclusive time window (driver parses against `datetime.now`); fixed-form cases assert exactly. |
@@ -33,7 +33,7 @@ None. All T001–T021 completed and ticked.
3333
Runner: `uv run pytest`. Environment for every row: **Python 3.12.13, pytest 9.0.3, pytest-httpx 0.36.0, asyncio mode=AUTO, project `.venv` (`uv sync --all-groups --all-extras`); no external infrastructure required** (all tests run locally; no E2E deferred). Rows are grouped by test function; the bracketed count is the number of parametrized variants, all PASSED.
3434

3535
| Test id | Type | Run command | Passed at (ISO 8601) | Env context | Verbatim pass line |
36-
|---------|------|-------------|----------------------|-------------|--------------------|
36+
| --------- | ------ | ------------- | ---------------------- | ------------- | -------------------- |
3737
| `test_rate_limit.py` handler suite (17 tests: compute_backoff growth/clamp, jittered_delay bounds/variance, parse_retry_after delta/http-date/past-zero/malformed×5, next_delay honour/fallback/clamp, should_retry budget×2) | unit (pure) | `uv run pytest tests/unit/test_rate_limit.py tests/unit/sdk/test_rate_limit_retry.py -v` | 2026-07-07T12:20:50Z | as above | `17 passed in 0.02s` |
3838
| `test_request_retries_429_then_succeeds` [standard, sync] | unit (client) | `uv run pytest tests/unit/sdk/test_rate_limit_retry.py -v` | 2026-07-07T12:24:28Z | as above | `6 passed in 0.02s` |
3939
| `test_request_passes_non_429_through_untouched` [standard-200, standard-500, sync-200, sync-500] | unit (client) | `uv run pytest tests/unit/sdk/test_rate_limit_retry.py -v` | 2026-07-07T12:24:28Z | as above | `6 passed in 0.02s` |
@@ -58,7 +58,7 @@ Runner: `uv run pytest`. Environment for every row: **Python 3.12.13, pytest 9.0
5858
Reviewed the full diff `a55cbaa..HEAD` with three independent agents (code correctness + async/sync parity + type design; error handling / silent failures; test coverage quality).
5959

6060
| Severity | File / test | Summary | Disposition |
61-
|----------|-------------|---------|-------------|
61+
| ---------- | ------------- | --------- | ------------- |
6262
| HIGH | `rate_limit.py` `parse_retry_after` | Negative `Retry-After` (e.g. `-5`) not floored → sync `time.sleep(-5.0)` raises `ValueError` (crash) + async/sync divergence; negative leaks into `RateLimitError.retry_after`. | **Fixed inline** (`8917fb9`): floor delta-seconds at `0.0`. |
6363
| MEDIUM/HIGH | `rate_limit.py` `parse_retry_after` | Pathological huge digit string raises uncaught `OverflowError` (crash); violates FR-004 fall-back. | **Fixed inline** (`8917fb9`): `except OverflowError: return None`, kept `except ValueError: pass` so HTTP-date fall-through still works. |
6464
| HIGH (test) | `test_rate_limit_retry.py` | SC-003/FR-002 exponential-growth + jitter-divergence unguarded at driver level — a bug pinning `attempt=0` would pass the suite. | **Fixed inline** (`8917fb9`): added `test_backoff_grows_exponentially_and_clamps` (identity-patched jitter → deterministic growth) + `test_jitter_differs_between_instances`, async+sync. |

0 commit comments

Comments
 (0)