ES /docs

AwsS3Manager#downloadParallel swallows S3 key-not-found error

RCA: MaskWork::maskPanos | cv2.cvtColor assertion failed on empty image

Error Log#

Datadog Logs

text
MaskWork::maskPanos | script Traceback (most recent call last):
  File "/tmp/lib/face-body-detector/cupix_inference.py", line 453, in <module>
    detect(args)
  File "/tmp/lib/face-body-detector/cupix_inference.py", line 334, in detect
    img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: digitaltwinmarine
  • 발생 횟수: 3
  • 최초 발생: 2026-04-09T21:24:22.601Z
  • 최근 발생: 2026-04-09T21:24:22.601Z

Root Cause Summary#

S3에서 pano 이미지를 다운로드할 때 "The specified key does not exist." 에러가 발생하여 이미지 파일이 디스크에 기록되지 않았습니다. 그러나 AwsS3Manager.downloadParallel()의 에러 핸들링이 예외를 catch 후 로그만 남기고 무시(swallow)하기 때문에, 다운로드 실패에도 불구하고 후속 mask 처리 단계가 계속 진행되었습니다. Python cupix_inference.py 스크립트가 존재하지 않는 이미지 파일을 cv2.imread()로 읽으면 None이 반환되고, 이어서 cv2.cvtColor(None, cv2.COLOR_BGR2RGB) 호출 시 !_src.empty() assertion이 실패하여 프로세스 전체가 exit code 1로 종료되었습니다.

Technical Analysis#

Code Path#

  • Entry point: pano-postprocessor-service.ts:79PanoPostprocessorService.run() 시작
  • pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos) 호출로 S3에서 이미지 다운로드
  • Failure point 1: aws-s3.manager.ts:75-77 — S3 다운로드 실패가 catch 후 로그만 남기고 무시됨
  • pano-postprocessor-service.ts:126maskWork.maskPanos(downloadDir, maskDir!, optionPath) 호출
  • mask-work.ts:29-32spawn('stdbuf', ['-oL', 'bash', BlurScriptPath]) 으로 Python 스크립트 실행
  • Failure point 2: cupix_inference.py:334cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB) 에서 empty 이미지에 대한 assertion 실패

S3 다운로드 에러 무시 (핵심 원인):

typescript
// aws-s3.manager.ts:60-78
const tasks = cpPano.map(cpPano => {
    return limit(async () => {
        try {
            // ... download 시도
            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);
            // 에러를 로그만 남기고 re-throw하지 않음 → 다운로드 실패해도 프로세스 계속 진행
        }
    });
});

마스크 처리에서 다운로드 실패 여부를 확인하지 않음:

typescript
// pano-postprocessor-service.ts:122-127
if (maskTypeOption) {
    this.maskWork.createOptionFile(optionPath, maskTypeOption);
    start = Date.now();
    await this.maskWork.maskPanos(downloadDir, maskDir!, optionPath);
    // downloadDir 내 파일 존재 여부를 확인하지 않고 바로 Python 스크립트 실행

Python 스크립트에서 이미지 로드 실패 시 None 반환:

text
# cupix_inference.py:334 (추정)
# cv2.imread()가 파일을 찾지 못하면 None을 반환
# cv2.cvtColor(None, cv2.COLOR_BGR2RGB) → assertion failed: !_src.empty()

기대 동작: S3에서 이미지 다운로드가 실패하면 해당 pano를 마스크 처리에서 제외하거나, 전체 프로세스를 중단해야 함. 실제 동작: 다운로드 실패가 무시되고 Python 스크립트가 존재하지 않는 파일을 처리하려다 crash.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-pano-postprocessor-instance (status:error OR status:info) from:2026-04-09T21:22:00Z to:2026-04-09T21:26:00Z sort:timestamp asc

타임라인 (3개의 동시 인스턴스에서 발생):

시각 (UTC) 이벤트
21:24:08.404Z PanoPostprocessorService::init (3개 인스턴스 동시 시작)
21:24:08.457Z PanoPostprocessorService::run | begin (3개)
21:24:09.737Z AwsS3Manager::downloadParallel | begin - "The specified key does not exist." (3개)
21:24:10.635Z download_panos elapsed_time:1136ms
21:24:22.601Z cv2.error: OpenCV(4.10.0) ... (-215:Assertion failed) !_src.empty() in function 'cvtColor'
21:24:22.601Z Python traceback: cupix_inference.py:334
21:24:23.371Z MaskWork::maskPanos | error code:1 (3개)
21:24:23.609Z PanoPostprocessorService::run | end - false (3개)
21:24:24.524Z terminateService | force shutdown after 10 seconds

S3 키 없음 에러 (직접 원인):

json
{
  "timestamp": "2026-04-09T21:24:09.737Z",
  "status": "error",
  "message": "AwsS3Manager::downloadParallel | begin - \"The specified key does not exist.\""
}

OpenCV assertion 실패:

text
MaskWork::maskPanos | script cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'

프로세스 종료:

json
{
  "timestamp": "2026-04-09T21:24:23.371Z",
  "status": "error",
  "message": "MaskWork::maskPanos | error code:1"
}

비교: 동일 시간대 성공 인스턴스 (21:23:49Z 시작):

text
21:23:49.130Z  PanoPostprocessorService::run | begin
21:23:51.468Z  download_panos elapsed_time:1201ms       ← S3 에러 없음
21:24:06.074Z  blur_panos elapsed_time:15493ms           ← mask 성공
21:24:07.173Z  change_masks elapsed_time:212ms
21:24:07.850Z  infer_panos elapsed_time:677ms
21:25:31.372Z  PanoPostprocessorService::run | end        ← 정상 종료

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: aws-s3.manager.ts:75-77
  • downloadParallel()에서 다운로드 실패한 pano 목록을 추적하고, 실패 건수를 반환하거나 실패한 pano ID 배열을 상위로 전달해야 합니다.
  • 파일: pano-postprocessor-service.ts:117-126
  • downloadParallel() 호출 후 다운로드 실패한 pano가 있으면 해당 pano를 maskPanos 입력에서 제외하거나, 전체 다운로드 실패 시 프로세스를 중단해야 합니다.

단기 개선 (1주 이내)#

  • cupix_inference.pydetect() 함수(line 334 부근)에서 cv2.imread() 결과가 None인 경우 해당 이미지를 skip하고 경고 로그를 남기는 방어 코드를 추가합니다. 이미지 하나의 실패가 전체 배치를 crash시키지 않아야 합니다.
  • AwsS3Manager.downloadParallel()에서 다운로드 실패 시 0-byte 파일이 디스크에 남을 수 있으므로, 실패한 경우 해당 파일을 삭제하는 cleanup 로직을 추가합니다.

장기 개선 (재발 방지)#

  • S3 key가 존재하지 않는 원인을 별도로 조사해야 합니다. pano의 resource 레코드에 기록된 storage_bucket_key가 실제 S3 오브젝트와 불일치하는 데이터 정합성 문제가 있을 수 있습니다.
  • maskPanos가 전체 디렉토리를 대상으로 Python 스크립트를 실행하는 방식이므로, 스크립트 입력으로 처리할 파일 목록을 명시적으로 전달하는 방식을 검토합니다.

Monitoring#

  • 다운로드 실패율 모니터링:
text
service:cupixworks-pano-postprocessor-instance status:error "The specified key does not exist"
  • mask 처리 실패 모니터링:
text
service:cupixworks-pano-postprocessor-instance status:error "MaskWork::maskPanos | error code"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard