|
| 1 | +# PR #619 Review Summary — `greg/stf-322`: bounded transport-failure retry |
| 2 | + |
| 3 | +## Critical Issues (must address before merge) |
| 4 | + |
| 5 | +1. **Worst-case timeout formula is incorrect** *(code-reviewer + comment-analyzer agree)* |
| 6 | + - `WebServiceClient.java:128-132`, `:188-193` |
| 7 | + - Javadoc states `(maxRetries + 1) x (connectTimeout + requestTimeout)`. But `requestTimeout` already covers the entire send (connect + send + receive); the two don't sum. Per-attempt is bounded by `max(connectTimeout, requestTimeout)`, total is `(maxRetries + 1) x requestTimeout` when both set. |
| 8 | + - **Fix:** Reword to `(maxRetries + 1) x requestTimeout`, or describe each attempt as bounded by the configured timeouts independently. |
| 9 | + |
| 10 | +2. **`isRetriableTransportFailure()` retry deny-list is overly broad** *(silent-failure-hunter)* |
| 11 | + - `WebServiceClient.java:401-422` |
| 12 | + - Default-true means `UnknownHostException`, `SSLHandshakeException`, `SSLPeerUnverifiedException`, and `ConnectException` (refused) are all retried — but these are typically *deterministic* failures. Retrying just delays surfacing real config bugs. |
| 13 | + - **Fix:** Either (a) flip to an allow-list (`SocketException`, `EOFException`, generic `IOException`), or (b) add explicit exclusions for `UnknownHostException` and `SSLHandshakeException`. |
| 14 | + |
| 15 | +## Important Issues (should address) |
| 16 | + |
| 17 | +3. **README missing the third exclusion** *(comment-analyzer)* |
| 18 | + - `README.md:118-127` lists two exceptions; the code also short-circuits when `Thread.currentThread().isInterrupted()` is true regardless of exception type. Add a bullet. |
| 19 | + |
| 20 | +4. **No test for retry exhaustion or `maxRetries >= 2`** *(pr-test-analyzer)* |
| 21 | + - The loop's terminating branch (`attempts >= maxRetries → throw`) at `WebServiceClient.java:391-394` is never exercised. A regression flipping `>=` to `>` (infinite loop) would not be caught. |
| 22 | + - **Fix:** Add `.maxRetries(2)` + three faults → `assertThrows` + `wireMock.verify(3, ...)`. |
| 23 | + |
| 24 | +5. **`testInterruptDuringRetry` doesn't verify "no retry"** *(pr-test-analyzer)* |
| 25 | + - `WebServiceClientTest.java:574-593` — add `wireMock.verify(1, postRequestedFor(...))`. Without it, the test would still pass if the interrupt-short-circuit branch were deleted. |
| 26 | + |
| 27 | +6. **Intermediate retry failures are silently dropped** *(silent-failure-hunter)* |
| 28 | + - `WebServiceClient.java:386-397` — when retries fail and the final attempt throws, prior `IOException` instances are discarded. |
| 29 | + - **Fix:** `e.addSuppressed(prior)` on rethrow so the final stack trace carries the full retry history. Project guidance ("do not ignore unexpected errors") supports this. |
| 30 | + |
| 31 | +7. **Pre-existing typo bundled in but not fixed** *(code-reviewer)* |
| 32 | + - `WebServiceClient.java:187`: "Request timeout duration. **here** is no timeout by default." — should be "There". Worth fixing while editing this Javadoc anyway. |
| 33 | + |
| 34 | +8. **Interrupt-flag restoration belongs in a separate commit** *(code-reviewer)* |
| 35 | + - `WebServiceClient.java:355, 375` — adding `Thread.currentThread().interrupt()` to existing `catch (InterruptedException)` blocks is an independent, legitimate bug fix. Per project hygiene, separate it from the retry feature. |
| 36 | + |
| 37 | +## Suggestions (nice to have) |
| 38 | + |
| 39 | +9. **Negative `maxRetries(-1)` validation is untested** — one-line `assertThrows(IllegalArgumentException.class, ...)`. |
| 40 | +10. **Tighten `assertThrows(Exception.class, ...)`** in `testNoRetryOnHttpTimeoutException`, `testMaxRetriesZeroDisablesRetry`, `testInterruptDuringRetry` — assert specific exception types. |
| 41 | +11. **Move retry-budget Javadoc** off `@param val` blocks into the method-level body — currently the same paragraph is duplicated on `connectTimeout()` and `requestTimeout()`. |
| 42 | +12. **Trim CHANGELOG entry** — currently duplicates README; one sentence + link is sufficient. |
| 43 | +13. **`maxRetries` Javadoc lists "connect timeout" as retriable** *(comment-analyzer)* — but it isn't (`HttpConnectTimeoutException extends HttpTimeoutException`). Drop from example list. |
| 44 | +14. **Soften phrasing**: "HTTP 4xx and 5xx responses cannot reach this predicate" → "do not reach" (same meaning, less hostage-to-fortune). |
| 45 | +15. **Confirm intentional default of `maxRetries = 1`** — silently changes upgrade behavior. Documented, but worth a sanity check that the team wants opt-out rather than opt-in. |
| 46 | + |
| 47 | +## Strengths |
| 48 | + |
| 49 | +- Builder API design follows existing conventions (`int val`, `IllegalArgumentException`, fluent return). |
| 50 | +- Thread-safety preserved: `final int`, local `attempts` counter, no shared state. |
| 51 | +- Classification logic catches the `HttpConnectTimeoutException extends HttpTimeoutException` JDK contract correctly with a single `instanceof` check. |
| 52 | +- The `BodyPublishers.ofString()` replayability comment is accurate (verified against JDK source). |
| 53 | +- `wireMock.verify(N, ...)` calls in 5 of 7 tests provide regression-resistant attempt-count assertions. |
| 54 | +- README is unusually clear — names actual JDK error messages users see and points at `jdk.httpclient.keepalive.timeout` as a complementary tuning knob. |
| 55 | +- The block comment in `isRetriableTransportFailure` explaining *why* timeouts are excluded ("customer-set budgets that retrying would silently extend") captures intent code alone can't. |
| 56 | + |
| 57 | +## Recommended action sequence |
| 58 | + |
| 59 | +1. Fix #1, #2 (critical) — they're behavioral correctness issues. |
| 60 | +2. Address #3-#8 — most can land as fixup commits per the project's commit-hygiene rule, *except* #8 itself which should be split out. |
| 61 | +3. Consider suggestions #9-#15 in the same fixup batch. |
| 62 | +4. Re-run `mvn test` and `mvn checkstyle:check`; re-request review on the deny-list-vs-allow-list discussion (#2) since it's a design judgment call rather than a clear-cut bug. |
0 commit comments