fix: retry transient Tencent quote failures
This commit is contained in:
@@ -50,10 +50,12 @@ class TencentQuoteProvider:
|
|||||||
*,
|
*,
|
||||||
endpoint: str = TENCENT_ENDPOINT,
|
endpoint: str = TENCENT_ENDPOINT,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
|
retries: int = 1,
|
||||||
fetcher: Callable[[str, float], bytes] | None = None,
|
fetcher: Callable[[str, float], bytes] | None = None,
|
||||||
):
|
):
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
self.retries = retries
|
||||||
self._fetcher = fetcher or _default_fetcher
|
self._fetcher = fetcher or _default_fetcher
|
||||||
|
|
||||||
def fetch_quotes(self, instruments: Iterable[Instrument]) -> dict[str, QuoteSnapshot]:
|
def fetch_quotes(self, instruments: Iterable[Instrument]) -> dict[str, QuoteSnapshot]:
|
||||||
@@ -62,12 +64,18 @@ class TencentQuoteProvider:
|
|||||||
return {}
|
return {}
|
||||||
query = ",".join(symbols)
|
query = ",".join(symbols)
|
||||||
url = f"{self.endpoint}{query}"
|
url = f"{self.endpoint}{query}"
|
||||||
try:
|
raw = self._fetch_with_retry(url)
|
||||||
raw = self._fetcher(url, self.timeout)
|
|
||||||
except (OSError, HTTPError, URLError) as exc:
|
|
||||||
raise QuoteFetchError(f"Tencent quote request failed: {exc}") from exc
|
|
||||||
return {quote.code: quote for quote in parse_tencent_response(raw)}
|
return {quote.code: quote for quote in parse_tencent_response(raw)}
|
||||||
|
|
||||||
|
def _fetch_with_retry(self, url: str) -> bytes:
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for _ in range(self.retries + 1):
|
||||||
|
try:
|
||||||
|
return self._fetcher(url, self.timeout)
|
||||||
|
except (OSError, HTTPError, URLError) as exc:
|
||||||
|
last_error = exc
|
||||||
|
raise QuoteFetchError(f"Tencent quote request failed: {last_error}") from last_error
|
||||||
|
|
||||||
|
|
||||||
def _default_fetcher(url: str, timeout: float) -> bytes:
|
def _default_fetcher(url: str, timeout: float) -> bytes:
|
||||||
with urlopen(url, timeout=timeout) as response:
|
with urlopen(url, timeout=timeout) as response:
|
||||||
|
|||||||
@@ -74,3 +74,25 @@ def test_provider_wraps_fetch_errors():
|
|||||||
|
|
||||||
with pytest.raises(QuoteFetchError, match="Tencent quote request failed"):
|
with pytest.raises(QuoteFetchError, match="Tencent quote request failed"):
|
||||||
provider.fetch_quotes([Instrument(id=1, code="000001", name="平安银行")])
|
provider.fetch_quotes([Instrument(id=1, code="000001", name="平安银行")])
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_retries_once_after_transient_fetch_error():
|
||||||
|
raw = (
|
||||||
|
'v_sz000001="51~平安银行~000001~10.60~10.47~10.44~0~0~0~'
|
||||||
|
'0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~0~~20260708161430~0.13~1.24~10.63~10.34";'
|
||||||
|
).encode("gbk")
|
||||||
|
calls = 0
|
||||||
|
|
||||||
|
def flaky_fetch(url: str, timeout: float) -> bytes:
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
if calls == 1:
|
||||||
|
raise OSError("bad gateway")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
provider = TencentQuoteProvider(fetcher=flaky_fetch, retries=1)
|
||||||
|
|
||||||
|
quotes = provider.fetch_quotes([Instrument(id=1, code="000001", name="平安银行")])
|
||||||
|
|
||||||
|
assert calls == 2
|
||||||
|
assert quotes["000001"].price == Decimal("10.60")
|
||||||
|
|||||||
Reference in New Issue
Block a user