Skip to content

Add retry logic with exponential backoff for transient failures - #28

Open
szemyd wants to merge 1 commit into
mainfrom
claude/flaky-symbol-fetch-client-ln16f8
Open

Add retry logic with exponential backoff for transient failures#28
szemyd wants to merge 1 commit into
mainfrom
claude/flaky-symbol-fetch-client-ln16f8

Conversation

@szemyd

@szemyd szemyd commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements automatic retry logic with exponential backoff for both the httpx and pyfetch HTTP transports. Transient failures (connection errors, rate limits, and 5xx errors) are now automatically retried, while client errors and successful responses are returned immediately.

Key Changes

  • New retry module (_retry.py): Shared backoff policy implementing exponential backoff with jitter, respecting server-sent Retry-After headers (capped at MAX_RETRY_DELAY)

  • httpx transport (_httpx_transport.py):

    • Added _get_with_retries() helper that retries on httpx.TransportError and retryable status codes (429, 502, 503, 504)
    • Updated fetch_json() to accept max_retries and backoff_base parameters
    • Retries are transparent to callers; final attempt is returned/raised as-is
  • pyfetch transport (_pyfetch_transport.py):

    • Added _pyfetch_get() and _parse_json_response() helpers to separate concerns
    • Added _retry_after() helper to extract Retry-After headers from pyfetch responses
    • Updated fetch_json() with same retry parameters and behavior as httpx transport
    • Catches generic exceptions (pyfetch surfaces errors as untyped JsException)
  • Configuration (config.py):

    • Added RETRYABLE_STATUS_CODES frozenset (429, 502, 503, 504)
    • Added MAX_RETRY_DELAY constant (10.0 seconds) to prevent excessive waits from server hints
  • Test coverage:

    • New test_retry.py with comprehensive httpx transport retry tests
    • Extended test_pyfetch_transport.py with pyfetch-specific retry tests
    • Updated FakeResponse mock to support headers for testing Retry-After behavior

Implementation Details

  • Exponential backoff formula: backoff_base * (2^attempt) + random(0, 1), capped at MAX_RETRY_DELAY
  • Server-sent Retry-After (delta-seconds form only) takes precedence over exponential schedule
  • Final attempt always executes, ensuring real errors surface to callers
  • Tests use mocked asyncio.sleep() to keep suite fast while verifying backoff behavior

https://claude.ai/code/session_01F2CWz3EUMbCukT4anW6edg

`fetch_json` surfaced any non-2xx as an APIError on the first attempt, so a
momentary 503 from the API (e.g. its Atlas symbol source being rate-limited
upstream) failed the whole call — the flaky `get_symbols` CI failure.

Both transports now retry 429/502/503/504 and connection errors with jittered
exponential backoff, honouring a server-sent `Retry-After` capped at 10s so a
synchronous call can never stall for minutes. Non-transient responses (401,
400, 404) are still raised on the first attempt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants