o2o-negosium-original/lps/worker/handlers.py
민헌 abcb58ba05 feat(lps): 워커 루프 + LISTEN/NOTIFY — 큐 소비 파이프라인 가동
큐를 실제로 돌린다: 적재 → 워커 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>
2026-07-08 16:58:58 +09:00

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