|
18 | 18 | from .request_options import RequestOptions |
19 | 19 | from httpx._types import RequestFiles |
20 | 20 |
|
21 | | -INITIAL_RETRY_DELAY_SECONDS = 0.5 |
22 | | -MAX_RETRY_DELAY_SECONDS = 10 |
23 | | -MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 |
| 21 | +INITIAL_RETRY_DELAY_SECONDS = 1.0 |
| 22 | +MAX_RETRY_DELAY_SECONDS = 60.0 |
| 23 | +JITTER_FACTOR = 0.2 # 20% random jitter |
24 | 24 |
|
25 | 25 |
|
26 | 26 | def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: |
@@ -64,24 +64,58 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float |
64 | 64 | return seconds |
65 | 65 |
|
66 | 66 |
|
| 67 | +def _add_positive_jitter(delay: float) -> float: |
| 68 | + """Add positive jitter (0-20%) to prevent thundering herd.""" |
| 69 | + jitter_multiplier = 1 + random() * JITTER_FACTOR |
| 70 | + return delay * jitter_multiplier |
| 71 | + |
| 72 | + |
| 73 | +def _add_symmetric_jitter(delay: float) -> float: |
| 74 | + """Add symmetric jitter (±10%) for exponential backoff.""" |
| 75 | + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR |
| 76 | + return delay * jitter_multiplier |
| 77 | + |
| 78 | + |
| 79 | +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: |
| 80 | + """ |
| 81 | + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). |
| 82 | + Returns seconds to wait, or None if header is missing/invalid. |
| 83 | + """ |
| 84 | + reset_time_str = response_headers.get("x-ratelimit-reset") |
| 85 | + if reset_time_str is None: |
| 86 | + return None |
| 87 | + |
| 88 | + try: |
| 89 | + reset_time = int(reset_time_str) |
| 90 | + delay = reset_time - time.time() |
| 91 | + if delay > 0: |
| 92 | + return delay |
| 93 | + except (ValueError, TypeError): |
| 94 | + pass |
| 95 | + |
| 96 | + return None |
| 97 | + |
| 98 | + |
67 | 99 | def _retry_timeout(response: httpx.Response, retries: int) -> float: |
68 | 100 | """ |
69 | 101 | Determine the amount of time to wait before retrying a request. |
70 | 102 | This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff |
71 | 103 | with a jitter to determine the number of seconds to wait. |
72 | 104 | """ |
73 | 105 |
|
74 | | - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. |
| 106 | + # 1. Check Retry-After header first |
75 | 107 | retry_after = _parse_retry_after(response.headers) |
76 | | - if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: |
77 | | - return retry_after |
| 108 | + if retry_after is not None and retry_after > 0: |
| 109 | + return min(retry_after, MAX_RETRY_DELAY_SECONDS) |
78 | 110 |
|
79 | | - # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. |
80 | | - retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) |
| 111 | + # 2. Check X-RateLimit-Reset header (with positive jitter) |
| 112 | + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) |
| 113 | + if ratelimit_reset is not None: |
| 114 | + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) |
81 | 115 |
|
82 | | - # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. |
83 | | - timeout = retry_delay * (1 - 0.25 * random()) |
84 | | - return timeout if timeout >= 0 else 0 |
| 116 | + # 3. Fall back to exponential backoff (with symmetric jitter) |
| 117 | + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) |
| 118 | + return _add_symmetric_jitter(backoff) |
85 | 119 |
|
86 | 120 |
|
87 | 121 | def _should_retry(response: httpx.Response) -> bool: |
|
0 commit comments