Skip to content

feat(client): exponential backoff retry for transient 5xx errors#167

Open
bennytimz wants to merge 1 commit into
tempoxyz:mainfrom
bennytimz:feat/client-retry-backoff-5xx
Open

feat(client): exponential backoff retry for transient 5xx errors#167
bennytimz wants to merge 1 commit into
tempoxyz:mainfrom
bennytimz:feat/client-retry-backoff-5xx

Conversation

@bennytimz

@bennytimz bennytimz commented Jul 3, 2026

Copy link
Copy Markdown

Summary

Adds configurable exponential-backoff retry for transient 5xx errors to PaymentTransport and Client. This builds on the 402 retry work introduced in PR #156 and PR #166.

  • New RetryPolicy dataclass — configurable max_attempts, backoff_delays, retryable_status_codes, and retry_on_connect_error
  • PaymentTransport — accepts retry_policy: RetryPolicy | None = None; a private _dispatch() method runs the retry loop before the 402 payment flow, so transient server errors are resolved before any payment credential is created
  • Clientretry_policy defaults to RetryPolicy() (on by default with sensible defaults); pass retry_policy=None for raw no-retry behaviour
  • RetryPolicy exported from both mpp and mpp.client for convenient imports

Design notes

Retry is scoped to the initial dispatch only. The 402 paid-retry request is not subject to 5xx retry — it is a distinct operation that has already incurred payment.

Async-generator bodies (guarded by PR #166) raise PaymentError before the dispatch loop, so the retry path never encounters a half-consumed stream.

RetryPolicy is on by default in Client. Users who want zero-overhead raw behaviour pass retry_policy=None.

# Default: retry on 500/502/503/504 up to 3 times with 0.5s, 1.0s backoff
async with Client(methods=[tempo(...)]) as client:
    response = await client.get(url)

# Custom policy
async with Client(methods=[tempo(...)], retry_policy=RetryPolicy(max_attempts=5)) as client:
    ...

# Disable retry
async with Client(methods=[tempo(...)], retry_policy=None) as client:
    ...

Test plan

  • test_5xx_retried_with_backoff — 503×2 then 200; asserts asyncio.sleep called with 0.5 and 1.0
  • test_5xx_exhausted_returns_last_responsemax_attempts=2, always 503; asserts 503 returned after 2 calls
  • test_connect_error_retriedConnectError then 200; asserts final response is 200
  • test_retry_disabled_when_policy_is_nonePaymentTransport(retry_policy=None), 503 returned immediately
  • test_non_retryable_5xx_not_retried — 501 not in retryable_status_codes; single attempt
  • test_client_retry_disabled_when_policy_is_noneClient(retry_policy=None) via HTTPXMock
  • All 29 tests in tests/test_client.py pass (24 existing + 5 new retry tests + 1 existing)

Adds RetryPolicy and wires it into PaymentTransport and Client.

RetryPolicy (src/mpp/client/retry.py):
  - max_attempts=3 with backoff_delays=[0.5, 1.0] seconds by default
  - Retries on status codes in retryable_status_codes (500/502/503/504)
  - Optionally retries on httpx.ConnectError / httpx.RemoteProtocolError
  - Exported from mpp and mpp.client for easy import

PaymentTransport:
  - New retry_policy: RetryPolicy | None = None parameter
  - _dispatch() helper runs the retry loop before 402 handling so that
    transient 5xx errors are resolved before the payment flow begins
  - asyncio.sleep provides the configurable backoff between attempts
  - Each retry attempt logs a warning with attempt number and URL
  - Async-generator bodies (guarded by PR tempoxyz#166) propagate before the
    retry loop, so no unsafe body replay can occur

Client:
  - retry_policy defaults to RetryPolicy() (retry on by default)
  - Pass retry_policy=None to get raw no-retry behaviour

Tests (tests/test_client.py):
  - test_5xx_retried_with_backoff: 503x2 then 200; verifies sleep(0.5) and sleep(1.0)
  - test_5xx_exhausted_returns_last_response: max_attempts=2, always 503
  - test_connect_error_retried: ConnectError then 200
  - test_retry_disabled_when_policy_is_none: PaymentTransport with retry_policy=None
  - test_non_retryable_5xx_not_retried: 501 passes through in one attempt
  - test_client_retry_disabled_when_policy_is_none: Client(retry_policy=None) via HTTPXMock
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.

1 participant