커머스 검색요청 계약을 프레임워크(protocol+service+RemoveNoneResponse)로 재구성.
실제 검색은 워커가 큐에서 꺼내 수행하도록 API 는 적재까지만 담당(비동기 분리).
- POST /v1/lps/search: 상품 리스트 → 상품별 SEARCH 잡 적재, product_code 로 활성 중복 방지, job_type→우선순위 매핑
- GET /v1/lps/jobs/{job_id}: 잡 상태/시도/결과 조회
- GET /v1/lps/queue/stats: 상태별 카운트(모니터링)
- protocol/lps_service 추가, job_crud.get() 단건조회, enums LPS_JOB_NOT_FOUND
- tests: 적재/중복/상태/미존재/통계 5건 (ASGI 클라이언트 + 실 lps_db) → 전체 16/16
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""LPS 검색 요청 도메인 로직. 요청을 검색 잡으로 큐에 적재하고 상태/통계를 조회한다.
|
|
|
|
라우터는 요청 검증→service 호출→RemoveNoneResponse 만 담당(backend 컨벤션).
|
|
실제 검색/크롤링은 워커가 큐에서 잡을 꺼내 파이프라인으로 수행한다(비동기 분리).
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from fastapi import Depends
|
|
|
|
from common.enums import ErrorType, JobStatus, JobType
|
|
from crud.job_crud import JobQueue
|
|
from router.v1.lps.protocol import (
|
|
EnqueuedItem,
|
|
Res_JobStatus,
|
|
Res_QueueStats,
|
|
Res_Search,
|
|
SearchItem,
|
|
)
|
|
|
|
# 요청 유형 → 우선순위(낮을수록 우선). 알 수 없는 유형은 가장 낮은 우선순위(배치와 동급).
|
|
_PRIORITY = {"new": 1, "single": 2, "negowiz": 3, "batch": 4}
|
|
|
|
|
|
class LpsService:
|
|
def __init__(self, queue: JobQueue = Depends(JobQueue)):
|
|
self.queue = queue
|
|
|
|
async def submit_search(self, items: list[SearchItem]) -> Res_Search:
|
|
res = Res_Search()
|
|
for it in items:
|
|
priority = _PRIORITY.get(it.job_type, 4)
|
|
job_id = await self.queue.enqueue(
|
|
JobType.SEARCH.value,
|
|
it.model_dump(),
|
|
priority=priority,
|
|
dedupe_key=f"search-{it.product_code}",
|
|
)
|
|
res.items.append(EnqueuedItem(product_code=it.product_code, job_id=job_id, duplicated=job_id is None))
|
|
res.accepted = sum(1 for i in res.items if i.job_id is not None)
|
|
return res
|
|
|
|
async def get_job(self, job_id_str: str) -> Res_JobStatus:
|
|
res = Res_JobStatus()
|
|
try:
|
|
uuid.UUID(job_id_str) # 잘못된 id 는 DB 조회 전에 컷
|
|
except (ValueError, TypeError):
|
|
res.result.SetResult(ErrorType.LPS_JOB_NOT_FOUND)
|
|
return res
|
|
|
|
row = await self.queue.get(job_id_str)
|
|
if row is None:
|
|
res.result.SetResult(ErrorType.LPS_JOB_NOT_FOUND)
|
|
return res
|
|
|
|
res.job_id = row["job_id"]
|
|
res.status = JobStatus(row["status"]).name
|
|
res.attempts = row["attempts"]
|
|
res.max_attempts = row["max_attempts"]
|
|
res.output = row.get("result")
|
|
res.last_error = row.get("last_error")
|
|
return res
|
|
|
|
async def stats(self) -> Res_QueueStats:
|
|
res = Res_QueueStats()
|
|
res.counts = await self.queue.counts()
|
|
return res
|