o2o-negosium-original/lps/worker_main.py
민헌 d54dee4f24 feat(lps): 재시도/못찾음 완결 로직 — 재정제 루프 + not_found + 네거티브 캐시
'못 찾음'을 유한하게 종료. 두 재시도 축을 분리(기술=큐 attempts/백오프,
검색어=refine 라운드). not_found 는 정상 종료(DONE)지 dead-letter 아님.

- ai/keyword: LLM 검색어 생성(정밀/광역) — 원본 0매칭 시에만 지연 호출(비용 절약)
- handler: 한정 재정제 루프(원본→정밀→광역, max_rounds=3) + 명시적 outcome(found/not_found)
  · 0매칭+소스정상 → 다음 라운드, 0매칭+기술실패 → raise(큐 재시도)
  · 라운드 소진 → not_found + 네거티브 캐시 기록
- negative_cache: search_negative 테이블 + TTL(24h) upsert — 같은 상품 재요청 재검색 차단
- worker_main: OPENAI 있으면 judge+keyword_gen ON, neg_cache 상시
- tests: 재정제/not_found/캐시히트/기술실패/캐시CRUD 11건 → 전체 39/39
- 라이브: 없는상품 3라운드→not_found(30s), 재요청 캐시히트(0.00s)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:12:47 +09:00

63 lines
2.4 KiB
Python

# LPS 워커 프로세스 진입점 (API 와 분리 실행 — 코드베이스 공유, 독립 스케일).
# python worker_main.py
# WORKER_CONCURRENCY=2 python worker_main.py
#
# 쿠팡 검색은 브라우저(Chrome)라 무겁고 컨텍스트당 직렬이므로 기본 동시성은 1.
# 여러 브라우저로 늘리려면 CoupangAdapter 인스턴스를 워커마다 따로 준다.
import asyncio
import os
from common.logger import LOG
from config.server_configs import web_server_config
from crud.job_crud import JobQueue
from crud.negative_cache import NegativeCache
from services.search.coupang.adapter import CoupangAdapter
from services.search.naver.adapter import NaverAdapter
from services.ai.similarity import SimilarityJudge
from services.ai.keyword import KeywordGenerator
from worker.handlers import build_search_handler
from worker.notify import JobListener
from worker.runner import Worker, run_reaper
LOG.SetPrefix(f"{web_server_config.server_name}-worker")
async def main(concurrency: int = 1):
queue = JobQueue()
# 쿠팡(브라우저, 무거움) + 네이버(오픈API, 가벼움) 동시 검색 → 병합 최저가
adapters = {"coupang": CoupangAdapter(headless=False), "naver": NaverAdapter()}
# OPENAI_API_KEY 있으면 '같은 상품' AI 판정 + 재검색어 생성 활성화
has_openai = bool(os.environ.get("OPENAI_API_KEY"))
judge = SimilarityJudge() if has_openai else None
keyword_gen = KeywordGenerator() if has_openai else None
LOG.i(f"AI(판정+검색어생성): {'ON' if has_openai else 'OFF(키 없음)'}")
handler = build_search_handler(adapters, judge=judge, keyword_gen=keyword_gen, neg_cache=NegativeCache())
stop = asyncio.Event()
listeners: list[JobListener] = []
tasks: list[asyncio.Task] = []
for i in range(concurrency):
listener = JobListener()
await listener.start()
listeners.append(listener)
worker = Worker(f"worker-{i}", queue, handler)
tasks.append(asyncio.create_task(worker.run(listener, stop)))
tasks.append(asyncio.create_task(run_reaper(queue, stop)))
LOG.i(f"LPS 워커 {concurrency}개 + reaper 기동")
try:
await asyncio.gather(*tasks)
finally:
stop.set()
for listener in listeners:
await listener.close()
for adapter in adapters.values():
await adapter.close()
if __name__ == "__main__":
asyncio.run(main(int(os.environ.get("WORKER_CONCURRENCY", "1"))))