fix(news): 연결 재시도 2회로 보강 — 드랍이 연결 단위 랜덤(재시도 1회도 연속 피격 실측) + 빈 에러 로그 repr
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -231,7 +231,8 @@ async def _run_locked():
|
|||||||
_record_success(health, count, status == "not_modified", now)
|
_record_success(health, count, status == "not_modified", now)
|
||||||
total += count
|
total += count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[{source.name}] 수집 실패: {e}")
|
# str 이 빈 예외(httpx.ConnectError('')) 대비 — health 기록과 동일 규칙
|
||||||
|
logger.error(f"[{source.name}] 수집 실패: {str(e) or repr(e)}")
|
||||||
source.last_fetched_at = datetime.now(timezone.utc)
|
source.last_fetched_at = datetime.now(timezone.utc)
|
||||||
_record_failure(health, str(e) or repr(e), now)
|
_record_failure(health, str(e) or repr(e), now)
|
||||||
|
|
||||||
@@ -244,19 +245,25 @@ ALLOWED_CONTENT_TYPES = ("application/rss+xml", "application/atom+xml",
|
|||||||
"application/xml", "text/xml")
|
"application/xml", "text/xml")
|
||||||
|
|
||||||
|
|
||||||
async def _get_with_connect_retry(client, url: str):
|
# 연결 재시도 간격 — MOEL 추가 실측(2026-06-11): 드랍이 연결 단위 랜덤이라
|
||||||
"""연결 계층(TCP/TLS) 오류만 1회 재시도 — HTTP 상태 오류는 비대상 (호출측 분기 보존).
|
# 1.5s 후 재시도도 연속으로 걸리는 케이스 발생(직후 다른 연결은 즉시 성공) → 2회로 보강.
|
||||||
|
_CONNECT_RETRY_DELAYS = (2.0, 5.0)
|
||||||
|
|
||||||
MOEL 실측(2026-06-11): 정부 사이트 보안장비가 첫 TLS 핸드셰이크를 간헐 드랍
|
|
||||||
(curl rc=35, 직후 재시도 성공) → 사이클당 1회 fetch 인 피드 수집이 ConnectError('')
|
async def _get_with_connect_retry(client, url: str):
|
||||||
로 실패 누적·circuit open. 재시도 1회면 흡수됨 — 지속 장애는 그대로 circuit 몫.
|
"""연결 계층(TCP/TLS) 오류만 재시도(최대 2회) — HTTP 상태 오류는 비대상 (호출측 분기 보존).
|
||||||
|
|
||||||
|
MOEL 실측(2026-06-11): 정부 사이트 보안장비가 TLS 핸드셰이크를 연결 단위로 간헐 드랍
|
||||||
|
(curl rc=35, 직후 재시도는 성공) → 사이클당 1회 fetch 인 피드 수집이 ConnectError('')
|
||||||
|
로 실패 누적·circuit open. 지속 장애는 그대로 circuit 몫.
|
||||||
"""
|
"""
|
||||||
try:
|
for delay in _CONNECT_RETRY_DELAYS:
|
||||||
return await client.get(url)
|
try:
|
||||||
except (httpx.ConnectError, httpx.ConnectTimeout) as e:
|
return await client.get(url)
|
||||||
logger.info(f"연결 오류 1회 재시도 ({url.split('?')[0]}): {repr(e)}")
|
except (httpx.ConnectError, httpx.ConnectTimeout) as e:
|
||||||
await asyncio.sleep(1.5)
|
logger.info(f"연결 오류 {delay}s 후 재시도 ({url.split('?')[0]}): {repr(e)}")
|
||||||
return await client.get(url)
|
await asyncio.sleep(delay)
|
||||||
|
return await client.get(url)
|
||||||
|
|
||||||
|
|
||||||
async def _is_portal_duplicate(session, title: str) -> bool:
|
async def _is_portal_duplicate(session, title: str) -> bool:
|
||||||
|
|||||||
@@ -139,12 +139,20 @@ class TestConnectRetry:
|
|||||||
assert resp == "OK" and client.calls == 2
|
assert resp == "OK" and client.calls == 2
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_persistent_connect_error_propagates(self):
|
async def test_second_retry_absorbs_consecutive_drop(self):
|
||||||
|
"""드랍이 연결 단위 랜덤이라 재시도 1회도 연속으로 걸림 (MOEL lawinfo 실측)."""
|
||||||
import httpx
|
import httpx
|
||||||
client = self._Client([httpx.ConnectError(""), httpx.ConnectError("")])
|
client = self._Client([httpx.ConnectError(""), httpx.ConnectError("")])
|
||||||
|
resp = await news_collector._get_with_connect_retry(client, "https://x/feed")
|
||||||
|
assert resp == "OK" and client.calls == 3
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_persistent_connect_error_propagates(self):
|
||||||
|
import httpx
|
||||||
|
client = self._Client([httpx.ConnectError("")] * 3)
|
||||||
with pytest.raises(httpx.ConnectError):
|
with pytest.raises(httpx.ConnectError):
|
||||||
await news_collector._get_with_connect_retry(client, "https://x/feed")
|
await news_collector._get_with_connect_retry(client, "https://x/feed")
|
||||||
assert client.calls == 2 # 1회만 재시도 — 지속 장애는 circuit 몫
|
assert client.calls == 3 # 최대 2회 재시도 — 지속 장애는 circuit 몫
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_non_connect_errors_not_retried(self):
|
async def test_non_connect_errors_not_retried(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user