큐를 실제로 돌린다: 적재 → 워커 claim → 핸들러 실행 → 결과 저장 → DONE. API(적재)와 워커(소비)를 분리 프로세스로(코드베이스 공유, 독립 스케일). - worker/notify: 전용 asyncpg LISTEN 리스너. enqueue 에서 pg_notify → 유휴 워커 즉시 기상(폴링 제거) - worker/runner: Worker(claim→처리, 처리중 heartbeat 로 lease 갱신, complete/fail) + run_reaper - worker/handlers: job_type 별 핸들러(주입식). SEARCH=소스 어댑터 검색→정규화 결과 - worker_main: API 분리 워커 진입점(브라우저 무거워 기본 동시성 1) - job_crud.enqueue: 삽입 시 pg_notify (중복 스킵 시엔 미발생) - tests: drain→DONE·실패→재시도→dead·reaper 회수 후 재처리·NOTIFY 기상 4건 (전체 20/20) - 라이브 E2E 확인: 적재→워커가 실제 쿠팡 검색(8건)→DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
36 lines
1.3 KiB
Python
36 lines
1.3 KiB
Python
"""잡 핸들러 — job_type 별 처리. 현재는 SEARCH(검색)만.
|
|
|
|
검색 핸들러는 소스 어댑터로 검색해 정규화 결과를 반환한다.
|
|
필터/이상치/AI 유사도(코어 파이프라인)는 다음 단계에서 이 핸들러 안에 결합한다.
|
|
"""
|
|
|
|
from common.enums import JobType
|
|
from services.search.contract import SearchAdapter
|
|
|
|
|
|
def build_search_handler(adapters: dict[str, SearchAdapter], default_source: str = "coupang", limit: int = 40):
|
|
"""검색 핸들러 생성. adapters = {source: SearchAdapter}."""
|
|
|
|
async def handler(job: dict) -> dict:
|
|
if job["job_type"] != JobType.SEARCH.value:
|
|
raise ValueError(f"unsupported job_type: {job['job_type']}")
|
|
|
|
payload = job.get("payload") or {}
|
|
query = (payload.get("product_name") or "").strip()
|
|
if not query:
|
|
raise ValueError("empty product_name")
|
|
|
|
adapter = adapters.get(default_source)
|
|
if adapter is None:
|
|
raise ValueError(f"no adapter for source: {default_source}")
|
|
|
|
products = await adapter.search(query, limit=limit)
|
|
return {
|
|
"source": adapter.source,
|
|
"query": query,
|
|
"count": len(products),
|
|
"products": [p.model_dump() for p in products],
|
|
}
|
|
|
|
return handler
|