fix(queue): enqueue 경로 중복 방어 — partial unique index + 중앙 enqueue_stage 함수

기존 UNIQUE(document_id, stage, status)는 pending+processing 동시 존재를
허용해서 stale 복구 시 충돌 발생. 2-layer 방어로 근본 차단:

1) DB: partial unique index uq_queue_active — 활성 행(pending/processing)은
   (document_id, stage)당 최대 1개만 허용
2) App: enqueue_stage() 중앙 함수 — INSERT ON CONFLICT DO NOTHING으로
   모든 9개 경로의 check-then-insert TOCTOU race 제거

migration 117은 guard check 포함 — 활성 중복이 남아있으면 RAISE EXCEPTION
으로 중단, 수동 정리 유도.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-15 08:37:32 +09:00
parent 8ec1e53ca4
commit 751cdc5be8
9 changed files with 77 additions and 65 deletions
+2 -16
View File
@@ -8,7 +8,7 @@ from sqlalchemy.orm import aliased
from core.database import async_session
from core.utils import setup_logger
from models.queue import ProcessingQueue
from models.queue import ProcessingQueue, enqueue_stage
logger = setup_logger("queue_consumer")
@@ -103,21 +103,7 @@ async def enqueue_next_stage(document_id: int, current_stage: str):
async with async_session() as session:
for next_stage in stages:
existing = await session.execute(
select(ProcessingQueue).where(
ProcessingQueue.document_id == document_id,
ProcessingQueue.stage == next_stage,
ProcessingQueue.status.in_(["pending", "processing"]),
)
)
if existing.scalar_one_or_none():
continue
session.add(ProcessingQueue(
document_id=document_id,
stage=next_stage,
status="pending",
))
await enqueue_stage(session, document_id, next_stage)
await session.commit()