ES /docs

MaskWork::maskPanos | script Premature end of JPEG file

RCA: MaskWork::maskPanos | script Premature end of JPEG file

Error Log#

Datadog Logs

text
MaskWork::maskPanos | script Premature end of JPEG file

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: hassan-allam
  • 발생 횟수: 1
  • 최초 발생: 2026-04-14T12:36:45.657Z
  • 최근 발생: 2026-04-14T12:36:45.657Z

Root Cause Summary#

S3에서 pano 이미지를 다운로드하는 중 ECONNRESET 네트워크 오류가 발생하여 JPEG 파일이 불완전하게(truncated) 저장되었습니다. AwsS3Manager.downloadParallel의 에러 핸들러(aws-s3.manager.ts:75-77)가 다운로드 실패를 로그만 남기고 catch한 뒤 예외를 다시 throw하지 않기 때문에, 불완전한 파일이 그대로 다음 단계인 face-body-detector 스크립트(maskPanos)에 전달되었습니다. Python의 이미지 라이브러리(libjpeg)가 truncated JPEG를 읽으려고 시도하면서 "Premature end of JPEG file" 경고를 stderr로 출력했고, 이것이 MaskWork.maskPanos의 stderr 핸들러(mask-work.ts:40-42)를 통해 error 레벨로 로깅되었습니다.

Technical Analysis#

Code Path#

  1. Entry point: pano-postprocessor-service.ts:46PanoPostprocessorService.init()run()을 호출합니다.

  2. Pano 다운로드: pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos)가 S3에서 pano 이미지를 병렬 다운로드합니다.

typescript
// aws-s3.manager.ts:60-79
const tasks = cpPano.map(cpPano => {
    return limit(async () => {
        try {
            // ...
            if (cpPano.panoBucketEndpoint) {
                await this.downloadByPanoApi(cpPano);
            } else {
                await this.download(
                    cpPano.panoBucketRegion!,
                    cpPano.panoBucketName!,
                    cpPano.panoBucketKey!,
                    cpPano.downloadImagePath!
                );
            }
        } catch (err) {
            logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s', cpPano.downloadImagePath, (err as Error).message);
            // 에러를 삼키고 계속 진행 — 불완전한 파일이 디스크에 남음
        }
    });
});
  1. S3 download 스트림: aws-s3.manager.ts:29-48s3.getObject().createReadStream()으로 스트림을 생성하고 fs.createWriteStream에 pipe합니다. 스트림 에러 시 writeStream.destroy()를 호출하지만, 이미 부분적으로 기록된 파일은 디스크에 남습니다.
typescript
// aws-s3.manager.ts:29-48
const stream = s3.getObject(params).createReadStream();
const writeStream = fs.createWriteStream(downloadPath);

return new Promise((resolve, reject) => {
    stream.on('error', (err) => {
        writeStream.destroy();  // 스트림만 닫고, 파일은 삭제하지 않음
        reject(err);
    });
    // ...
    stream.pipe(writeStream);
});
  1. Failure point: mask-work.ts:29-42maskPanos가 bash 스크립트(/tmp/lib/face-body-detector/run.sh)를 실행하며, 스크립트 내부의 Python 코드가 truncated JPEG를 읽으려다 "Premature end of JPEG file"을 stderr로 출력합니다. stderr의 모든 출력은 logger.error로 로깅됩니다.
typescript
// mask-work.ts:29-42
const process: ChildProcessWithoutNullStreams = spawn('stdbuf', ['-oL', 'bash', BlurScriptPath], {
    cwd: path.dirname(BlurScriptPath),
    stdio: 'pipe',
    env: { INPUT_DIRPATH: inputDir, OUTPUT_DIRPATH: outputDir, OPTION_FILE_PATH: optionPath }
});

process.stderr.on('data', (data) => {
    logger.error('MaskWork::maskPanos | script %s', data.toString());
    // stderr의 모든 출력을 error로 처리 — Python 경고도 포함
});
  1. 처리 계속: pano-postprocessor-service.ts:126maskPanos가 프로세스 종료 코드 0으로 정상 종료하면 Promise가 resolve되므로, "Premature end of JPEG file" 경고에도 불구하고 blur_panos 단계가 성공적으로 완료됩니다. 이후 change_masks, infer_panos, resize_panos 단계도 정상 진행됩니다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-pano-postprocessor-instance @capture.id:33484

Capture 33484의 전체 처리 타임라인 (job 89628):

text
21:30:51 KST [info]  PanoPostprocessorService::init
21:30:51 KST [info]  PanoPostprocessorService::authenticate | begin
21:30:51 KST [info]  CupixAuth::setSession | session_id: 5ebb9eb99cc1c2d0ef9c627c661eebbac5f3566c
21:30:51 KST [info]  PanoPostprocessorService::authenticate | end
21:30:51 KST [info]  PanoPostprocessorService::run | begin
21:31:15 KST [error] AwsS3Manager::downloadParallel | begin - "read ECONNRESET"   ← S3 다운로드 중 연결 끊김
21:32:27 KST [info]  PanoPostprocessorService Elapsed Time - dd_step:download_panos&dd_elapsed_time:92556ms
21:32:43 KST [error] MaskWork::maskPanos | script ... UserWarning: No PYTORCH_KERNEL_CACHE_PATH ...
21:32:51 KST [error] MaskWork::maskPanos | script ... RuntimeWarning: overflow encountered in exp ...
21:36:45 KST [error] MaskWork::maskPanos | script Premature end of JPEG file   ← 이 클러스터의 에러
21:43:14 KST [info]  PanoPostprocessorService Elapsed Time - dd_step:blur_panos&dd_elapsed_time:646749ms
21:43:43 KST [info]  PanoPostprocessorService Elapsed Time - dd_step:change_masks&dd_elapsed_time:29180ms
21:44:57 KST [info]  PanoPostprocessorService Elapsed Time - dd_step:infer_panos&dd_elapsed_time:74608ms

핵심 관찰:

  1. 21:31:15에 ECONNRESET 에러 발생 — S3 스트림에서 연결이 끊겨 다운로드가 중단됨
  2. 다운로드 단계 완료 후 (92초 소요) blur 단계가 시작됨 — 에러에도 불구하고 계속 진행
  3. 21:36:45에 "Premature end of JPEG file" — truncated 파일을 face-body-detector가 읽으려다 실패
  4. blur_panos 단계는 646초(약 10분) 걸렸지만 성공적으로 완료 — 프로세스 종료 코드 0
  5. 이후 change_masks, infer_panos 단계도 정상 완료

14일 범위 빈도 확인:

text
service:cupixworks-pano-postprocessor-instance "Premature end of JPEG"

14일간 이 에러는 1건만 발생하여, 간헐적인 네트워크 문제로 인한 단발성 이벤트로 판단됩니다.

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:33-35

  • S3 스트림 에러 발생 시 writeStream.destroy() 후 부분적으로 기록된 파일을 삭제하도록 fs.unlink(downloadPath) 추가 필요

  • 이렇게 하면 truncated JPEG가 디스크에 남아 후속 단계에서 문제를 일으키는 것을 방지할 수 있음

  • 파일: applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:75-77

  • downloadParallel의 catch 블록에서 다운로드 실패한 pano를 실패 목록에 추가하고, 해당 파일을 디스크에서 삭제하는 로직 추가 필요

  • 다운로드 실패한 pano는 후속 mask/infer/resize 단계에서 건너뛸 수 있도록 호출자에게 알려야 함

단기 개선 (1주 이내)#

  • AwsS3Manager.download에 재시도 로직(exponential backoff) 추가 — 일시적인 ECONNRESET 같은 네트워크 오류 시 자동 복구
  • downloadByPanoApi에도 동일한 재시도 로직 적용
  • MaskWork.maskPanos의 stderr 핸들러(mask-work.ts:40-42)에서 Python 경고(UserWarning, RuntimeWarning)와 실제 에러를 구분하여 로그 레벨을 분리 — 현재 stderr의 모든 출력이 error로 로깅되어 노이즈가 발생

장기 개선 (재발 방지)#

  • 다운로드 후 파일 무결성 검증(file size 확인 또는 JPEG 헤더/EOF 마커 검사) 단계 추가
  • 다운로드 실패한 pano를 별도 추적하여 파이프라인 전체에서 skip하는 메커니즘 도입
  • 처리 결과 요약에 다운로드 실패/skip된 pano 수를 포함하여 모니터링 가능하도록 개선

Monitoring#

  • S3 다운로드 실패 빈도 추적:
text
service:cupixworks-pano-postprocessor-instance status:error "ECONNRESET"
  • JPEG 파일 무결성 에러 추적:
text
service:cupixworks-pano-postprocessor-instance status:error "Premature end of JPEG"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 이 에러는 14일간 1건만 발생한 단발성 이벤트이며, face-body-detector 스크립트가 종료 코드 0으로 완료되어 전체 처리 파이프라인은 정상 진행되었습니다. 해당 pano의 mask 품질에 영향이 있을 수 있으나, 서비스 중단이나 데이터 손실은 없었습니다.