ES /docs

[CupixAerialMap] postprocess fail - error:({"message":"VipsJpeg: Corrupt JPEG data: premature end of

RCA: [CupixAerialMap] postprocess fail — VipsJpeg: Corrupt JPEG data: premature end of data segment

Overview#

What Happened#

2026-07-22 00:03:35 KST 에 production aerial-map-service (us-west-2) 의 postprocess Fargate 태스크가 VipsJpeg: Corrupt JPEG data: premature end of data segment 예외로 종료되었다. Aerial map id 546 (session 11381160) 한 건의 processing pipeline 이 실패했고, ECS task 는 exit code 1 로 stop, Step Functions 는 States.TaskFailed 로 전이했다. 동일 postprocess 실행에서 catch 두 지점이 각각 로그를 남겨 fingerprint 가 다른 두 cluster (41be567a-…, d913e019-…) 로 잡혔다.

Quick Facts#

Field Value
exception.class Error (native, thrown by sharp/libvips)
exception.message VipsJpeg: Corrupt JPEG data: premature end of data segment
top_frame src/postprocess/index.ts:152 (inner catch) → src/postprocess/index.ts:179 (outer catch)
runtime Fargate 1.4.0, node in cupix-aerial-map-postprocess:prod container, memory 8192 MB
env production, region us-west-2, tenant cupix
aerial_map.id 546
pix4d.project_id 2814054

Affected Teams#

Team / Domain Error Count Impact
aerial-map-service (postprocess Fargate task) 2 log entries / 1 pipeline run Aerial map 546 postprocess 실패, 사용자에게 orthomosaic/mesh/pointcloud/dsm 산출물 미제공

Timeline#

  1. 2026-07-22 00:02:17 KST — ECS Fargate task faf9829074af4ddeb6cdb346ec980840 StartedAt (postprocess container 부팅).
  2. 2026-07-22 00:02:24 KST 부근[CupixAerialMap] postprocess fargate memory limit: 8192 MB 및 pix4d output 목록 로드 (thumbnail JPEG 포함).
  3. 2026-07-22 00:03:35 KST — postprocess 내부 catch (index.ts:152) 로 [CupixAerialMap] postprocess fail VipsJpeg: Corrupt JPEG data: premature end of data segment 기록, 이어 outer catch (index.ts:179) 에서 JSON.stringify 포함 로그 기록.
  4. 2026-07-22 00:04:30 KST — ECS task StoppedAt (Essential container in task exited, ExitCode 1).
  5. 2026-07-22 00:04:32 KST — Step Functions postprocess-checkStates.TaskFailed 감지, cause(undefined) 로 이행.
  6. 2026-07-22 00:04:38 KST — 후속 data-collect Lambda 정상 실행, 실패 이벤트 아카이빙.

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Aaerial-map-service%20status%3Aerror%20%40environment%3Aproduction%20%22%5BCupixAerialMap%5D%20postprocess%20fail%20-%20error%3A(%7Bmessage%3AVipsJpeg%3A%20Corrupt%20JPEG%20data%3A%20premature%20end%20of%22&from_ts=1784642580000&to_ts=1784649840000&live=false)

text
[CupixAerialMap] postprocess fail - error:({"message":"VipsJpeg: Corrupt JPEG data: premature end of data segment"}) / message:(VipsJpeg: Corrupt JPEG data: premature end of data segment)

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1 (같은 실행 내 2개 로그, 2개 cluster)
  • 최초 발생: 2026-07-22 00:03:35 KST
  • 최근 발생: 2026-07-22 00:03:35 KST
  • 비즈니스 영향: aerial map 546 의 orthomosaic, pointcloud, mesh, dsm 산출물이 업로드되지 않음. Pix4D processing 은 성공했으나 Cupix 측 postprocess 가 중단되어 최종 뷰어에서 결과 확인 불가.

Root Cause Summary#

Postprocess Fargate 태스크가 Pix4D 로부터 다운로드받은 JPEG 입력을 sharp (libvips wrapper) 로 디코딩하는 도중 libvips 의 JPEG 디코더가 파일의 마지막 data segment 를 발견하기 전에 EOF 에 도달하여 VipsJpeg: Corrupt JPEG data: premature end of data segment 예외를 던졌다. 이 예외는 try/catch 내에서 잡혔지만 AerialMapError 인스턴스가 아니므로 saveError(aerialMapId, 'AMB720', …) 로만 기록 후 throw error 로 재던지며 프로세스가 exit 1 로 종료됐다. JPEG 자체가 손상되었거나(Pix4D 산출물 이슈), 다운로드가 중간에 잘려 로컬 파일이 불완전한 상태로 남았을 가능성이 root cause 이며, 현재 코드에는 JPEG 무결성 검증이나 다운로드 재시도 로직이 없어 단일 실패가 전체 postprocess 를 실패시킨다.

Technical Analysis#

Code Path#

  • Entry point: src/postprocess/index.ts:36 (app)
  • JPEG 사용 지점: processThumbnail (project_thumb.jpg 다운로드 → S3 업로드) 및 processAerialPhotocreateThumbnail (스트림 파이프를 sharp() 로 디코드), processMesh (mesh zip 내 texture.jpg 처리)
  • Failure surface: sharp(…) 호출 (src/postprocess/tile.ts:56, src/common/util.ts:261, src/common/util.ts:293)
  • 내부 catch: src/postprocess/index.ts:148-157
  • 외부 catch: src/postprocess/index.ts:178-182

pix4d 출력 목록에는 thumbnail: {…}/project_thumb.jpg 가 포함돼 있으며, processThumbnail 가 이를 다운로드 후 그대로 S3 에 업로드한다. 반면 aerial photo 원본 스트림은 createThumbnail 내부에서 sharp() 로 재인코딩된다. 로그상 아래 sharp 호출 중 하나가 libvips 예외를 발생시켰다.

src/postprocess/index.ts:148-157typescript
} catch (error: any) {
  if (error instanceof AerialMapError) {
    await cupixApi.saveError(aerialMapId, error.code, error.reason);
  } else {
    logger.error(`[CupixAerialMap] postprocess fail ${error.message}`);
    await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB720'].code, ERROR_CODE['AMB720'].reason);
  }

  throw error;
}
src/postprocess/index.ts:172-183typescript
(async () => {
  try {
    await app(input);
    logger.info(`[CupixAerialMap] postprocess success`);
    await sleep(30000); // sleep for log push
    process.exit(0);
  } catch (error: any) {
    logger.error(`[CupixAerialMap] postprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000); // sleep for log push
    process.exit(1);
  }
})();

이 두 catch 가 같은 예외를 각각 로깅하기 때문에 error-sweeper 가 두 개의 fingerprint (5148571f…, 5601703a…) 로 분리 저장했고 status board 에서도 동일 incident 로 묶였다.

src/common/util.ts:290-307typescript
export const createThumbnail = async (stream: NodeJS.ReadableStream): Promise<NodeJS.ReadableStream> => {
  return new Promise((resolve, reject) => {
    const webpStream = stream.pipe(
      sharp()
        .resize(512, 512, {
          fit: 'cover',
          position: 'center',
        })
        .webp({
          effort: 0,
          quality: 85,
        }),
    );

    webpStream.on('finish', () => resolve(webpStream));
    webpStream.on('error', (err) => reject(err));
  });
};
src/postprocess/process.ts:56-79typescript
for (let i = 0; i < aerialPhotos.length; i++) {
  const aerialPhoto = aerialPhotos[i];
  const aerialPhotoUploadCredential = await cupixApi.createAerialPhotoS3UploadCredential(aerialMapId, aerialPhoto.id);

  const imageError = report.imageErrors[aerialPhoto.id];
  logger.debug(`[CupixAerialMap] aerial photo(${aerialPhoto.id}) imageError: ${JSON.stringify(imageError)}`);
  const cameraCalibration = calibration[aerialPhoto.id];
  ...
  const stream = await downloadStreamByUrl(aerialPhoto.downloadUrl);
  const thumbnailKey = `${aerialPhotoUploadCredential.basepath}/thumbnail/thumbnail`;
  const thumbnailStream = await createThumbnail(stream);
  await uploadS3StreamByKey({
    credential: aerialPhotoUploadCredential,
    imageKey: thumbnailKey,
    stream: thumbnailStream,
  });

processAerialPhoto 는 aerial photo 원본을 HTTP 로 스트리밍하여 sharp 파이프에 그대로 흘려보낸다. 스트림이 완전히 수신되기 전에 소스가 끊기거나 원본 JPEG 자체가 truncated 라면 libvips 는 premature end of data segment 를 뱉는다. 재시도, 파일 크기 검증, sharp().on('error') 이후 skip 로직이 없기 때문에 사진 한 장의 손상이 전체 postprocess 를 실패로 만든다.

기대 동작: 손상된 이미지 1장은 skip 하거나 다시 다운로드 후 재시도, 정상 aerial photo 는 계속 처리하여 pipeline 성공. 실제 동작: 첫 실패 예외가 Promise.all(tasks) 로 전파되어 전체 postprocess 가 즉시 종료.

Log Evidence#

Datadog query (재현 가능):

text
service:aerial-map-service "VipsJpeg" "premature end"

핵심 로그 (시각 KST 변환):

2026-07-22 00:03:35 KSTjson
{
  "message": "[CupixAerialMap] postprocess fail VipsJpeg: Corrupt JPEG data: premature end of data segment"
}
2026-07-22 00:03:35 KSTjson
{
  "message": "[CupixAerialMap] postprocess fail - error:({\"message\":\"VipsJpeg: Corrupt JPEG data: premature end of data segment\"}) / message:(VipsJpeg: Corrupt JPEG data: premature end of data segment)"
}

Pix4D 출력 확인 (aerial map 546, project 2814054):

text
thumbnail: prod-pix4d-cloud-default/org_grp-ccd56fbd-.../project-2814054/thumb/project_thumb.jpg  (region us-east-1)
mesh:      prod-pix4d-cloud-default/.../reconstructions/cupix-production-pjhoerr-546-1784643225971_mesh.zip
outputs:   ["orthomosaic","pointcloud","mesh","dsm"]

ECS task 종료 정보 (Step Functions postprocess-check 이벤트에서 추출):

2026-07-22 00:04:32 KSTjson
{
  "Error": "States.TaskFailed",
  "Cause.ExitCode": 1,
  "Cause.StoppedReason": "Essential container in task exited",
  "Cause.StartedAt": 1784646137086,
  "Cause.StoppedAt": 1784646270731,
  "TaskDefinition": "cupix-aerial-map-postprocess-task-def-production:3",
  "Image": "cupix-aerial-map-postprocess:prod",
  "Memory": "8192",
  "Cpu": "2048"
}

Fargate memory 는 8192 MB 로 여유가 컸으며(uncertain — RSS 값 없음), OOM 관련 kernel 메시지도 관찰되지 않아 메모리 부족은 배제된다. Pix4D processing 자체는 성공(result:"success") 상태로 postprocess 태스크를 트리거한 이후 실패한 것이므로 상류 서비스는 정상이다.

Status board 확인:

text
scope: svc:aerial-map-service::unknown
active incident: 2026-07-21-svc-aerial-map-service--unknown-1
cluster_ids: [d913e019-5ded-4587-8f04-51a70354ddb9, 41be567a-8c5e-4e93-8086-00695852a09e]

두 cluster 는 동일 aerial map id 546 실행의 inner/outer catch 로그로, 하나의 실제 이벤트다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 sharp/libvips 가 손상 또는 truncated JPEG 입력을 디코딩하다 예외를 던졌고, 재시도/스킵 로직 부재로 전체 postprocess 가 중단됨 에러 메시지 VipsJpeg: Corrupt JPEG data: premature end of data segment 는 libvips JPEG 디코더가 EOF 를 조기 감지할 때만 던지는 문자열; sharp 호출은 tile.ts:56, util.ts:261,293 세 곳에 존재; index.ts:148-157 의 catch 가 non-AerialMapError 를 그대로 재throw; Pix4D output 목록에 project_thumb.jpg 존재 Confirmed
H2 Fargate task 가 OOM 으로 killed 되면서 사이드 이펙트로 로그가 남음 Sharp 는 대용량 이미지에서 메모리를 많이 사용할 수 있음 ECS StopCode 는 EssentialContainerExited (OOM 이면 OutOfMemoryError), ExitCode 1 은 정상 예외 종료; Memory 한도 8192 MB, postprocess fargate memory limit 로그 확인 후 정상 진행됨 Rejected
H3 Pix4D 상류 processing 이 실패했고 그 여파로 postprocess 가 손상된 입력을 받음 손상된 JPEG 를 산출한 상류 서비스가 원인일 수 있음 pix4d-process-check 이벤트가 result:"processing"postprocess-checkresult:"success" 로 넘겨줌; Pix4D getProcessingOutput 응답에서 모든 output availability:"done" Rejected as root cause, but Pix4D 산출물 JPEG 자체의 손상 가능성은 H1 의 하위 원인으로 남음
H4 Network transient error 로 인해 다운로드가 중간에 끊겨 로컬 JPEG 이 truncated downloadStreamByUrl 로 스트리밍 받아 sharp 파이프에 그대로 흘리는 경로(processAerialPhoto)가 존재; premature EOF 는 truncated 파일에서 흔한 증상 단일 실행/1건이라 재현 로그 부족, 어느 JPEG 인지 특정하는 로그 없음 (uncertain -- needs verification: 실패 직전에 어떤 aerial photo id 를 처리 중이었는지 debug 로그 확인 필요) Inconclusive (H1 의 잠재 하위 원인)

Fix Recommendation#

즉시 조치 (Critical)#

  • src/postprocess/process.ts:56-79 processAerialPhoto 루프: 개별 aerial photo 처리를 try/catch 로 감싸 실패 시 해당 photo 만 image_error 상태로 기록하고 다음 photo 로 진행. 전체 postprocess 를 중단시키지 않도록 방향 전환.
  • src/postprocess/index.ts:148-157 내부 catch: error.messageVipsJpeg 또는 Corrupt JPEG 가 포함될 때 별도 error code (예: 신규 AMB72x) 로 저장하여 재실행/사용자 알림 로직을 분기할 수 있게 한다. 현재는 모두 AMB720 로 뭉뚱그려 저장되어 원인 분류가 불가.
  • 실패한 JPEG 를 특정하기 위해 createThumbnail 진입 시 aerialPhoto.id 를 로그에 남기고, sharp 스트림에 .on('error', …) 를 붙여 어떤 파일이 실패했는지 식별.

단기 개선 (1주 이내)#

  • downloadStreamByUrl 로 받은 스트림을 sharp 에 직접 파이프하는 대신 임시 파일에 저장 후 fs.statSync 로 크기 검증 → sharp 처리하는 2-단계 방식으로 변경. Premature EOF 를 스트림 종료 시점에서 조기 감지하고 재시도 가능.
  • processAerialPhoto 다운로드 호출에 exponential backoff 재시도(예: 3회) 추가. Pix4D S3 크로스 리전 다운로드 (us-east-1 → us-west-2 task) 는 간헐적 network 결함에 노출된다.
  • processThumbnail (process.ts:103-120) 도 동일 패턴 적용: 실패 시 project_thumb 미업로드로 남기되 다른 tasks 는 완료.

장기 개선 (재발 방지)#

  • postprocess 단계별 실패를 격리하는 아키텍처 재검토. 현재 Promise.all(tasks) 로 orthomosaic/mesh/pointcloud/dsm/thumbnail/aerial photo 를 병렬로 묶어 첫 실패에 전체가 종료된다. 단계별 결과를 aggregate 하고 부분 성공을 허용하도록 개편.
  • Pix4D 출력 JPEG 무결성 검증 훅(간단한 SOI/EOI 마커 체크 또는 sharp metadata() dry run) 을 도입해 실제 postprocess 이전에 손상 파일을 감지.
  • error-sweeper 사이드에서 동일 실행이 두 fingerprint 로 분리되는 문제를 완화하기 위해 inner catch 로그와 outer catch 로그를 하나로 합치거나, outer catch 만 남기고 stack trace 를 그대로 출력.

Monitoring#

  • Sharp/libvips 관련 실패 카운트를 별도 메트릭으로 추적하여 재발 여부 판단.
text
service:aerial-map-service status:error @environment:production "VipsJpeg"
  • postprocess 전체 실패율 (aerial_map 단위):
text
service:aerial-map-service status:error @environment:production "[CupixAerialMap] postprocess fail"
  • Fargate task 실패 (ExitCode !=0) 추이:
text
service:aerial-map-service status:info @environment:production "States.TaskFailed"

Risk Assessment#

  • Risk level: medium (production 서비스, 사용자 데이터 처리 실패, 단 발생 빈도는 낮음 — 지난 14일간 유사 지점 발생 1건, 다른 fingerprint 는 preprocess 계열)
  • 예상 복잡도: standard (개별 aerial photo 실패 격리 + 재시도 도입은 표준 리팩터링; 실패 JPEG 특정을 위한 로그 보강 포함)