plan(SP22): Task 4 — distinguish 4xx (terminal) from 5xx (retry) + drop dead BACKOFF_S[2]
Update _get_typed in the plan to: - raise immediately on 4xx (terminal) - retry on 5xx and RequestError (transient) - use BACKOFF_S = (1.0, 2.0) instead of (1.0, 2.0, 4.0) - use RuntimeError instead of assert for the context-manager guard Also add test_4xx_is_terminal_not_retried to the plan's test list.
This commit is contained in:
@@ -747,7 +747,7 @@ T = TypeVar("T", bound=BaseModel)
|
|||||||
|
|
||||||
DEFAULT_TIMEOUT = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=5.0)
|
DEFAULT_TIMEOUT = httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=5.0)
|
||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
BACKOFF_S = (1.0, 2.0, 4.0)
|
BACKOFF_S = (1.0, 2.0) # only used between attempts 0→1 and 1→2
|
||||||
|
|
||||||
|
|
||||||
# --- Response models ---------------------------------------------------------
|
# --- Response models ---------------------------------------------------------
|
||||||
@@ -820,24 +820,31 @@ class CycloneClient:
|
|||||||
async def _get_typed(
|
async def _get_typed(
|
||||||
self, path: str, model: type[T]
|
self, path: str, model: type[T]
|
||||||
) -> T:
|
) -> T:
|
||||||
assert self._http is not None, "use `async with CycloneClient(...)`"
|
if self._http is None:
|
||||||
|
raise RuntimeError("use `async with CycloneClient(...)`")
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
for attempt in range(MAX_RETRIES):
|
for attempt in range(MAX_RETRIES):
|
||||||
try:
|
try:
|
||||||
resp = await self._http.get(path)
|
resp = await self._http.get(path)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return model.model_validate(resp.json())
|
return model.model_validate(resp.json())
|
||||||
except (httpx.HTTPStatusError, httpx.RequestError) as e:
|
except httpx.HTTPStatusError as e:
|
||||||
|
# 4xx is terminal — surface immediately, do not retry
|
||||||
|
if e.response is not None and e.response.status_code < 500:
|
||||||
|
raise
|
||||||
last_exc = e
|
last_exc = e
|
||||||
if attempt < MAX_RETRIES - 1:
|
except httpx.RequestError as e:
|
||||||
wait_s = BACKOFF_S[attempt]
|
# connect/read failures are transient — retry
|
||||||
log.warning(
|
last_exc = e
|
||||||
"api_get_retry",
|
if attempt < MAX_RETRIES - 1:
|
||||||
path=path, attempt=attempt + 1, wait_s=wait_s,
|
wait_s = BACKOFF_S[attempt]
|
||||||
error=str(e),
|
log.warning(
|
||||||
)
|
"api_get_retry",
|
||||||
await asyncio.sleep(wait_s)
|
path=path, attempt=attempt + 1, wait_s=wait_s,
|
||||||
assert last_exc is not None
|
error=str(last_exc),
|
||||||
|
)
|
||||||
|
await asyncio.sleep(wait_s)
|
||||||
|
assert last_exc is not None # loop body always sets this on the way out
|
||||||
raise last_exc
|
raise last_exc
|
||||||
|
|
||||||
# --- methods -----------------------------------------------------------
|
# --- methods -----------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user