본문으로 건너뛰기

OMR Report Job Lifecycle

OMR grading 또는 report generation이 이미 request path를 떠났고, 실패 지점이 backend job state, callback counting, artifact finalization일 때 이 가이드를 사용합니다.

worker CPU scaling 문제와는 다릅니다. worker가 단순히 느리거나 작다면 OMR Grading Scaling에서 시작합니다.

증상

  • worker callback이 올 것으로 기대했는데 OMR 또는 report job이 PENDING 또는 PROCESSING에 머뭅니다.
  • job이 terminal state에 도달했지만 success/fail counter 합이 요청 item count와 맞지 않습니다.
  • duplicate callback은 skip되는데 job이 finalize되지 않습니다.
  • 개별 PDF는 있는데 report ZIP download가 not found를 반환합니다.
  • retry 또는 late callback이 terminal job을 변경합니다.

알려진 실패 이력

Commit실패 모드지속되는 수정
2b20846ejob row는 persist됐지만 commit과 broker send 사이에서 RabbitMQ publish가 유실될 수 있었습니다.JobRequestForwarder를 통한 Spring Modulith transactional outbox.
bde8f625concurrent callback이 optimistic lock에 걸렸고, 실패한 transaction 뒤에도 dedup key가 남을 수 있었습니다.callback update를 retry하고 uncommitted failure 후 dedup key를 해제합니다.
5023d873OMR result가 per-job lifecycle과 충분히 강하게 연결되지 않았고, job versioning 누락이 callback persistence를 깨뜨릴 수 있었습니다.V40 versioning, V41 result-to-job linkage, V42 lookup index, status endpoint를 추가했습니다.
4820197fworker loss 뒤 stale async job이 계속 열린 상태로 남을 수 있었습니다.ExamJobTimeoutSweeper가 오래된 PENDING/PROCESSING OMR 및 report job을 FAILED로 표시합니다.
a98cc026batch report data가 chunked job에 잘못된 context를 사용할 수 있었습니다.ReportBatchDataQueryService가 job, exam, tenant, student chunk membership을 검증합니다.

현재 런타임 계약

표면소스 경로필요한 동작
Job creationResultCommandService.confirmBatchOmrGrading(...), ReportJobService.createBatchJob(...)job과 request event를 같은 transaction에 persist합니다.
Outbox forwardingmodules/exam/src/main/java/com/lumie/exam/adapter/out/messaging/JobRequestForwarder.javacommitted outbox event를 RabbitMQ로 전달하고 restart 때 outstanding publication을 retry합니다.
OMR callbackOmrGradingJobProcessor.processGradingCallback(...)job/image 단위로 deduplicate하고 optimistic lock을 retry하며 counter가 total에 도달할 때만 finalize합니다.
Report callbackReportJobService.processCallback(...)job/report 단위로 deduplicate하고 PDF artifact를 검증하며 완료 시 ZIP을 생성합니다.
Timeout sweepExamJobTimeoutSweeper.sweepStaleJobs(...)넉넉한 timeout 뒤 stale open job을 fail시켜 polling이 종료되고 사용자가 retry할 수 있게 합니다.
Batch dataReportBatchDataQueryService.getBatchData(...)job ownership, exam id, tenant, chunk membership을 검증합니다.

outbox retry 동작은 app/src/main/resources/application.yamlspring.modulith.events.republish-outstanding-events-on-restart=truecompletion-mode=delete로 설정됩니다.

진단

  1. job이 OMR grading인지 report generation인지 구분합니다.
  2. job row의 status, total count, processed count, success count, fail count를 읽습니다.
  3. backend log에서 Background job enqueued, enqueue_failed, duplicate callback skipped, optimistic lock retry, terminal-state log를 확인합니다.
  4. worker message가 전송되지 않았다면 RabbitMQ를 의심하기 전에 Spring Modulith event_publication 상태를 봅니다.
  5. callback은 도착했지만 counter가 맞지 않으면 dedup key와 optimistic lock retry log를 확인합니다.
  6. worker가 사라졌다면 ExamJobTimeoutSweeper가 timeout window 뒤 stale job을 FAILED로 표시했는지 확인합니다.
  7. report file은 있는데 ZIP download가 실패하면 report fileKey validation과 ZIP zipFileKey를 확인합니다.
  8. batch report data가 일부 subset에서 틀리면 jobId, examId, studentIds, tenant slug가 모두 같은 job에 속하는지 검증합니다.

유용한 소스 확인:

cd lumie-backend
rg -n "event_publication|tryFinalize|markIfAbsent|finalizeIfDone|zipFileKey|close.*stale|existsByIdAndStatusIn" \
modules/exam app/src/main/resources

수정

  • job row는 있지만 worker request가 전송되지 않았다면 outbox contract를 유지하고 outstanding event replay를 사용합니다. 임의 RabbitMQ message를 수동 publish하지 않습니다.
  • uncommitted failure 뒤 dedup key가 progress를 막았다면 dedup key를 해제하고 callback retry path가 다시 실행되게 합니다.
  • counter가 total보다 낮고 더 이상 callback이 오지 않는다면 job을 PROCESSING에 두지 말고 stale-job close path를 사용합니다.
  • terminal completion 뒤 report ZIP 생성이 실패했다면 ReportJobService.getZipBytes(...) 경로로 검증된 per-student PDF에서 ZIP을 다시 만듭니다.
  • batch report data가 잘못된 chunk를 쓰고 있다면 query를 넓히지 말고 jobId/studentIds request path를 고칩니다.
  • callback processing이 missing column 또는 result-to-job lookup 오류로 실패하면 Java code를 바꾸기 전에 V40, V41, V42가 순서대로 적용됐는지 확인합니다.

검증 증거

  • job status가 COMPLETED 또는 FAILED에 도달하고 counter가 total과 일치합니다.
  • 해당 job의 outbox publication이 complete 상태입니다.
  • event_publication에 해당 job request의 stuck publication이 없습니다.
  • duplicate callback이 counter를 두 번 증가시키지 않습니다.
  • late callback이 terminal job을 변경하지 않습니다.
  • finalization 후 report ZIP download가 성공합니다.
  • tenant/job/exam/student validation이 mismatched batch data request를 거부합니다.

예방

  • job persistence와 enqueue intent를 outbox로 atomic하게 유지합니다.
  • worker callback은 item index 단위로 idempotent해야 합니다.
  • storage I/O는 긴 database transaction 밖에 두되 counter persist는 짧은 transaction에서 수행합니다.
  • concurrency protection을 대체하지 않고 optimistic-lock retry를 제거하지 않습니다.
  • job state, callback dedup, artifact creation을 별개 기능이 아니라 하나의 lifecycle로 취급합니다.

관련 페이지