datacenter IP + Akamai 대응. 매 요청 IP 변경은 쿠키-IP 불일치로 재챌린지를 유발하므로, sticky 세션(일정 시간 같은 IP) + 주기적 회전 방식 사용. - search/proxy.DecodoProxy: username 에 -session-<시간창id>-sessionduration-<분> 부착 → 창 안에선 같은 IP, 창이 지나면 새 IP. 자격증명은 .env(DECODO_*), 4개 다 있어야 활성 - coupang/adapter: proxy 주입 + 세션창 경과 시 브라우저 재기동(IP 회전). 무프록시면 직접 연결 - worker_main: 쿠팡에만 DecodoProxy 경유(네이버는 공식 API라 미적용) - .env.example: DECODO_HOST/PORT/USERNAME/PASSWORD/SESSION_MINUTES 슬롯 - tests: 활성조건·sticky username·server형식·세션창 회전 4건 → 전체 43/43 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.8 KiB
Python
67 lines
2.8 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.proxy import DecodoProxy
|
|
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, 가벼움) 동시 검색 → 병합 최저가
|
|
# DECODO 프록시: .env 에 값 있으면 쿠팡만 sticky+주기적 회전으로 경유(없으면 직접 연결)
|
|
proxy = DecodoProxy()
|
|
LOG.i(f"DECODO 프록시: {'ON(sticky ' + str(proxy.session_minutes) + '분 회전)' if proxy.enabled else 'OFF(.env 미설정)'}")
|
|
adapters = {"coupang": CoupangAdapter(headless=False, proxy=proxy), "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"))))
|