perf(lps): Step1 — 폴백 데드라인 + fail-fast로 검색 지연 상한
G마켓 챌린지가 안 풀린 채 25~70초를 먹던 문제에 상한을 씌운다. 폴백은 '있으면 좋은' 보강이라 데드라인으로 잘라도 안전(그 몰만 스킵). - handler: 각 폴백 크롤에 fallback_deadline_sec(기본 15s) wait_for. 초과 시 스킵. _timed_search 예외를 BaseException 로(CancelledError 계측 포함). - 폴백 어댑터 max_block_retries=0(챌린지 대기 2배 방지), ESM ready_timeout 22→12s, st11 20→15s. - 테스트: 느린 폴백 데드라인 스킵 1종. 실측(동시성2·4상품): 상품당 p50 84.6s→34.2s(~2.5x), 벽시계 181s→65s(~2.8x), 처리량 1.3→3.7 상품/분, 1000건 $7.12→$5.06(~29%↓, 실패 챌린지 대역폭 절감). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ba67275cd8
commit
4b8dee8583
@ -20,7 +20,7 @@ _BLOCK_MARKERS = ("errors.edgesuite.net", "You don't have permission to access",
|
||||
class EsmAdapter(BrowserSearchAdapter):
|
||||
block_markers = _BLOCK_MARKERS
|
||||
min_result_html = 8000 # 챌린지 페이지(~21KB)는 크므로 short 폴백은 명백한 에러만
|
||||
ready_timeout_ms = 22000 # '잠시만' 챌린지 통과(~9초) 여유
|
||||
ready_timeout_ms = 12000 # 챌린지 통과(정상 ~9초). 폴백은 데드라인이 상한이라 과도한 대기 회피
|
||||
block_resources_default = True # 라우팅 on — 단, 아래 집합만(CSS/JS 유지)
|
||||
blocked_resource_types = {"image", "media", "font"} # 대역폭 대부분(이미지)만 차단, 챌린지/렌더용 CSS/JS는 유지
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ class ElevenStAdapter(BrowserSearchAdapter):
|
||||
block_markers = _BLOCK_MARKERS
|
||||
min_result_html = 20000 # PC 정상 결과는 수백 KB — 차단/미렌더 페이지는 작다(→회전/재시도)
|
||||
ready_selector = READY_SELECTOR
|
||||
ready_timeout_ms = 20000
|
||||
ready_timeout_ms = 15000
|
||||
block_resources_default = True # 라우팅 on — 이미지/미디어/폰트만(CSS/JS 유지해야 렌더됨)
|
||||
blocked_resource_types = {"image", "media", "font"}
|
||||
scroll_steps = 3 # 결과가 지연 로딩 → 스크롤로 트리거
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"""검색 핸들러 테스트 — 병합·실패격리·AI판정·재정제 루프·not_found·네거티브 캐시 (fake 의존성)."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from common.enums import JobType
|
||||
@ -8,17 +10,20 @@ from worker.handlers import build_search_handler
|
||||
|
||||
|
||||
class FakeAdapter:
|
||||
def __init__(self, source, by_query=None, products=None, fail=False, uses_proxy=False, last_bytes=0):
|
||||
def __init__(self, source, by_query=None, products=None, fail=False, uses_proxy=False, last_bytes=0, delay=0.0):
|
||||
self.source = source
|
||||
self._by_query = by_query # {query: [products]}
|
||||
self._products = products or []
|
||||
self._fail = fail
|
||||
self._delay = delay # search 지연(초) — 데드라인 테스트용
|
||||
self.uses_proxy = uses_proxy # DECODO 경유 여부(비용 귀속)
|
||||
self.last_bytes = last_bytes
|
||||
self.calls = []
|
||||
|
||||
async def search(self, query, limit=40):
|
||||
self.calls.append(query)
|
||||
if self._delay:
|
||||
await asyncio.sleep(self._delay)
|
||||
if self._fail:
|
||||
raise AdapterError("boom", source=self.source, blocked=True)
|
||||
if self._by_query is not None:
|
||||
@ -150,6 +155,22 @@ async def test_fallback_crawls_only_uncovered_malls():
|
||||
assert malls == {"G마켓", "옥션", "11번가"} # 네이버 G마켓 + 크롤 옥션·11번가
|
||||
|
||||
|
||||
async def test_fallback_deadline_skips_slow_mall():
|
||||
# 느린 폴백(데드라인 초과)은 스킵되고, 빠른 폴백은 병합된다 — 전체 지연에 상한.
|
||||
adapters = {"naver": FakeAdapter("naver", products=[_np("naver", 9000, mall="네이버")])}
|
||||
slow = FakeAdapter("gmarket", products=[_np("gmarket", 1000, mall="G마켓")], delay=1.0) # 데드라인 초과
|
||||
fast = FakeAdapter("st11", products=[_np("st11", 3000, mall="11번가")], delay=0.0)
|
||||
r = await build_search_handler(
|
||||
adapters, judge=FakeJudge(lambda c: True),
|
||||
fallback_adapters={"gmarket": slow, "st11": fast},
|
||||
fallback_deadline_sec=0.2,
|
||||
)(_job())
|
||||
malls = {m["mall_name"] for m in r["by_mall"]}
|
||||
assert "11번가" in malls # 빠른 폴백 병합됨
|
||||
assert "G마켓" not in malls # 느린 폴백은 데드라인 초과로 스킵
|
||||
assert r["lowest"]["price"] == 3000 # G마켓 1000은 스킵됐으므로 최저가 아님
|
||||
|
||||
|
||||
async def test_fallback_failure_is_isolated():
|
||||
adapters = {"naver": FakeAdapter("naver", products=[_np("naver", 9000, mall="네이버")])}
|
||||
st11 = FakeAdapter("st11", fail=True) # 크롤 실패
|
||||
|
||||
@ -56,6 +56,7 @@ def build_search_handler(
|
||||
fallback_adapters: dict[str, SearchAdapter] | None = None,
|
||||
ai_model: str = "",
|
||||
proxy_cost_per_gb: float = 0.0,
|
||||
fallback_deadline_sec: float = 15.0,
|
||||
):
|
||||
"""검색 핸들러 생성.
|
||||
judge: SimilarityJudge(같은 상품 판정) / keyword_gen: KeywordGenerator(정밀·광역 재검색어) /
|
||||
@ -82,8 +83,8 @@ def build_search_handler(
|
||||
res = await adapter.search(query, limit=limit)
|
||||
metrics.add_fetch(source, getattr(adapter, "last_bytes", 0), int((time.monotonic() - t0) * 1000), crawl=crawl, via_proxy=via_proxy)
|
||||
return res
|
||||
except Exception:
|
||||
metrics.add_fetch(source, 0, int((time.monotonic() - t0) * 1000), crawl=crawl, via_proxy=via_proxy)
|
||||
except BaseException: # CancelledError(데드라인 취소) 포함 — 소요/바이트는 계측하고 재전파
|
||||
metrics.add_fetch(source, getattr(adapter, "last_bytes", 0), int((time.monotonic() - t0) * 1000), crawl=crawl, via_proxy=via_proxy)
|
||||
raise
|
||||
|
||||
async def _search_round(query: str, metrics: SearchMetrics):
|
||||
@ -124,8 +125,15 @@ def build_search_handler(
|
||||
return matched
|
||||
|
||||
async def _crawl_match(src, adapter):
|
||||
# 폴백은 '있으면 좋은' 보강이라 데드라인을 건다 — 초과 시 그 몰만 스킵(전체 지연에 상한).
|
||||
try:
|
||||
crawled = await _timed_search(adapter, query, src, metrics, crawl=True)
|
||||
crawled = await asyncio.wait_for(
|
||||
_timed_search(adapter, query, src, metrics, crawl=True),
|
||||
timeout=fallback_deadline_sec,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
LOG.w(f"[fallback:{src}] 데드라인 {fallback_deadline_sec:.0f}s 초과 → 스킵")
|
||||
return []
|
||||
except Exception as ex:
|
||||
LOG.w(f"[fallback:{src}] 크롤 실패(무시): {type(ex).__name__}: {ex}")
|
||||
return []
|
||||
|
||||
@ -46,10 +46,11 @@ def _build_worker(i: int, concurrency: int, has_openai: bool, neg_cache, history
|
||||
"coupang": CoupangAdapter(headless=False, user_data_dir=_pf("coupang"), proxy=proxy, on_detect=bot_log.record),
|
||||
"naver": NaverAdapter(), # httpx 직접(프록시 미경유) — 워커별 인스턴스(last_bytes 경합 회피)
|
||||
}
|
||||
# 폴백은 데드라인이 상한이라 봇감지 재시도(챌린지 대기 2배)를 끈다(max_block_retries=0) — 빠르게 포기·스킵.
|
||||
fallback_adapters = {
|
||||
"gmarket": EsmAdapter("gmarket", headless=False, user_data_dir=_pf("gmarket"), proxy=proxy, on_detect=bot_log.record),
|
||||
"auction": EsmAdapter("auction", headless=False, user_data_dir=_pf("auction"), proxy=proxy, on_detect=bot_log.record),
|
||||
"st11": ElevenStAdapter(headless=False, user_data_dir=_pf("st11"), proxy=proxy, on_detect=bot_log.record),
|
||||
"gmarket": EsmAdapter("gmarket", headless=False, user_data_dir=_pf("gmarket"), proxy=proxy, on_detect=bot_log.record, max_block_retries=0),
|
||||
"auction": EsmAdapter("auction", headless=False, user_data_dir=_pf("auction"), proxy=proxy, on_detect=bot_log.record, max_block_retries=0),
|
||||
"st11": ElevenStAdapter(headless=False, user_data_dir=_pf("st11"), proxy=proxy, on_detect=bot_log.record, max_block_retries=0),
|
||||
}
|
||||
# AI 도 워커별 인스턴스 — 공유 상태(last_usage) 경합 원천 제거
|
||||
judge = SimilarityJudge() if has_openai else None
|
||||
|
||||
Loading…
Reference in New Issue
Block a user