배경
현재 인덱싱 Worker 기능을 활성화하면 Worker 등록, Heartbeat와 만료 Lease 복구 Scheduler가 동작한다.
PENDING Job Claim, Attempt 시작, 파싱·청킹, Embedding 저장, 완료·실패와 Lease 갱신 기능도 각각 구현돼 있다.
하지만 실행 중인 Worker가 PENDING Job을 자동으로 Polling하고 이 단계들을 연결하는 실행 Loop가 없다.
지금은 관리자 API를 수동 호출해야 하므로 업로드된 문서가 자동으로 인덱싱 완료까지 진행되지 않는다.
목표
- 등록이 완료된 Worker가 PENDING Job을 주기적으로 Claim
- 설정된 최대 동시 실행 수를 넘지 않도록 실행 슬롯 관리
- Claim → Attempt 시작 → 파싱·청킹 → Embedding → 완료 흐름 자동 연결
- 장시간 외부 I/O 중 현재 Claim의 Lease를 주기적으로 갱신
- 단계 실패를 제한된 실패 유형으로 분류해 기존 Retry·최종 실패 정책으로 전달
- Claim Token 세대가 바뀌거나 Lease를 잃은 실행의 후속 저장·완료 차단
- 정상 종료 시 신규 Claim 중단과 실행 중 작업의 예측 가능한 정리
- 다중 Worker 환경에서도 DB Claim·Lease 계약으로 Job 중복 실행 방지
핵심 설계
1. Polling과 실행 슬롯
indexing.worker.enabled=true일 때만 Poller와 실행 Executor를 생성한다.
- Worker 등록이 완료돼
workerId가 존재할 때만 Polling한다.
- 최대 동시 실행 수를 고정된 설정으로 제한한다.
- Polling 주기마다 사용 가능한 슬롯 수만큼만 Claim을 시도한다.
- 슬롯을 먼저 예약한 뒤 Claim해 동시에 실행할 수 없는 Job을 과도하게 선점하지 않는다.
- Claim할 Job이 없는
Optional.empty()는 오류가 아닌 정상적인 빈 Queue 결과로 처리한다.
- 실제 중복 Claim 방지는 기존 PostgreSQL
FOR UPDATE SKIP LOCKED 계약을 재사용한다.
2. Job 실행 오케스트레이션
한 Claim의 실행 Context는 다음 값으로 고정한다.
- Job ID
- Worker ID
- Claim Token
- Document Version ID
- Embedding Model ID
- Lease 만료 시각
- 시작된 Attempt ID
실행 순서:
- Claim 결과 수신
- 현재 Claim의 Attempt 시작
- Version 상태가
UPLOADED 또는 PARSING이면 파싱·Chunk 저장
- Version 상태가
CHUNKED 또는 EMBEDDING이면 Embedding Set 생성·저장
- 전체 결과가 준비되면 인덱싱 완료
- 성공·실패와 관계없이 로컬 실행 슬롯 반환
기존 단계별 Service의 멱등 재생 계약을 재사용한다. Poller는 Controller를 통한 자기 자신 HTTP 호출 대신
애플리케이션 내부 Service를 오케스트레이션하며, 각 Service가 기존 Transaction과 소유권 검증 경계를
그대로 소유한다.
3. Transaction과 외부 I/O 경계
- Poller 또는 전체 Pipeline 메서드에 하나의 긴 Transaction을 적용하지 않는다.
- Claim, Attempt 시작, 파싱 준비·완료, Embedding 준비·완료, 인덱싱 완료·실패는 기존 짧은 Transaction을 사용한다.
- MinIO 읽기, Text 파싱과 외부 Embedding 호출 중에는 DB 행 잠금을 유지하지 않는다.
- 외부 작업 뒤 저장 단계에서 Worker·Claim Token·Lease와 Snapshot을 다시 검증한다.
- 완료·실패·갱신·복구 경쟁은 기존 Job 우선 잠금 계약으로 직렬화한다.
4. Lease 갱신
- 활성 실행마다 Lease 만료 전 고정 주기로 갱신을 시도한다.
- 갱신 주기는 0보다 크고
lease-duration보다 짧아야 한다.
- 갱신은 기존 Worker·Claim Token·
locked_at을 바꾸지 않는다.
- 실행이 완료·실패하거나 슬롯에서 제거되면 갱신 작업을 즉시 취소한다.
- 갱신 실패로 소유권을 잃으면 해당 실행은 후속 저장·완료를 시도하지 않는다.
- 중단할 수 없는 외부 호출이 이미 진행 중이어도 완료 Transaction의 소유권 재검증이 늦은 결과 저장을 차단한다.
- Claim Token은 일반 로그, Metric Tag와 오류 메시지에 기록하지 않는다.
5. 실패 분류와 보고
Attempt 시작 이후의 실패는 기존 DocumentIndexingFailureService로 전달한다.
| 실패 원인 |
실패 유형 |
| MinIO 읽기·연결 장애 |
STORAGE_UNAVAILABLE |
| 지원하지 않는 형식·빈 문서·Decode 오류 |
DOCUMENT_CONTENT_INVALID |
| Embedding 서버 Timeout·연결 장애 |
EMBEDDING_PROVIDER_UNAVAILABLE |
| Vector 개수·차원·응답 불일치 |
EMBEDDING_RESULT_INVALID |
| Job·Attempt·Version 상태 불일치 |
INDEXING_STATE_INCONSISTENT |
| 분류되지 않은 Worker 내부 오류 |
WORKER_INTERNAL_ERROR |
- 자유 형식 Stack Trace 전체를 실패 메시지나 Event에 저장하지 않는다.
- 오류 분류와 안전한 길이 제한 메시지만 전달한다.
- Attempt 시작 전 실패는 가짜 Attempt를 만들지 않는다.
- 실패 보고 자체가 소유권 상실로 거부되면 상태를 임의 보정하지 않고 Lease 복구 경로에 맡긴다.
Error 계열 JVM 치명 오류를 일반 Retry 실패로 변환하지 않는다.
6. 종료와 실행 Context 정리
- 애플리케이션 종료가 시작되면 신규 Polling과 Claim을 먼저 중단한다.
- 설정된 Grace Period 동안 이미 실행 중인 작업의 완료를 기다린다.
- Grace Period를 넘긴 작업은 로컬 Future와 Lease 갱신을 취소하고 Executor를 종료한다.
- 종료 시 진행 중 Job을 임의로 FAILED 처리하지 않는다. DB 상태는 기존 Lease 만료 복구가 회수한다.
- Worker STOPPED 기록은 신규 Claim 중단 뒤 수행하며 기존 생명주기 종료 계약과 순서를 명확히 한다.
- 슬롯, 실행 Context와 Lease 갱신 Task는 성공·실패·취소 모든 경로에서 정확히 한 번 정리한다.
설정 초안
indexing:
worker:
polling-interval: 1s
max-concurrency: 2
lease-renewal-interval: 1m
shutdown-grace-period: 30s
검증 조건:
- Polling 주기: 양수
- 최대 동시 실행 수: 1 이상
- Lease 갱신 주기: 양수이며 Lease 기간보다 짧음
- 종료 대기 시간: 음수 불가
기존 API 전용 실행의 기본값 indexing.worker.enabled=false는 유지한다.
동시성 및 불변식
- 한 Worker가 Claim한 실행 수는 최대 동시 실행 수를 넘지 않는다.
- 여러 Worker가 동시에 Polling해도 한 Job은 하나의 Claim Token 세대만 가진다.
- 실행 슬롯이 없을 때 새 Job을 Claim하지 않는다.
- Lease 갱신, 완료, 실패와 만료 복구가 경쟁해도 하나의 Job 상태 전이만 Commit된다.
- 과거 Claim Token의 파싱·Embedding 저장·완료·실패는 거부된다.
- Attempt 번호는 Job 행 잠금 안에서 증가한다.
- 실행 종료 후 슬롯과 갱신 Task가 누수되지 않는다.
- 한 Job의 실패가 다른 실행 슬롯의 Job을 중단하지 않는다.
테스트
단위 테스트
- Worker 미등록 시 Polling Skip
- 빈 Queue 처리
- 사용 가능한 슬롯 수만큼 Claim
- 최대 동시 실행 수 초과 방지
- Version 상태별 파싱·Embedding·완료 단계 선택
- 단계별 멱등 재생 흐름
- 실행 중 Lease 갱신과 종료 후 갱신 취소
- 실패 원인별 제한된 실패 유형 매핑
- Attempt 시작 전 실패와 실패 보고 충돌 처리
- 한 Job 실패 뒤 다른 슬롯 계속 실행
- 종료 순서, Grace Period와 슬롯 정리
통합·동시성 테스트
- 실제 PostgreSQL에서 다중 Poller의 중복 Claim 방지
- 최대 동시 실행 수 이하의 Job만 PROCESSING 전환
- Lease 갱신과 복구 경쟁에서 단일 소유권 유지
- 완료·실패·복구 경쟁의 단일 상태 전이
- 소유권을 잃은 외부 작업 결과의 저장 차단
- Retry 가능한 실패의 예약 Queue 복귀
- Retry 소진 시 최종 실패
- Application Context 종료 중 신규 Claim 중단
- 전체 Build와 기존 Claim·Attempt·완료·실패·복구 회귀 테스트
운영 및 보안
- 로그에는 Worker ID, Job ID, Attempt ID, 단계, 결과 분류와 실행 시간만 기록한다.
- Claim Token, Chunk 본문, Vector, 인증 정보와 외부 응답 전문을 기록하지 않는다.
- Polling 빈 결과는 INFO 로그를 반복하지 않고 필요하면 낮은 수준으로 제한한다.
- Job 성공·실패·Skip 수를 집계할 수 있는 구조를 유지하되 Metric Dashboard 도입은 별도 작업으로 분리한다.
- Executor Queue는 무제한으로 늘어나지 않도록 실행 슬롯 수와 같은 경계로 제한한다.
제외 범위
- Message Broker 또는 신규 분산 Lock 도입
- 동적 Worker Auto Scaling
- Claim 직후 로컬 실행 Queue의 영속화
- 프로세스 강제 종료 시 로컬 작업 재개
- Chunk·Embedding 단위 Checkpoint와 부분 재개
- Retry Jitter
- 관리자 수동 Retry·취소 API
- Worker 전용 Machine Credential 및 Principal 바인딩
- Dashboard·Metric·Alert 구축
- 외부 Embedding 서버와 MinIO 자체 구현 변경
완료 조건
관련 구현
배경
현재 인덱싱 Worker 기능을 활성화하면 Worker 등록, Heartbeat와 만료 Lease 복구 Scheduler가 동작한다.
PENDING Job Claim, Attempt 시작, 파싱·청킹, Embedding 저장, 완료·실패와 Lease 갱신 기능도 각각 구현돼 있다.
하지만 실행 중인 Worker가 PENDING Job을 자동으로 Polling하고 이 단계들을 연결하는 실행 Loop가 없다.
지금은 관리자 API를 수동 호출해야 하므로 업로드된 문서가 자동으로 인덱싱 완료까지 진행되지 않는다.
목표
핵심 설계
1. Polling과 실행 슬롯
indexing.worker.enabled=true일 때만 Poller와 실행 Executor를 생성한다.workerId가 존재할 때만 Polling한다.Optional.empty()는 오류가 아닌 정상적인 빈 Queue 결과로 처리한다.FOR UPDATE SKIP LOCKED계약을 재사용한다.2. Job 실행 오케스트레이션
한 Claim의 실행 Context는 다음 값으로 고정한다.
실행 순서:
UPLOADED또는PARSING이면 파싱·Chunk 저장CHUNKED또는EMBEDDING이면 Embedding Set 생성·저장기존 단계별 Service의 멱등 재생 계약을 재사용한다. Poller는 Controller를 통한 자기 자신 HTTP 호출 대신
애플리케이션 내부 Service를 오케스트레이션하며, 각 Service가 기존 Transaction과 소유권 검증 경계를
그대로 소유한다.
3. Transaction과 외부 I/O 경계
4. Lease 갱신
lease-duration보다 짧아야 한다.locked_at을 바꾸지 않는다.5. 실패 분류와 보고
Attempt 시작 이후의 실패는 기존
DocumentIndexingFailureService로 전달한다.STORAGE_UNAVAILABLEDOCUMENT_CONTENT_INVALIDEMBEDDING_PROVIDER_UNAVAILABLEEMBEDDING_RESULT_INVALIDINDEXING_STATE_INCONSISTENTWORKER_INTERNAL_ERRORError계열 JVM 치명 오류를 일반 Retry 실패로 변환하지 않는다.6. 종료와 실행 Context 정리
설정 초안
검증 조건:
기존 API 전용 실행의 기본값
indexing.worker.enabled=false는 유지한다.동시성 및 불변식
테스트
단위 테스트
통합·동시성 테스트
운영 및 보안
제외 범위
완료 조건
docs/design/, 실행 검증 결과는docs/test-results/에 기록한다.관련 구현