Skip to content

[Feat] 인덱싱 실패 종료 및 지연 재시도 #88

Description

@Gimini-3

배경

문서 인덱싱 성공 경로는 Attempt·Job·Version·Document를 원자적으로 INDEXED로 전환한다. 반면 파싱, Embedding 생성 또는 완료 단계에서 오류가 발생하면 Attempt와 Job이 STARTED/PROCESSING에 남으며, 이미 준비된 retry_count, max_retry_count, 오류 필드와 실패·재시도 이벤트가 실제 전이에 사용되지 않는다.

현재 Claim을 소유한 Worker가 실패를 명시적으로 보고하면 Attempt를 종료하고, 서버가 실패 유형과 남은 횟수를 기준으로 지연 재시도 또는 최종 실패를 결정한다.

목표

  • Worker·Claim Token·Lease·Attempt를 검증하는 실패 종료 API 제공
  • Attempt를 오류 원인과 함께 FAILED로 종료
  • 재시도 가능한 실패는 Job을 실행 가능 시각이 지정된 PENDING으로 복귀
  • 영구 실패 또는 재시도 소진 시 Job·Version을 최종 FAILED로 종료
  • 새 Version 실패 시 기존 검색 가능한 current Version과 Document의 INDEXED 상태 유지
  • 검색 가능한 Version이 없는 최종 실패 시 Document를 FAILED로 전환
  • 동일 실패 요청의 멱등 재생과 완료 요청과의 동시성 안전성 보장
  • 실패·재시도 상태와 이벤트를 하나의 Transaction으로 저장

API 초안

POST /admin/indexing-jobs/{jobId}/attempts/{attemptId}/fail
{
  "workerId": 7,
  "claimToken": "34c19d16-6ae1-4f6a-a35d-0123456789ab",
  "failureType": "EMBEDDING_PROVIDER_UNAVAILABLE",
  "errorMessage": "Embedding provider request timed out"
}

Worker가 retryable을 직접 결정하지 않고 서버가 제한된 failureType별 정책으로 판정한다.

failureType 재시도
STORAGE_UNAVAILABLE 가능
DOCUMENT_CONTENT_INVALID 불가
EMBEDDING_PROVIDER_UNAVAILABLE 가능
EMBEDDING_RESULT_INVALID 불가
INDEXING_STATE_INCONSISTENT 불가
WORKER_INTERNAL_ERROR 가능

최초 처리와 동일 요청 재전송은 모두 200 OK를 반환한다. 응답은 이후 Job 변경과 무관하게 재생할 수 있도록 Attempt의 고정된 실패 결과만 포함한다.

상태 전이

재시도

  • Attempt: STARTED -> FAILED
  • Job: PROCESSING -> PENDING
  • Version: 현재 처리 단계 유지
  • Document: 변경 없음
  • retry_count 증가 및 next_retry_at 계산
  • Worker·Claim Token·Lease 해제
  • 최근 오류 Snapshot 저장
  • 단계별 실패 이벤트와 RETRY 이벤트 저장

Version 상태를 유지해 다음 Claim이 기존 멱등 처리 규칙으로 재개한다.

  • PARSING + Chunk 없음: 파싱 재개
  • EMBEDDING + Embedding 없음: Embedding 재실행
  • EMBEDDING + 전체 Embedding 존재: 저장 결과 재생 후 완료

최종 실패

  • Attempt: STARTED -> FAILED
  • Job: PROCESSING -> FAILED
  • Version: 현재 처리 단계 -> FAILED
  • 검색 가능한 current Version이 없으면 Document -> FAILED
  • 기존 current Version이 INDEXED이면 Document와 포인터 유지
  • 실패 Version의 ACTIVE Embedding은 STALE 처리
  • 단계별 실패 이벤트와 FAILED 이벤트 저장

재시도 정책

max_retry_count = 3은 최초 실행 이후 최대 3회 재시도로 정의하며 최대 Attempt 수는 4회다.

  • 첫 재시도: 10초
  • 두 번째 재시도: 20초
  • 세 번째 재시도: 40초
  • 최대 지연: 5분

embedding_jobs.next_retry_at을 추가하고 Claim 후보를 다음과 같이 제한한다.

status = 'PENDING'
AND (next_retry_at IS NULL OR next_retry_at <= :claimedAt)

별도 Scheduler 없이 기존 Claim API가 실행 가능 시각이 지난 Job을 선택한다.

Transaction 및 잠금

  1. Job 쓰기 잠금
  2. 기존 FAILED Attempt 멱등 재생 여부 확인
  3. Worker·Claim Token·Lease 검증
  4. Attempt 실행 Context 검증
  5. Version 쓰기 잠금
  6. Document 쓰기 잠금
  7. 재시도 또는 최종 실패 상태 전이
  8. Embedding 상태 정리와 이벤트 저장
  9. Commit

완료 흐름과 동일하게 Job -> Version -> Document 잠금 순서를 사용한다. 완료와 실패가 경쟁하면 먼저 Job 잠금을 획득해 커밋한 한쪽만 성공한다.

멱등성

이미 FAILED인 동일 Attempt에 같은 Worker·Claim Token·실패 유형·오류 메시지로 재요청하면 저장된 실패 결과를 반환한다.

재생 시 Retry 횟수와 실행 시각을 다시 계산하거나 상태·이벤트·실패 시각을 변경하지 않는다. 동일 Attempt를 다른 실패 내용으로 종료하려 하면 409 Conflict로 거부한다.

제외 범위

  • Lease 연장 API
  • 만료 Lease 자동 회수
  • DEAD Worker Job 복구
  • Worker 자동 Polling Loop
  • 관리자 수동 재시도·취소 API
  • 부분 Chunk·Embedding 삭제 도구
  • Retry Jitter와 실패 Dashboard

Lease가 이미 만료됐거나 Worker가 죽어 실패 API를 호출하지 못한 Job의 복구는 후속 작업에서 다룬다.

완료 조건

  • 유효한 현재 Attempt만 실패를 보고할 수 있다.
  • 재시도 가능한 오류는 Job을 지연 PENDING으로 복귀시킨다.
  • 예약 시각 이전 Job은 Claim되지 않고 이후에는 Claim된다.
  • 재Claim은 새로운 Claim Token과 Attempt 번호를 사용한다.
  • 영구 오류 또는 Retry 소진은 Job·Version을 최종 실패 처리한다.
  • 새 Version 실패 시 기존 검색 결과와 current Version이 유지된다.
  • 검색 가능한 Version이 없는 최종 실패 문서는 검색 대상에서 제외된다.
  • 최종 실패 Version의 ACTIVE Embedding은 검색에 사용되지 않는다.
  • 동일 실패 재요청이 Retry 횟수와 이벤트를 중복 생성하지 않는다.
  • 완료와 실패의 동시 요청에서 부분 상태가 남지 않는다.
  • Transaction 실패 시 모든 변경이 Rollback된다.
  • 실제 PostgreSQL에서 실행 가능 시각과 잠금 경쟁을 검증한다.
  • 전체 테스트가 통과한다.

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