feat: PROCESSING 좀비 상태 복구 - 실행 소유권(analysis_run_id) 및 can_retry API 필… - #84
Conversation
|
전체적으로 #52 의도대로 잘 구현된 것 확인했습니다. 다만 머지 전에 한 가지 동시성 케이스만 확인/수정 부탁드립니다. 현재 분석이 이미 5분을 넘긴 상태에서는 아래 순서가 가능해 보입니다.
즉 상태 소유권 덮어쓰기는 막았지만,
이 케이스 테스트도 하나 추가 부탁드립니다. |
|
지적하신 동시성 케이스 반영했습니다. 수정 내용 _finish_success()가 소유권을 잃었을 때(조건부 UPDATE 0건) 조용히 return하던 걸, StaleAnalysisRunError를 던지도록 변경했습니다. python _execute_analysis()도 이 예외를 잡아서, StudyTask 저장은 끝났더라도 최종 완료 처리 시점에 소유권을 잃었다면 tasks를 정상 반환하지 않고 예외를 그대로 전파하도록 수정했습니다. python 테스트 추가 test_ownership_lost_between_save_and_finish_does_not_report_success — _finish_success()를 patch해서, 그 함수가 호출되는 바로 그 시점(StudyTask 저장 커밋 직후, 완료 처리 직전)에 소유권이 다른 실행으로 넘어가는 상황을 재현했습니다. analyze_and_estimate()가 StaleAnalysisRunError를 던지고, analysis_status는 COMPLETED로 갱신되지 않으며, 이미 저장된 StudyTask는 그대로 남아있는 것까지 확인했습니다. 기존 test_finish_success_ignored_when_run_superseded도 새 동작(예외 발생)에 맞게 같이 수정했습니다. 검증 결과 python manage.py test → Ran 252 tests, OK 참고: _finish_failure()는 이번엔 대칭적으로 고치지 않았습니다. 실패 경로에서 소유권을 잃으면 원래 예외(AIAnalysisError 등)가 그대로 "실패"로 전달되는데, 이건 이번에 고친 "거짓 성공" 케이스보다 위험도가 낮다고 판단했습니다. 필요하시면 후속으로 대칭 처리하겠습니다. |
|
추가 수정 확인했습니다. 기존에 말씀드린 다만 머지 전에 한 가지 케이스만 추가로 확인 부탁드립니다. 재시도 횟수를 모두 사용한 상태에서 마지막 실행이 좀비가 되면 FE에서 상태를 구분할 수 없습니다. 현재 정책상 좀비 PROCESSING이어도 다만 예를 들어 2번째 재시도가 시작되어
인 상태에서 서버가 비정상 종료되고 5분이 지나면
이 됩니다. 그런데 현재 그러면 FE에서는
만 받아서 정상적으로 마지막 재시도가 진행 중인 상태와, 이미 5분을 넘긴 좀비인데 재시도 횟수까지 소진된 상태를 구분할 수 없습니다. 이 경우 #52에서 해결하려던 PROCESSING 고착 문제가 최종 재시도에서 다시 발생할 수 있을 것 같습니다.
그러면 FE에서는
처럼 처리할 수 있을 것 같습니다. 이 케이스에 대한 View 응답 테스트도 하나 추가해주시면 좋겠습니다. 그 외 기존 동시성 수정 사항은 확인됐고, 이 부분 반영되면 머지해도 될 것 같습니다. 참고로 병합 차단 사항은 아니지만 |
|
지적하신 두 가지 다 반영했습니다.
get_analysis_status()가 이미 계산하던 값을 응답에 노출만 하면 됐습니다. json 말씀하신 조합대로 FE에서 구분 가능합니다. is_stale=True, can_retry=True → 재시도 가능 정확히 지적하신 시나리오(2번째 재시도가 좀비가 되고 5분 경과)를 재현하는 테스트를 추가했습니다. python
StaleAnalysisRunError가 View의 generic except Exception:에 걸려 "알 수 없는 시스템 오류"로 나오던 부분 — material_analyze(), material_retry_analyze() 둘 다 except StaleAnalysisRunError:를 DuplicateAnalysisRequestError와 같은 급으로 분리해서, "다른 요청이 먼저 이 자료를 처리했습니다" 안내로 바꿨습니다. _finish_failure()의 소유권 상실 시 대칭 처리 — _finish_success()처럼 조용히 넘어가는 대신 StaleAnalysisRunError를 던지도록 변경했고, _execute_analysis()의 실패 경로 4곳(StaleAnalysisRequestError, AIAnalysisError, Exception, tasks 0개) 전부에서 이 예외가 원래 실패 사유보다 우선 전파되도록 했습니다. 관련 테스트도 추가했습니다 (AI 호출 실패와 소유권 상실이 동시에 벌어지는 상황을 재현해서, "네트워크 오류" 같은 낡은 메시지 대신 StaleAnalysisRunError가 전파되는지 확인). 검증 결과 python manage.py test → Ran 256 tests, OK |
|
추가 반영 확인했습니다! 다만 최종으로 한 가지 데이터 정합성 케이스만 보완 부탁드립니다. 현재 _save_tasks_with_estimates()의 transaction에서 StudyTask가 먼저 저장/commit되고, 이후 별도로 _finish_success()가 호출됩니다. 이번 테스트에서도 이 사이에 소유권을 잃으면 StaleAnalysisRunError는 정상 전파되지만 이미 저장된 StudyTask는 그대로 남는 것으로 검증하고 있습니다. 예를 들어 run A가 StudyTask 저장 직후 run B에게 takeover되고, run B가 AI 호출 단계에서 실패해서 저장 단계까지 도달하지 못하면, material의 최종 상태는 FAILED인데 run A가 만든 StudyTask가 그대로 남을 수 있습니다. material_detail에서도 현재 StudyTask를 상태와 무관하게 조회하고 있어서 stale 결과가 노출될 가능성이 있습니다. 따라서 StudyTask 저장과 analysis_status=COMPLETED 최종 전이를 같은 짧은 transaction 안에서 처리해서, 최종 소유권 확인이 실패하면 StudyTask 저장도 같이 rollback되도록 하는 게 가장 안전할 것 같습니다. AI 호출은 이미 transaction 밖이므로 네트워크 호출 동안 DB transaction을 잡는 문제는 없습니다. 추가로 두 가지는 작은 사항입니다. retry_analysis()에서 마지막 재시도 동시 요청 시, 한 요청이 retry_count=2 / PROCESSING을 선점하면 다른 요청이 DuplicateAnalysisRequestError 대신 RetryLimitExceededError를 받을 수 있어 보여서 PROCESSING 상태 판정 순서만 한번 확인 부탁드립니다. 이 부분까지 정리하고 최신 dev 반영 후 전체 테스트 한 번 재확인되면 머지해도 될 것 같습니다. |
|
지적하신 세 가지 다 반영했습니다.
_finish_success()를 별도로 두지 않고, _save_tasks_with_estimates() 안에서 StudyTask 저장(bulk_update) 직후 analysis_status=COMPLETED 전이까지 같은 @transaction.atomic 블록에서 처리하도록 합쳤습니다. python select_for_update()로 잠근 행을 트랜잭션이 끝날 때까지 계속 들고 있기 때문에, 최종 완료 전이 시점에도 소유권이 그대로 보존됩니다. 혹시 실패하더라도 트랜잭션 전체가 롤백되어 StudyTask 저장까지 함께 취소되므로, 말씀하신 "저장은 됐는데 상태만 다른 것으로 바뀌는" 고아 데이터 문제가 해소됩니다. AI 네트워크 호출은 이미 이 함수 밖에서 끝난 뒤라 트랜잭션 동안 DB 커넥션을 오래 점유하는 문제는 없습니다. _finish_success() 함수는 더 이상 필요 없어져서 삭제했고, _execute_analysis()도 그에 맞게 정리했습니다. 테스트 추가: test_studytask_save_and_completion_are_rolled_back_together — bulk_update 직후(같은 트랜잭션 안)에 소유권을 뺏는 상황을 재현해서, StudyTask가 저장되지 않은 채로 롤백되는지 확인했습니다.
단순히 "PROCESSING을 먼저 체크"로 순서만 바꾸면 기존에 잘 동작하던 "좀비이면서 재시도까지 소진된" 케이스가 깨져서(RetryLimitExceededError가 나와야 하는데 DuplicateAnalysisRequestError로 잘못 나옴), is_stale 여부까지 같이 확인하도록 조정했습니다. python 테스트 추가: test_retry_race_loser_gets_duplicate_request_not_retry_limit_exceeded — 마지막 재시도 슬롯을 두 요청이 동시에 노리는 경쟁 상황을 재현했습니다. 기존 test_retry_rejected_when_retry_count_maxed_even_if_zombie(진짜 좀비+소진 케이스)도 여전히 통과합니다. |
|
최신 커밋 다시 확인했습니다. StudyTask 저장과 완료 전이 원자성, 마지막 재시도 동시 요청 처리까지 기존에 말씀드린 핵심 부분은 잘 반영된 것 같습니다.
현재 PR 브랜치가 최신 python manage.py check
python manage.py makemigrations --check
python manage.py test여기까지 통과하면 제 기준에서는 #84 머지해도 될 것 같습니다. |
close #52
작업 내용
exams/models.py
StudyMaterial에 analysis_started_at(PROCESSING 진입 시각), analysis_run_id(실행 식별자 UUID) 필드 추가
exams/services/analysis_orchestrator.py
PROCESSING_TIMEOUT_SECONDS = 300(5분) 기준으로 좀비 PROCESSING 판정, retry_analysis()에서만 구제 허용 (정책 단순화)
재시도 횟수 제한을 FAILED/좀비 양쪽에 동일하게 적용 (좀비라고 우회 불가)
analysis_started_at이 NULL인 경우(필드 추가 이전부터 PROCESSING이었던 데이터)도 좀비로 취급
analysis_run_id로 실행 소유권 관리 — 저장 직전 소유권 재확인(StaleAnalysisRunError), _finish_success/_finish_failure도 조건부 UPDATE로 변경해 뒤늦은 실행이 최신 상태를 덮어쓰지 못하게 방지
get_analysis_status()에 is_stale/can_retry/retry_after_seconds 추가
exams/views.py
material_analysis_status 응답에 can_retry/retry_after_seconds 포함
마이그레이션: exams/migrations/0003_studymaterial_analysis_run_id_and_more.py
exams/tests.py
ProcessingTimeoutTestCase(22개), MaterialAnalysisViewTestCase에 5개 추가 (총 27개)
검증 결과
python manage.py test exams.tests.ProcessingTimeoutTestCase → Ran 22 tests, OK
python manage.py test exams.tests.MaterialAnalysisViewTestCase → Ran 25 tests, OK
python manage.py test → Ran 240 tests, OK
manage.py check / makemigrations --check → 이상 없음