작업이 실패한 위치와 몇 번째 시도인지를 함께 남긴다
최종 작업 상태만 FAILED로 남으면 렌더가 실패했는지, 렌더를 끝내고 저장에서 실패했는지 구분하기 어렵다. 재시도가 같은 작업 ID를 사용하면 단계 이름까지 같아도 서로 다른 시도의 기록이다. 단계의 소속을 작업 ID와 시도 번호로 묶어야 완료한 단계와 다시 시작한 단계를 해석할 수 있다.
AI Commons의 작업 로그에 단계 이벤트를 추가하고, 후속 수정으로 종료 경합을 보완했다. 제품 통합 전의 기능 구현이다. 프로세서가 실행 전체를 AsyncLocalStorage로 감싸 domain, jobId, jobType, attempt를 전달한다. 시도 번호는 attemptsMade + 1이며 안쪽 서비스는 인자를 계속 전달하지 않고 현재 컨텍스트에서 읽는다.
단계는 JobEvent의 STEP 행으로 저장한다. stepKey가 실행 위치, stepStatus가 단계 상태, endedAt이 닫힘 여부, detailJson이 진행 상세를 나타낸다. 컨텍스트 밖에서는 DB를 호출하지 않는다. 상세 API는 단계와 일반 이벤트를 나누므로 STEP이 STALLED 횟수에 섞이지 않고, 처리 시작 시각도 현재 시도의 단계에서 구한다.
한 시도에는 한 단계만 열어 두는 순차 모델
start()는 같은 도메인·작업·시도 안에 동일한 열린 단계가 있으면 그 행을 재사용한다. 다른 열린 단계가 있으면 먼저 COMPLETED로 닫는다. 이 규칙은 다음 단계 진입을 앞 단계 종료로 보는 순차 처리에 맞는다. 병렬 작업의 모든 자식 실행을 독립적으로 추적하는 span 트리로 사용할 수는 없다.
다음은 기록의 의미를 설명하는 흐름이며 실제 실행에서 수집한 로그가 아니다.
- 시도 1: RENDER 완료 → SAVE 실행 중 실패완료한 RENDER는 유지하고 열린 SAVE만 FAILED로 닫는다.
- 시도 1의 최종 JobLog 기록단계 닫기를 먼저 기다린 뒤 작업 실패를 기록한다.
- 시도 2: RENDER와 SAVE 완료같은 jobId라도 attempt가 달라 첫 시도의 실패와 구분된다.
프로세서의 성공 경로는 열린 단계를 COMPLETED로 닫고 최종 JobLog를 쓴다. 오류·대상 삭제 경로는 FAILED로 닫은 뒤 종료 기록을 쓴다. 실행 시간 상한으로 이미 회수된 작업의 늦은 완료도 FAILED로 닫는다. 함수가 반환됐다는 이유만으로 버린 결과를 성공 단계로 남기면 최종 작업 실패와 단계 성공이 서로 다른 의미를 갖게 되기 때문이다.
타이머 취소와 DB 갱신 조건을 함께 둔다
진행 상세를 매번 저장하면 작은 진척에도 쓰기가 발생한다. 기록기는 첫 진행을 바로 쓰고, 이후 1000ms 안의 호출은 최신 상세로 합쳐 후행 갱신한다. 따라서 중간 상세는 생략될 수 있다. 종료할 때 명시한 상세는 대기 중인 상세보다 우선하며, 종료·다음 단계 시작·일괄 닫기에서 해당 타이머를 취소한다.
타이머 취소만으로 이미 시작된 비동기 쓰기를 되돌릴 수는 없다. 후속 수정 42973283a는 진행 상세를 읽은 행 ID에 무조건 쓰는 대신, 쓰기 순간에도 열린 행만 갱신하도록 바꿨다. 다음은 실제 조건을 줄인 코드다.
await prisma.jobEvent.updateMany({
where: { ...scope(ctx), stepKey, endedAt: null },
data: { detailJson: detail },
}); | 동작 | 보호하는 범위 |
|---|---|
| DB 정리 전에 타이머 취소 | DB 정리가 실패해도 예약한 진행 쓰기가 계속 남지 않게 한다. |
| progress의 endedAt: null | 종료가 먼저 적용된 행은 후행 상세 갱신의 대상에서 빠진다. |
| end의 endedAt: null | 열린 행 조회 뒤 다른 경로가 닫았으면 기존 종료 상태를 덮지 않는다. |
각 DB 쓰기는 여전히 await하며 별도 대기 상한은 없다. DB 오류와 경고 로깅 오류를 흡수해 본 작업의 오류로 전파하지 않는 대신 진단 기록을 잃을 수 있다. 프로세스가 갑자기 종료되면 프로세서의 닫기 코드도 실행되지 않는다. 열린 행 조회와 재사용은 애플리케이션 규칙이며 여러 프로세스의 동시 쓰기가 정확히 한 번만 발생한다는 보장은 아니다.
행의 상태와 API의 의미를 함께 검증한다
기록기 단위 테스트는 컨텍스트 부재, 열린 단계 재사용, 최신 상세의 디바운스, 종료 시 타이머 취소와 명시 상세 우선순위, DB 실패, 닫힌 행을 건드리지 않는 갱신 조건을 다룬다. 프로세서 표 기반 테스트는 일곱 처리기의 일반 오류·대상 삭제 경로에서 단계 닫기가 최종 JobLog보다 먼저 호출되는지 확인한다.
PostgreSQL 통합 테스트는 테스트 컨테이너에 실제 행을 넣어 단계 순서와 종료 시각, 실패 후 앞선 완료 단계 유지, 시도 1·2의 분리, API의 steps/events 분리와 현재 시도의 시작 시각을 확인하도록 구성돼 있다. 접근할 수 없는 DB 주소에서도 기록기 호출이 거절되지 않는 사례가 있다. 이 검증은 DB 조건과 조회 계약을 다루며, 모든 실제 워커가 강제 종료 전에 단계를 닫는다는 운영 보장은 아니다.
AI Commons의 단계 기록에는 단계 이름만큼 종료 규칙이 중요하다. 기록 수를 늘리는 것보다 어느 시도의 열린 상태인지, 최종 상태를 언제 확정했는지를 일관되게 읽을 수 있어야 한다.