AwsS3Manager#downloadParallel swallows S3 key-not-found error
RCA: MaskWork::maskPanos | cv2.cvtColor assertion failed on empty image
Error Log#
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:79—PanoPostprocessorService.run()시작 pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos)호출로 S3에서 이미지 다운로드- Failure point 1:
aws-s3.manager.ts:75-77— S3 다운로드 실패가 catch 후 로그만 남기고 무시됨 pano-postprocessor-service.ts:126—maskWork.maskPanos(downloadDir, maskDir!, optionPath)호출mask-work.ts:29-32—spawn('stdbuf', ['-oL', 'bash', BlurScriptPath])으로 Python 스크립트 실행- Failure point 2:
cupix_inference.py:334—cv2.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.py의detect()함수(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