ES /docs

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

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 의 postprocess Fargate task 가 sharp/libvips JPEG 디코딩 단계에서 VipsJpeg: Corrupt JPEG data: premature end of data segment 로 실패했다. aerial map 546 (pix4d project 2814054) 의 후처리 파이프라인이 report, flight_path 단계까지 정상 완료된 뒤 aerial photo 썸네일 생성 단계에서 중단되었으며 해당 postprocess task 는 exit code 1 로 종료되었다.

Quick Facts#

Field Value
exception.class Error (libvips 로부터 전달됨)
exception.message VipsJpeg: Corrupt JPEG data: premature end of data segment
top_frame applications/aerial-map-service/src/code/src/common/util.ts:290-306 (createThumbnail)
runtime Node.js on Fargate, sharp (libvips)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
aerial-map-service 2 (본 클러스터 + 41be567a-8c5e-4e93-8086-00695852a09e) aerial map 546 후처리 실패 — 사용자에게 결과 미노출, AMB720 코드로 상태 저장

두 클러스터는 동일 실행에서 발생한 서로 다른 log line (top-level catch vs 내부 catch) 이며 실제 후처리 실패 건수는 1건이다.

Timeline#

  1. 2026-07-22 00:02:17 KST — postprocess Fargate task 기동, workspace /workspace/1784646137444 생성, memory limit 8192 MB (Datadog info log)
  2. 2026-07-22 00:02:20 KST — pix4d 결과 목록 수신 (report_pdf, report_json, flight_path, thumbnail 등 포함)
  3. 2026-07-22 00:02:23 KSTpix4d report output post-processed
  4. 2026-07-22 00:02:24 KSTpix4d flight path output post-processed
  5. 2026-07-22 00:03:35 KST — sharp/libvips 가 JPEG stream 을 디코딩하는 도중 VipsJpeg: Corrupt JPEG data: premature end of data segment 발생, processAerialPhoto 루프 실패 (pix4d aerial photo post-processed info 로그 없음)
  6. 2026-07-22 00:03:35 KST — inner catch 에서 [CupixAerialMap] postprocess fail ${error.message} 로깅 후 saveError(aerialMapId, 'AMB720') 호출
  7. 2026-07-22 00:03:35 KST — 예외가 재-throw 되어 outer IIFE 의 catch 에서 [CupixAerialMap] postprocess fail - error:(...) / message:(...) 로 재로깅, process.exit(1)

Error Log#

Datadog Logs

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

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1 (동일 실행에서 발생한 상위 catch 재로깅 클러스터 41be567a-8c5e-4e93-8086-00695852a09e 를 합쳐도 실제 실행 실패 1건)
  • 최초 발생: 2026-07-22 00:03:35 KST
  • 최근 발생: 2026-07-22 00:03:35 KST

Root Cause Summary#

aerial map 546 의 후처리 실행 중 processAerialPhoto 가 각 aerial photo 의 원본 이미지를 axios stream (downloadStreamByUrl) 으로 받아 createThumbnail (sharp/libvips) 로 파이프하는데, 원본 JPEG stream 이 완결되기 전에 EOF 가 도달하여 libvips 가 Corrupt JPEG data: premature end of data segment 를 던졌다. 코드 경로에는 스트림 완결성(Content-Length 대비 수신 바이트 수) 검증, 재시도, corrupt 이미지 skip 로직이 전혀 없어 단일 손상/절단된 사진 하나가 전체 aerial map 후처리를 실패시키는 구조다. 최근 7일 내 동일 scope (svc:aerial-map-service::unknown) 로 이미 resolved 된 incident (2026-07-16-svc-aerial-map-service--unknown-1) 가 존재해 재발성 이슈로 보인다.

Technical Analysis#

Code Path#

  • Entry point: applications/aerial-map-service/src/code/src/postprocess/index.ts:172-183 — IIFE 가 app(input) 을 실행하고 실패 시 outer log emit
  • Main orchestration: postprocess/index.ts:36-158 (app)
  • Sub-step call: postprocess/index.ts:101processAerialPhoto(cupixApi, aerialMapId, report!, calibration!, aerialPhotos)
  • Failure point: applications/aerial-map-service/src/code/src/postprocess/process.ts:66-73 (stream download → sharp pipe) 를 통해 common/util.ts:290-306createThumbnail 에서 libvips 오류 발생

Outer/inner 이중 로깅으로 인해 클러스터가 두 개로 쪼개진다:

applications/aerial-map-service/src/code/src/postprocess/index.ts:148-183typescript
} 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;
}
// ...
(async () => {
  try {
    await app(input);
    logger.info(`[CupixAerialMap] postprocess success`);
    await sleep(30000);
    process.exit(0);
  } catch (error: any) {
    logger.error(`[CupixAerialMap] postprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000);
    process.exit(1);
  }
})();
  • 내부 catch (line 152) → 현재 클러스터의 대표 메시지 생성
  • 외부 IIFE catch (line 179) → 자매 클러스터 41be567a-... 의 대표 메시지 생성
  • 동일 예외가 두 번 로그되어 서로 다른 fingerprint 로 클러스터링됨

실제 실패 지점:

applications/aerial-map-service/src/code/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 stream = await downloadStreamByUrl(aerialPhoto.downloadUrl);
  const thumbnailKey = `${aerialPhotoUploadCredential.basepath}/thumbnail/thumbnail`;
  const thumbnailStream = await createThumbnail(stream);
  await uploadS3StreamByKey({
    credential: aerialPhotoUploadCredential,
    imageKey: thumbnailKey,
    stream: thumbnailStream,
  });
  // ...
}
  • downloadStreamByUrl 은 axios responseType: 'stream' 만 사용 — 완결성 검증/재시도 없음:
applications/aerial-map-service/src/code/src/common/s3.ts:164-167typescript
export const downloadStreamByUrl = async (downloadUrl: string) => {
  const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' });
  return response.data;
};
  • createThumbnail 이 sharp 인스턴스에 원본 stream 을 pipe:
applications/aerial-map-service/src/code/src/common/util.ts:290-306typescript
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));
  });
};

기대 동작: for 루프가 aerialPhotos 를 모두 순회, 각 사진의 손상/네트워크 문제는 개별 handling 하고 pix4d aerial photo post-processed info 로그 출력. 실제 동작: 한 장의 사진 stream 이 조기 EOF → libvips 예외 → 루프 중단 → 전체 postprocess 실패, 성공한 사진들의 metadata 저장까지 롤백 없이 중단.

Log Evidence#

Datadog query (본 클러스터):

text
service:aerial-map-service status:error @environment:production "[CupixAerialMap] postprocess fail VipsJpeg: Corrupt JPEG data: premature end of data segment"

주변 컨텍스트 query:

text
service:aerial-map-service @environment:production
(2026-07-21T15:02:00 ~ 2026-07-21T15:05:00 UTC)

수집된 로그 (KST 로 정렬):

text
00:02:17  info   [CupixAerialMap] postprocess fargate memory limit: 8192 MB
00:02:17  info   [CupixAerialMap] aerial map 546's workspace workspace: /workspace/1784646137444
00:02:20  info   [CupixAerialMap] postprocess - pix4d output:({... "thumbnail":{...".jpg"} ...})
00:02:23  info   [CupixAerialMap] pix4d report output post-processed
00:02:24  info   [CupixAerialMap] pix4d flight path output post-processed
00:03:35  error  [CupixAerialMap] postprocess fail VipsJpeg: Corrupt JPEG data: premature end of data segment
00:03:35  error  [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 photo post-processed (process.ts:81) 로그가 없음 → processAerialPhotofor 루프 완료 전 예외
  • pix4d thumbnail output post-processed (process.ts:119) 로그도 없음 → Promise.all(tasks) 대기 중 processAerialPhoto 실패로 인해 즉시 rethrow (index.ts:101Promise.all 이전에 sequential 호출)
  • 실행 대상 pix4d project: 2814054, run id 1784643225971, aerial map id 546

Status board:

json
{
  "scope": "svc:aerial-map-service::unknown",
  "active": { "id": "2026-07-21-svc-aerial-map-service--unknown-1", "cluster_ids": ["d913e019-...", "41be567a-..."] },
  "recent": [ { "id": "2026-07-16-svc-aerial-map-service--unknown-1", "resolved_at": "2026-07-16T22:47:24.990Z" } ]
}

5일 전 동일 scope 로 resolved 된 incident 가 있어 재발성 패턴이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 processAerialPhoto 가 개별 aerial photo JPEG stream 을 sharp 로 디코딩하다가 premature EOF 로 libvips 예외를 던졌고, 재시도/skip 로직이 없어 전체 postprocess 가 실패했다 로그 순서상 report/flight_path 완료 후, aerial photo post-processed 로그 없이 VipsJpeg 오류; sharp/libvips 는 JPEG 절단 시 정확히 이 메시지를 emit; downloadStreamByUrl (s3.ts:164-167) 는 완결성 검증 없음; createThumbnail (util.ts:290-306) 은 error handler 만 있고 재시도 없음 Confirmed
H2 pix4d thumbnail output (project_thumb.jpg) 자체가 손상되어 processThumbnail 이 실패했다 pix4d output 목록에 thumbnail .jpg 존재 processThumbnail (process.ts:103-120) 은 sharp 를 사용하지 않고 단순히 download → S3 upload 만 함 — libvips 를 거치지 않아 VipsJpeg 를 던질 수 없음; 또한 Promise.all(tasks) 이전에 sequential processAerialPhoto 가 먼저 실패했다는 로그 순서와 일치하지 않음 Rejected
H3 AerialMapError (AMB721 등 코드 매핑) 로 인한 실패 inner catch 는 AerialMapError 를 분기 처리 로그가 postprocess fail ${error.message} 형식 (line 152) 이므로 non-AerialMapError 분기; AerialMapError 라면 message 대신 code/reason 이 saveError 로만 저장됨 Rejected
H4 Fargate memory 부족으로 sharp 가 실패 sharp 는 대용량 이미지에서 OOM 가능 로그에 memory limit 8192 MB 명시, 실패 메시지는 OOM 이 아닌 명확한 Corrupt JPEG data: premature end of data segment (libvips 의 데이터 완결성 오류) Rejected
H5 외부 dependency 전체 outage (pix4d 또는 S3) 재발성 (7/16 resolved incident 존재) status board scope 가 dep:* 이 아닌 svc:aerial-map-service::unknown; 같은 실행에서 pix4d output 조회/다운로드는 정상 (report, flight_path 완료); pix4d 원본 photo download URL 은 aerialPhoto 별이라 단일 사진만 실패했을 가능성 Rejected as blanket outage; 단일 asset 문제로 좁혀짐

Fix Recommendation#

즉시 조치 (Critical)#

  • applications/aerial-map-service/src/code/src/postprocess/process.ts:56-79processAerialPhoto 루프 안에서 개별 사진 실패를 격리 (try/catch per iteration). 실패한 사진은 imageError 로 표기하고 다음 사진 진행 — 한 장 손상으로 전체 aerial map 이 실패하는 blast radius 를 축소.
  • applications/aerial-map-service/src/code/src/common/s3.ts:164-167 downloadStreamByUrl 에 stream 완결성 검증 추가 방향: Content-Length 헤더 존재 시 실제 수신 바이트와 비교, response.dataerror/aborted 이벤트를 명시적으로 reject. 필요 시 axios-retry 로 재시도 (idempotent GET).
  • 손상/절단 이미지 처리 정책 결정: skip 후 aerial map 을 partial 성공으로 마무리 vs 명확한 AerialMapError code 를 새로 정의 (예: AMB726 aerial_photo_corrupted) 하여 UI/API 상에서 재-업로드 유도.

단기 개선 (1주 이내)#

  • postprocess/index.ts:148-183 의 이중 catch 로깅 정리 — 동일 예외가 서로 다른 fingerprint 로 클러스터링되어 alert noise 를 유발. inner catch 에서 이미 log & saveError 를 처리했다면 outer catch 는 rethrow 만 하거나, 반대로 outer 만 남기고 inner 는 saveError 만 수행.
  • processAerialPhototasks 배열의 Promise.all 에 합류시키지 않는 이유(현재 sequential 호출) 를 재검토. sequential 이 필수라면 실패한 사진의 partial 상태에 대한 문서화 필요.
  • Datadog monitor: VipsJpeg 문자열 및 postprocess fail count 를 24h rolling 로 추적하여 재발 즉시 인지.

장기 개선 (재발 방지)#

  • 업로드 파이프라인(사용자 사진 → S3) 자체에서 이미지 유효성 검증 (sharp metadata() 호출 성공 여부, EXIF 파싱) 을 postprocess 이전 단계로 이동. common/util.ts:261 에 이미 metadata 조회 로직이 있으므로 원본 업로드 완료 시점에 재사용 가능.
  • Fargate 후처리 재실행 정책 수립: transient network 로 인한 truncated stream 일 경우 자동 재시도 (aerialMap 단위 backoff), corrupt 원본이면 즉시 사용자에게 알림.
  • aerial-map-service::unknown root_cause_type 을 좀 더 세분화 (예: image_decode, pix4d_download) 하여 status board 가 재발성을 조기 인식하도록 함 — 7/16 사건과 이번 사건 모두 unknown 으로 묶여 있어 재발 신호가 약함.

Monitoring#

  • Datadog log alert — postprocess VipsJpeg 실패 count
text
service:aerial-map-service status:error @environment:production "VipsJpeg"
  • Datadog log alert — postprocess 전체 실패 count (fingerprint 무관)
text
service:aerial-map-service status:error @environment:production "postprocess fail"
  • Datadog log query — aerial photo 처리 완료율 (성공 signal 로 baseline)
text
service:aerial-map-service @environment:production "pix4d aerial photo post-processed"
  • Datadog log query — 후처리 실행 규모 (분모)
text
service:aerial-map-service @environment:production "postprocess fargate memory limit"

Risk Assessment#

  • Risk level: medium (사용자당 aerial map 결과 미노출, 데이터 손실은 없음; 재발성 있음)
  • 예상 복잡도: standard — 개별 사진 격리 + stream 완결성 검증은 국소 변경이며 downstream API 변경 불필요