MaskWork::maskPanos | script Premature end of JPEG file
RCA: MaskWork::maskPanos | script Premature end of JPEG file
Error Log#
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#
-
Entry point:
pano-postprocessor-service.ts:46—PanoPostprocessorService.init()이run()을 호출합니다. -
Pano 다운로드:
pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos)가 S3에서 pano 이미지를 병렬 다운로드합니다.
// 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);
// 에러를 삼키고 계속 진행 — 불완전한 파일이 디스크에 남음
}
});
});
- S3 download 스트림:
aws-s3.manager.ts:29-48—s3.getObject().createReadStream()으로 스트림을 생성하고fs.createWriteStream에 pipe합니다. 스트림 에러 시writeStream.destroy()를 호출하지만, 이미 부분적으로 기록된 파일은 디스크에 남습니다.
// 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);
});
- Failure point:
mask-work.ts:29-42—maskPanos가 bash 스크립트(/tmp/lib/face-body-detector/run.sh)를 실행하며, 스크립트 내부의 Python 코드가 truncated JPEG를 읽으려다 "Premature end of JPEG file"을 stderr로 출력합니다. stderr의 모든 출력은logger.error로 로깅됩니다.
// 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 경고도 포함
});
- 처리 계속:
pano-postprocessor-service.ts:126—maskPanos가 프로세스 종료 코드 0으로 정상 종료하면 Promise가 resolve되므로, "Premature end of JPEG file" 경고에도 불구하고 blur_panos 단계가 성공적으로 완료됩니다. 이후change_masks,infer_panos,resize_panos단계도 정상 진행됩니다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-pano-postprocessor-instance @capture.id:33484
Capture 33484의 전체 처리 타임라인 (job 89628):
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
핵심 관찰:
- 21:31:15에
ECONNRESET에러 발생 — S3 스트림에서 연결이 끊겨 다운로드가 중단됨 - 다운로드 단계 완료 후 (92초 소요) blur 단계가 시작됨 — 에러에도 불구하고 계속 진행
- 21:36:45에 "Premature end of JPEG file" — truncated 파일을 face-body-detector가 읽으려다 실패
- blur_panos 단계는 646초(약 10분) 걸렸지만 성공적으로 완료 — 프로세스 종료 코드 0
- 이후 change_masks, infer_panos 단계도 정상 완료
14일 범위 빈도 확인:
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 다운로드 실패 빈도 추적:
service:cupixworks-pano-postprocessor-instance status:error "ECONNRESET"
- JPEG 파일 무결성 에러 추적:
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 품질에 영향이 있을 수 있으나, 서비스 중단이나 데이터 손실은 없었습니다.