Retry transient 5xx and network errors with exponential backoff - #4
Merged
Conversation
Upstream genealogy services (Geneteka, GenBaza, Lubgens, Genealogia w Archiwach, GenPod) intermittently return 502/503/504 or briefly drop connections. These were surfacing as tool failures even though a quick retry almost always succeeds. Add a shared `request_with_retry` helper in `sources/_http_retry.py` that wraps an HTTP send callable, retrying on `httpx.TransportError`, `httpx.TimeoutException`, and 502/503/504 responses with full-jitter exponential backoff (3 attempts / 1s base / 30s cap by default). Each of the five live source clients now routes its requests through it, keeping the per-source rate limiter inside the retried callable so pacing is still respected on retries. Tunable via env / CLI / Claude Desktop user_config: GENEALOGY_RETRY_MAX_ATTEMPTS, GENEALOGY_RETRY_BASE_DELAY, GENEALOGY_RETRY_MAX_DELAY. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The retry tuning (attempts / base delay / max delay) doesn't need to be user-facing — keep them as module constants in `_http_retry.py` instead of CLI flags, env vars, and Claude Desktop user_config fields. The helper still accepts overrides as kwargs for tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-call `request_with_retry(_send)` wrapper with `RetryTransport`, an `httpx.BaseTransport` middleware. Each client now sets `transport=RetryTransport(...)` once at construction; every `client.get`/`client.post` automatically retries on transient errors, and the call sites collapse back to plain HTTP calls — no `_send` closures, no retry-aware control flow leaking into the source clients. `RetryTransport` composes with any other transport (including the `MockTransport` instances used in tests), so test wiring stays the same. On 5xx exhaustion the last response is returned (preserving the caller's `raise_for_status` behaviour); on transport-error exhaustion the last exception is re-raised. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Upstream genealogy services (Geneteka, GenBaza, Lubgens, Genealogia w Archiwach, GenPod) intermittently return
502/503/504or briefly drop connections. Today these surface as tool failures even though a short retry almost always recovers. This PR adds a shared transient-error retry layer used by every live HTTP client.What changed
src/polish_genealogy_mcp/sources/_http_retry.py—request_with_retry()wraps an HTTP send callable with full-jitter exponential backoff. Retries on:httpx.TransportError/httpx.TimeoutException502,503,504Defaults: 3 attempts, 1s base delay, 30s cap. Non-retryable responses are returned untouched (the caller still calls
raise_for_status).geneteka(search,get_regions_html)genbaza(_get)lubgens(search)genealogia_w_archiwach(Vaadin bootstrap GET/POST +_post_uidl)genpod(login GET/POST, post-login home GET, GraphQL POST)The per-source rate limiter's
wait()is invoked inside the retried callable so pacing is still respected across retries.user_config:GENEALOGY_RETRY_MAX_ATTEMPTS(default 3)GENEALOGY_RETRY_BASE_DELAY(default 1)GENEALOGY_RETRY_MAX_DELAY(default 30)Wired through
_cli_config.CONFIG_ENTRIESandmanifest.json.Tests
tests/test_http_retry.py: success on first try, retry-then-success on 502 andConnectError, exhaustion on persistent 503 /ReadTimeout, no retry on 404, jitter respectsmax_delay.tests/test_genbaza_client.py: existing 503 case keeps theHTTPStatusErrorassertion (now withGENEALOGY_RETRY_BASE_DELAY=0to keep the test fast); added a502 → 502 → 200end-to-end retry case and a non-retried 404 case.Reviewer notes
sleepso unit tests don't actually sleep; production code usestime.sleep.random.uniform(0, capped_delay)) is used to avoid synchronized retries if multiple requests fail at once.🤖 Generated with Claude Code