|
18 | 18 | import asyncio |
19 | 19 | from collections.abc import Sequence |
20 | 20 | import datetime |
| 21 | +import json |
21 | 22 | from unittest import mock |
22 | 23 | import pytest |
23 | 24 | try: |
@@ -61,11 +62,14 @@ def _final_codes(retried_codes: Sequence[int] = _RETRIED_CODES): |
61 | 62 | return [code for code in range(100, 600) if code not in retried_codes] |
62 | 63 |
|
63 | 64 |
|
64 | | -def _httpx_response(code: int): |
| 65 | +def _httpx_response(code: int, response_json=None): |
| 66 | + content = b'' |
| 67 | + if response_json is not None: |
| 68 | + content = json.dumps(response_json).encode('utf-8') |
65 | 69 | return httpx.Response( |
66 | 70 | status_code=code, |
67 | 71 | headers={'status-code': str(code)}, |
68 | | - content=b'', |
| 72 | + content=content, |
69 | 73 | ) |
70 | 74 |
|
71 | 75 |
|
@@ -144,6 +148,99 @@ def fn(): |
144 | 148 | assert timestamps[4] - timestamps[3] >= datetime.timedelta(seconds=8) |
145 | 149 |
|
146 | 150 |
|
| 151 | +_RETRY_OPTIONS_NO_JITTER = types.HttpRetryOptions( |
| 152 | + attempts=2, |
| 153 | + initial_delay=0.25, |
| 154 | + max_delay=10, |
| 155 | + exp_base=2, |
| 156 | + jitter=0, |
| 157 | +) |
| 158 | + |
| 159 | + |
| 160 | +def _resource_exhausted_error_payload( |
| 161 | + retry_delay: str, |
| 162 | + *, |
| 163 | + status: str = 'RESOURCE_EXHAUSTED', |
| 164 | + wrapped: bool = True, |
| 165 | +): |
| 166 | + details = { |
| 167 | + 'code': 429, |
| 168 | + 'message': 'Resource exhausted.', |
| 169 | + 'status': status, |
| 170 | + 'details': [ |
| 171 | + { |
| 172 | + '@type': 'type.googleapis.com/google.rpc.RetryInfo', |
| 173 | + 'retryDelay': retry_delay, |
| 174 | + } |
| 175 | + ], |
| 176 | + } |
| 177 | + if wrapped: |
| 178 | + return {'error': details} |
| 179 | + return details |
| 180 | + |
| 181 | + |
| 182 | +def _retry_and_capture_sleep(status_code: int, error_payload: dict[str, object]): |
| 183 | + def fn(): |
| 184 | + errors.APIError.raise_for_response(_httpx_response(status_code, error_payload)) |
| 185 | + |
| 186 | + retrying = tenacity.Retrying( |
| 187 | + **api_client.retry_args(_RETRY_OPTIONS_NO_JITTER) |
| 188 | + ) |
| 189 | + with mock.patch('tenacity.wait.random.uniform', return_value=0.0): |
| 190 | + with mock.patch('tenacity.nap.time.sleep') as mock_sleep: |
| 191 | + with pytest.raises(errors.APIError): |
| 192 | + retrying(fn) |
| 193 | + assert mock_sleep.call_count == 1 |
| 194 | + return mock_sleep.call_args.args[0] |
| 195 | + |
| 196 | + |
| 197 | +def test_retry_wait_uses_retry_info_for_429_resource_exhausted(): |
| 198 | + retry_delay_seconds = _retry_and_capture_sleep( |
| 199 | + 429, |
| 200 | + _resource_exhausted_error_payload('21.943984799s'), |
| 201 | + ) |
| 202 | + assert retry_delay_seconds == pytest.approx(22.943984799) |
| 203 | + |
| 204 | + |
| 205 | +def test_retry_wait_ignores_retry_info_when_status_not_resource_exhausted(): |
| 206 | + retry_delay_seconds = _retry_and_capture_sleep( |
| 207 | + 429, |
| 208 | + _resource_exhausted_error_payload( |
| 209 | + '9s', status='UNAVAILABLE' |
| 210 | + ), |
| 211 | + ) |
| 212 | + assert retry_delay_seconds == 0.25 |
| 213 | + |
| 214 | + |
| 215 | +def test_retry_wait_ignores_retry_info_when_code_not_429(): |
| 216 | + retry_delay_seconds = _retry_and_capture_sleep( |
| 217 | + 500, |
| 218 | + _resource_exhausted_error_payload('9s'), |
| 219 | + ) |
| 220 | + assert retry_delay_seconds == 0.25 |
| 221 | + |
| 222 | + |
| 223 | +def test_retry_wait_falls_back_on_malformed_retry_delay(): |
| 224 | + retry_delay_seconds = _retry_and_capture_sleep( |
| 225 | + 429, |
| 226 | + _resource_exhausted_error_payload('invalid-delay'), |
| 227 | + ) |
| 228 | + assert retry_delay_seconds == 0.25 |
| 229 | + |
| 230 | + |
| 231 | +def test_retry_wait_supports_error_details_with_or_without_error_wrapper(): |
| 232 | + wrapped_retry_delay = _retry_and_capture_sleep( |
| 233 | + 429, |
| 234 | + _resource_exhausted_error_payload('3.5s', wrapped=True), |
| 235 | + ) |
| 236 | + unwrapped_retry_delay = _retry_and_capture_sleep( |
| 237 | + 429, |
| 238 | + _resource_exhausted_error_payload('3.5s', wrapped=False), |
| 239 | + ) |
| 240 | + assert wrapped_retry_delay == pytest.approx(4.5) |
| 241 | + assert unwrapped_retry_delay == pytest.approx(4.5) |
| 242 | + |
| 243 | + |
147 | 244 | def test_retry_args_enabled_with_custom_values_are_not_overridden(): |
148 | 245 | options = types.HttpRetryOptions( |
149 | 246 | attempts=10, |
|
0 commit comments