Skip to content

refactor: 비정상 종료 시 PROCESSING 상태가 장시간 유지되는 문제 개선 - #53

Open
6ye0m wants to merge 1 commit into
devfrom
52-refactor-processing
Open

refactor: 비정상 종료 시 PROCESSING 상태가 장시간 유지되는 문제 개선#53
6ye0m wants to merge 1 commit into
devfrom
52-refactor-processing

Conversation

@6ye0m

@6ye0m 6ye0m commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

close #52

작업 내용

exams/models.py
StudyMaterial에 analysis_started_at(PROCESSING 진입 시각) 필드 추가
exams/services/analysis_orchestrator.py
PROCESSING_TIMEOUT_SECONDS = 300(5분) 상수 추가
_start_processing()이 기존 조건(PENDING / FAILED+재시도횟수남음) 외에도, "PROCESSING 상태로 5분 이상 경과한 좀비 상태"를 감지해서 최초 분석·재시도 양쪽 경로 모두에서 구제 가능하도록 조건 확장 (Q 객체로 OR 조건 처리)
PROCESSING 진입 시마다 analysis_started_at 갱신
get_analysis_status()에 is_stale(bool) 필드 추가 — 화면에서 "너무 오래 걸림" 표시/재시도 버튼 노출 여부 판단에 사용 가능
마이그레이션: exams/migrations/0003_studymaterial_analysis_started_at.py
exams/tests.py
ProcessingTimeoutTestCase 신규 추가 (7개)
PROCESSING 시작 시 시각 기록 확인
신선한(5분 안 지난) PROCESSING은 여전히 중복 요청 거부
좀비 PROCESSING은 최초분석/재시도 양쪽 경로로 구제 가능, 재시도 횟수도 정상 증가
is_stale 판정 3가지 케이스(좀비/신선/비-PROCESSING) 검증

검증 결과

python manage.py test exams.tests.ProcessingTimeoutTestCase → Ran 7 tests, OK
python manage.py test → Ran 158 tests, OK
manage.py check / makemigrations --check → 이상 없음

@wngjs8114

Copy link
Copy Markdown
Collaborator

비정상 종료로 PROCESSING 상태가 영구적으로 유지되는 문제를 인식하고, analysis_started_at과 조건부 UPDATE를 이용해 복구하려는 방향은 좋습니다.

다만 현재 구현은 timeout이 지난 기존 요청을 실제로 무효화하지 않기 때문에, 오래 걸리는 기존 요청과 새 요청이 동시에 실행되면서 상태와 StudyTask를 서로 덮어쓸 수 있습니다. 머지 전에 아래 사항을 보완해야 할 것 같습니다.

1. 각 분석 실행을 구분할 수 있는 실행 식별자가 필요합니다

현재는 analysis_started_at이 5분 이상 지났으면 새 요청이 PROCESSING 상태를 다시 차지하지만, 기존 요청은 계속 실행될 수 있습니다.

예를 들면 다음 상황이 가능합니다.

요청 A 분석 시작
→ 5분 경과
→ 요청 B가 stale 상태를 인수하고 분석 시작
→ B가 먼저 완료해 작업 저장
→ A가 뒤늦게 완료해 B의 작업과 상태를 다시 덮어씀

현재 _finish_success()_finish_failure()는 해당 요청이 여전히 최신 실행인지 확인하지 않고 상태를 저장합니다. StudyTask 저장 단계도 동일합니다.

analysis_run_id와 같은 UUID 필드를 추가해 실행 소유권을 구분하는 방식을 권장합니다.

analysis_run_id = models.UUIDField(null=True, blank=True)

처리 흐름은 다음처럼 구성하면 좋을 것 같습니다.

run_id = uuid.uuid4()

# 시작 시 PROCESSING + run_id 저장
# AI 호출
# 작업 저장 전 DB의 run_id가 현재 run_id와 같은지 확인
# 성공/실패 상태 변경도 run_id가 같은 경우에만 허용

기존 요청의 run_id가 현재 DB 값과 다르면 해당 결과는 저장하지 않고 폐기해야 합니다.

2. stale 재시도에도 최대 재시도 횟수를 적용해주세요

현재 재시도 eligibility는 아래와 같습니다.

Q(
    analysis_status=MaterialStatus.FAILED,
    analysis_retry_count__lt=MAX_RETRY_COUNT,
) | is_zombie_processing

이 구조에서는 stale PROCESSING이면 analysis_retry_count가 이미 최대값이어도 전이에 성공하고, F("analysis_retry_count") + 1로 값이 3 이상이 될 수 있습니다.

재시도 횟수 조건을 전체 조건에 적용해야 합니다.

eligible = (
    Q(analysis_retry_count__lt=MAX_RETRY_COUNT)
    & (
        Q(analysis_status=MaterialStatus.FAILED)
        | is_zombie_processing
    )
)

관련해서 analysis_retry_count=MAX_RETRY_COUNT인 stale PROCESSING이 거부되는 테스트도 추가해주세요.

3. 최초 분석과 재시도 양쪽에서 stale 상태를 구제하지 않는 것이 좋겠습니다

현재 stale PROCESSING은 아래 두 함수에서 모두 다시 시작할 수 있습니다.

analyze_and_estimate()
retry_analysis()

하지만 최초 분석 경로에서는 analysis_retry_count가 증가하지 않고, 재시도 경로에서는 증가합니다. 동일한 stale 상태인데 어떤 엔드포인트를 호출했는지에 따라 횟수 정책이 달라집니다.

정책을 아래처럼 단순화하는 편이 명확해 보입니다.

analyze_and_estimate:
PENDING만 허용

retry_analysis:
FAILED 또는 stale PROCESSING 허용
단, retry_count < MAX_RETRY_COUNT

화면에서 is_stale=True일 때도 기존 재시도 엔드포인트를 사용하면 됩니다.

4. 기존 PROCESSING 데이터의 analysis_started_at=NULL을 처리해주세요

이번 마이그레이션은 필드를 nullable로 추가하므로, 배포 전에 이미 PROCESSING이었던 자료는 다음 상태가 됩니다.

analysis_status = PROCESSING
analysis_started_at = NULL

현재 stale 조건은 analysis_started_at__lt=stale_cutoff만 사용하고, get_analysis_status()도 시작 시각이 None이면 is_stale=False를 반환합니다.

따라서 이런 기존 자료는 여전히 영구적으로 PROCESSING에 남습니다.

아래 중 하나가 필요합니다.

1. 데이터 마이그레이션으로 기존 PROCESSING을 FAILED로 전환
2. PROCESSING이면서 analysis_started_at이 NULL이면 stale로 판정

해당 케이스에 대한 테스트도 추가해주세요.

5. PR #50을 먼저 반영한 뒤 rebase가 필요합니다

현재 PR #53의 analysis_orchestrator.py는 아직 analyze_study_material()과 외부 transaction.atomic을 사용하는 기존 구조입니다.

PR #50에서는 이미 이 부분을 아래 구조로 변경하고 있습니다.

fetch_extracted_tasks()  # AI 호출, 트랜잭션 없음
save_extracted_tasks()   # DB 저장, 짧은 트랜잭션

두 PR이 같은 실행 흐름을 크게 수정하므로 다음 순서가 안전합니다.

PR #50 수정 및 머지
→ PR #53을 최신 dev로 rebase
→ 새 fetch/save 구조에 timeout과 실행 식별자 적용
→ 전체 테스트 재실행

추가 권장 테스트

- retry_count가 최대값인 stale PROCESSING은 재시도 불가
- analysis_started_at이 NULL인 PROCESSING 복구
- 새 요청이 takeover한 뒤 기존 요청의 성공 처리 무시
- 새 요청이 takeover한 뒤 기존 요청의 실패 처리 무시
- 기존 요청이 새 요청의 StudyTask를 덮어쓰지 못함
- PR #50 구조에서도 AI 호출 시 transaction이 열리지 않음

analysis_started_at을 추가하고 stale 여부를 화면에 전달하는 부분 자체는 좋습니다. 다만 timeout을 기준으로 새 실행을 허용하려면, 이전 실행이 이후 결과를 저장하지 못하게 하는 실행 소유권 검증이 반드시 함께 들어가야 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 비정상 종료 시 PROCESSING 상태가 장시간 유지되는 문제 개선

2 participants