Skip to content

[Fix] RagJobWorker의 정상 완료가 RagJobTimeoutSweeper의 타임아웃 확정을 덮어쓸 수 있는 문제 수정 #288

Description

@kangcheolung

배경

#286(RAG 답변 생성이 PROCESSING 상태로 무기한 대기하는 문제 수정)에서 RagJobTimeoutSweeper
추가하면서, "스위퍼 → Worker" 방향의 경합만 조건부 UPDATE(forceFailIfProcessing,
WHERE id=? AND status='PROCESSING')로 막았다. Worker가 이미 정상 완료한 job을 스위퍼가
뒤늦게 덮어쓰지 못하도록 하는 안전장치다.

반대 방향("Worker → 스위퍼", 즉 스위퍼가 먼저 타임아웃 처리한 job을 Worker가 뒤늦게 정상
완료 처리로 덮어쓰는 경우)은 막혀 있지 않다. RagResponseCommandService.completeSuccess()/
completeFailed()는 여전히 일반 dirty-checking UPDATE라 조건이 전혀 없다.

(#286 PR #287에 대한 CodeRabbit 리뷰에서 지적됨)

문제 시나리오

t=0s    job 생성, PROCESSING. 큐에 밀려있어 Worker가 아직 못 집음
t=85s   Worker가 이 job을 집어 처리 시작 (Ollama 호출, 오래 걸림)
t=90s   RagJobTimeoutSweeper가 "90초 지남" 판단 → forceFailIfProcessing()으로
        FAILED + fallback 답변 커밋 성공 (이 시점 status는 아직 PROCESSING이라 조건 통과)
        → WebSocket 알림 → 프론트는 폴링/소켓을 닫고 fallback 답변 표시, 대기 종료
t=115s  Worker의 Ollama 호출이 뒤늦게 완료됨
        → completeSuccess() 호출, 조건 없이 그냥 UPDATE
        → DB의 FAILED 상태를 SUCCESS + 진짜 답변으로 무조건 덮어씀
        → RagJobWorker가 notifyAnswerReady()를 또 호출하지만, 프론트는 이미
          t=90s에 구독을 닫아서 아무도 받지 못함

결과:

  • DB엔 최종적으로 "정상 성공"이 남지만, 사용자는 실제로 fallback 답변을 보고 끝났다 —
    "무슨 일이 실제로 있었는지"에 대한 기록 자체가 실제 사용자 경험과 어긋난다(감사/분석 시
    잘못된 그림을 준다).
  • GPU/Worker가 1개뿐인데, 이미 아무도 안 볼 답을 계산하느라 그 시간만큼 뒤에 대기 중인
    다른 job의 처리가 늦어진다 — #286이 해결하려던 큐 적체 문제를 스스로 더 악화시키는 셈.

수정 방향

completeSuccess()/completeFailed()forceFailIfProcessing()과 동일한 패턴으로
WHERE status='PROCESSING' 조건부 UPDATE로 바꾼다.

  • RagResponseRepository: completeSuccessIfProcessing(...) 신규 추가(성공 시 채우는
    필드 전용). completeFailed() 쪽은 이미 있는 forceFailIfProcessing()을 그대로 재사용
    가능(같은 SQL 형태).
  • RagResponseCommandService.completeSuccess()/completeFailed(): 반환 타입을
    voidboolean(영향받은 행 > 0)으로 변경.
  • RagFacade.processJob(): 반환값을 확인해 0건이면(=스위퍼가 이미 먼저 확정함) citation
    저장을 스킵하고 반환값(boolean)을 그대로 상위로 전파.
  • RagJobWorker.processNext(): processJob()의 반환값이 false면 notifyAnswerReady()
    호출하지 않는다(이미 스위퍼가 알림을 보냈으므로 중복/불필요한 알림 방지).

영향 범위

RagResponseRepository, RagResponseCommandService, RagFacade, RagJobWorker와 이
네 곳에 딸린 기존 테스트(RagResponseCommandServiceTest, RagFacadeTest,
RagJobWorkerTest) 전부 — #286 이전부터 있던 정상 완료 경로를 건드리는 리팩터링이라
#286과 분리해서 진행한다.

Acceptance Criteria

  • completeSuccess/completeFailed가 조건부 UPDATE로 동작하며, 이미 FAILED로
    확정된 job에 대해서는 아무것도 덮어쓰지 않는다(영향받은 행 0건)
  • 위 케이스에서 Worker가 중복 WebSocket 알림을 보내지 않는다
  • "스위퍼가 먼저 확정한 뒤 Worker가 뒤늦게 완료 시도"하는 경합을 재현하는 테스트 추가
    (통합 또는 @DataJpaTest 수준)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions