큐를 실제로 돌린다: 적재 → 워커 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>
53 lines
1.8 KiB
Python
53 lines
1.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 services.search.coupang.adapter import CoupangAdapter
|
|
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()
|
|
adapters = {"coupang": CoupangAdapter(headless=False)} # 워커들이 공유(내부 직렬화)
|
|
handler = build_search_handler(adapters)
|
|
|
|
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"))))
|