AwsS3Manager#downloadParallel swallows S3 key not found errors
RCA: PanoPostprocessorService::run | end - false
Error Log#
PanoPostprocessorService::run | end - false
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: digitaltwinmarine
- 발생 횟수: 3
- 최초 발생: 2026-04-09T21:24:23.609Z
- 최근 발생: 2026-04-09T21:24:23.609Z
Root Cause Summary#
S3에서 pano 이미지 다운로드 시 일부 파일의 S3 key가 존재하지 않아 다운로드에 실패했으나, AwsS3Manager::downloadParallel의 catch 블록이 에러를 로깅만 하고 조용히 무시합니다. 다운로드되지 않은 이미지 파일이 로컬 디스크에 없는 상태에서 MaskWork::maskPanos의 Python face-body-detector 스크립트(cupix_inference.py)가 해당 이미지를 cv2.imread()로 로드하면 빈 이미지(None)가 반환되고, 이를 cv2.cvtColor()에 전달하여 OpenCV assertion failure(!_src.empty())가 발생합니다. Python 스크립트가 exit code 1로 종료되면 maskPanos의 Promise가 reject(false)로 거부되고, 이 false 값이 stringifyError(error)를 통해 "false" 문자열로 변환되어 PanoPostprocessorService::run | end - false 에러 메시지로 기록됩니다.
Technical Analysis#
Code Path#
- Entry point:
pano-postprocessor-service.ts:43—init()메서드에서authenticate()→run()→terminateService()순서로 실행 pano-postprocessor-service.ts:79—run()시작, capture ID 678511, job ID 1006116pano-postprocessor-service.ts:117—awsS3Manager.downloadParallel(cpPanos)호출
// applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:60-77
const tasks = cpPano.map(cpPano => {
return limit(async () => {
try {
// ... download logic
} catch (err) {
logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s', cpPano.downloadImagePath, (err as Error).message);
// 에러를 catch하고 로깅만 함 — 에러를 re-throw하지 않음
// 다운로드 실패한 pano 이미지는 디스크에 존재하지 않은 채로 다음 단계로 진행
}
});
});
- 다운로드 실패 후에도 실행이 계속되어
pano-postprocessor-service.ts:126에서maskWork.maskPanos()호출
// applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:23-58
maskPanos(inputDir: string, outputDir: string, optionPath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
// ...
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.on('close', (code) => {
if (code === 0) {
resolve(true);
} else {
logger.error('MaskWork::maskPanos | error code:%d', code);
reject(false); // boolean false를 reject — Error 객체가 아님
}
});
});
};
- Python 스크립트가 존재하지 않는 이미지 파일을 읽으려다 OpenCV assertion failure로 exit code 1 종료
reject(false)→ catch 블록에서stringifyError(false)호출
// applications/agents/packages/base/src/util/utils.ts:10-12
export const stringifyError = (error: any): string => {
return JSON.stringify(error, Object.getOwnPropertyNames(error));
// JSON.stringify(false, ...) → "false"
};
- Failure point:
pano-postprocessor-service.ts:191—logger.error('PanoPostprocessorService::run | end - %s', stringifyError(error))에서"false"출력
// applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:188-199
} catch (error) {
hasError = true;
await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Error);
logger.error('PanoPostprocessorService::run | end - %s', stringifyError(error));
// error는 boolean false → stringifyError는 "false" 반환
} finally {
if (!hasError) {
await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Done);
}
await this.panoPostprocessorManager.updateJobState(jobId!, TESLA.UpdateJobRequest.StateEnum.Stopped);
// ...
}
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-pano-postprocessor-instance @host.name:58f281671bed
Time range: 2026-04-09T21:20:00Z to 2026-04-09T21:30:00Z
실행 타임라인 (host: 58f281671bed, capture: 678511):
| 시각 (UTC) | Level | Message |
|---|---|---|
| 21:24:08 | info | PanoPostprocessorService::init |
| 21:24:08 | info | PanoPostprocessorService::authenticate | begin |
| 21:24:08 | info | CupixAuth::setSession | session_id: 4e0d5965ca1f9b14c4b96f1af0d07afa32e8d8a1 |
| 21:24:08 | info | PanoPostprocessorService::authenticate | end |
| 21:24:08 | info | PanoPostprocessorService::run | begin |
| 21:24:09 | error | AwsS3Manager::downloadParallel | begin - "The specified key does not exist." |
| 21:24:10 | info | PanoPostprocessorService Elapsed Time - dd_step:download_panos&dd_elapsed_time:1136ms |
| 21:24:22 | error | MaskWork::maskPanos | script cv2.error: OpenCV(4.10.0) ...error: (-215:Assertion failed) !_src.empty() in function 'cvtColor' |
| 21:24:22 | error | MaskWork::maskPanos | script Traceback ... cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB) |
| 21:24:23 | error | MaskWork::maskPanos | error code:1 |
| 21:24:23 | error | PanoPostprocessorService::run | end - false |
| 21:24:24 | info | PanoPostprocessorService::run | end |
| 21:24:24 | info | PanoPostprocessorService::terminateService | force shutdown after 10 seconds |
S3 키 누락 에러 (원문):
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
OpenCV 에러 (원문):
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'
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)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
에러 로그 속성 (3건 모두 동일):
{
"capture": { "id": 678511 },
"team": { "domain": "digitaltwinmarine", "id": 200 },
"job": { "id": 1006116 },
"user": { "id": 1584, "email": "tbruger@digitaltwinmarine.com" },
"region": "us-west-2",
"session": { "id": "4e0d5965ca1f9b14c4b96f1af0d07afa32e8d8a1" }
}
동일 세션의 두 번째 실행 (21:24:44~21:25:31, 성공):
같은 호스트에서 곧바로 재실행되어, 동일한 PyTorch 경고(UserWarning: No PYTORCH_KERNEL_CACHE_PATH)는 발생했지만 S3 키 누락 없이 모든 단계(download → blur → change_masks → infer → resize)를 정상 완료했습니다. 이는 S3 키 누락이 일시적(transient) 상태 문제였음을 시사합니다.
Fix Recommendation#
즉시 조치 (Critical)#
aws-s3.manager.ts:75-77:downloadParallel에서 다운로드 실패 시 에러를 무시하지 않고 실패한 pano ID를 추적하여, 다운로드 실패한 pano를 후속 처리(mask, infer, resize)에서 제외하도록 수정mask-work.ts:55:reject(false)대신reject(new Error(\maskPanos failed with exit code: ${code}`))` 형태로 Error 객체를 전달하여 디버깅 가능한 에러 메시지를 보장
단기 개선 (1주 이내)#
pano-postprocessor-service.ts:117이후에 다운로드 성공 여부를 검증하는 로직 추가 — 다운로드된 파일이 실제로 디스크에 존재하는지 확인하고, 누락된 pano를 처리 대상에서 제외하거나 재시도AwsS3Manager::downloadParallel에서 실패한 pano 목록을 반환하도록 인터페이스 변경
장기 개선 (재발 방지)#
- S3 다운로드 재시도 로직(exponential backoff) 도입 — 일시적 S3 키 누락/접근 실패에 대응
- pano postprocessor 전체 파이프라인에 단계별 validation gate를 추가하여, 이전 단계 실패가 다음 단계로 전파되지 않도록 보장
Monitoring#
- S3 다운로드 실패율 모니터링을 위한 Datadog 쿼리:
service:cupixworks-pano-postprocessor-instance "downloadParallel | download failed"
- mask 처리 실패 모니터링:
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos | error code"
- 전체 run 실패 빈도:
service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService::run | end -"
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — 다운로드 실패 추적 및 후속 단계 필터링 로직 추가 필요.
reject(false)수정은 trivial.