Skip to content

Commit e21bd70

Browse files
mirror29claude
andcommitted
fix(data): 重试财经快讯瞬时连接故障
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d5e0e20 commit e21bd70

2 files changed

Lines changed: 83 additions & 7 deletions

File tree

services/data/src/inalpha_data/connectors/cn_market.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
)
6262

6363

64+
_TRANSIENT_RETRY_DELAY_S = 0.5
65+
_TRANSIENT_RETRY_TIMEOUT_S = 10.0
66+
67+
6468
class CnMarketError(RuntimeError):
6569
"""市场级数据源失败(网络 / 反爬 / 改版)。API 层转 502,不静默。"""
6670

@@ -303,12 +307,47 @@ async def _get_json(
303307
wait = self._min_interval - elapsed
304308
if wait > 0:
305309
await asyncio.sleep(wait + random.uniform(0.1, 0.5))
306-
try:
307-
resp = await self._client.get(url, params=params, headers=headers)
308-
except httpx.HTTPError as exc:
309-
raise CnMarketError(f"{host_key} request failed: {exc}") from exc
310-
finally:
311-
self._host_last[host_key] = time.monotonic()
310+
# 首轮 10s + 重试完整 timeout,保证整体仍低于 orchestration 30s 调用预算。
311+
# `_host_last` 记录每次尝试结束;重试循环不会再次走上方限速计算,显式
312+
# sleep 使用两者较大值,既快速恢复,也不突破源站最小请求间隔。
313+
resp: httpx.Response | None = None
314+
for attempt in range(2):
315+
try:
316+
resp = await self._client.get(
317+
url,
318+
params=params,
319+
headers=headers,
320+
timeout=(
321+
min(self._timeout, _TRANSIENT_RETRY_TIMEOUT_S)
322+
if attempt == 0
323+
else self._timeout
324+
),
325+
)
326+
break
327+
except httpx.TransportError as exc:
328+
if attempt == 0:
329+
_logger.warning(
330+
"cn_market_transient_retry",
331+
host=host_key,
332+
error_type=type(exc).__name__,
333+
error=str(exc),
334+
)
335+
await asyncio.sleep(
336+
max(_TRANSIENT_RETRY_DELAY_S, self._min_interval)
337+
)
338+
continue
339+
raise CnMarketError(
340+
f"{host_key} request failed after retry: "
341+
f"{type(exc).__name__}: {exc}"
342+
) from exc
343+
except httpx.HTTPError as exc:
344+
raise CnMarketError(
345+
f"{host_key} request failed: {type(exc).__name__}: {exc}"
346+
) from exc
347+
finally:
348+
self._host_last[host_key] = time.monotonic()
349+
if resp is None: # pragma: no cover - 循环必返回或抛错,供类型收窄
350+
raise CnMarketError(f"{host_key} request failed without response")
312351
if resp.status_code in (403, 429):
313352
raise CnMarketError(f"{host_key} rate-limited/blocked: HTTP {resp.status_code}")
314353
if resp.status_code >= 400:

services/data/tests/test_market.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import asyncio
99
from typing import Any
1010

11+
import httpx
1112
import pytest
1213
from fastapi.testclient import TestClient
1314

@@ -287,6 +288,42 @@ async def test_connector_caches_success() -> None:
287288
await conn.close()
288289

289290

291+
async def test_get_json_retries_one_transient_connect_timeout(
292+
monkeypatch: pytest.MonkeyPatch,
293+
) -> None:
294+
"""TLS 建连瞬时超时应重试一次,避免东财边缘节点抖动直接变成 502。"""
295+
conn = CnMarketConnector()
296+
conn._min_interval = 1.0
297+
conn._timeout = 15.0
298+
299+
class FakeResp:
300+
status_code = 200
301+
302+
@staticmethod
303+
def json() -> dict[str, Any]:
304+
return {"data": {"fastNewsList": []}}
305+
306+
attempts = 0
307+
308+
seen_timeouts: list[float] = []
309+
310+
async def fake_get(url: str, params=None, headers=None, **kwargs: Any) -> FakeResp:
311+
nonlocal attempts
312+
attempts += 1
313+
seen_timeouts.append(kwargs["timeout"])
314+
if attempts == 1:
315+
raise httpx.ConnectTimeout("TLS handshake timed out")
316+
return FakeResp()
317+
318+
monkeypatch.setattr(conn._client, "get", fake_get)
319+
result = await conn._get_json("h", "https://example.com", params=None, headers=None)
320+
321+
assert result == {"data": {"fastNewsList": []}}
322+
assert attempts == 2
323+
assert seen_timeouts == [10.0, 15.0]
324+
await conn.close()
325+
326+
290327
async def test_get_json_serializes_per_host(monkeypatch: pytest.MonkeyPatch) -> None:
291328
"""同 host 两次请求间隔 ≥ min_interval(防封铁律)。"""
292329
conn = CnMarketConnector()
@@ -307,7 +344,7 @@ async def fake_sleep(d: float) -> None:
307344
sleeps.append(d)
308345
await real_sleep(0)
309346

310-
async def fake_get(url: str, params=None, headers=None) -> FakeResp:
347+
async def fake_get(url: str, params=None, headers=None, **kwargs: Any) -> FakeResp:
311348
return FakeResp()
312349

313350
monkeypatch.setattr(conn._client, "get", fake_get)

0 commit comments

Comments
 (0)