ES /docs

AwsS3Manager#downloadParallel swallows S3 key not found errors

RCA: PanoPostprocessorService::run | end - false

Error Log#

Datadog Logs

text
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:43init() 메서드에서 authenticate()run()terminateService() 순서로 실행
  • pano-postprocessor-service.ts:79run() 시작, capture ID 678511, job ID 1006116
  • pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos) 호출
typescript
// 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() 호출
typescript
// 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) 호출
typescript
// 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:191logger.error('PanoPostprocessorService::run | end - %s', stringifyError(error)) 에서 "false" 출력
typescript
// 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 쿼리:

text
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 키 누락 에러 (원문):

text
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."

OpenCV 에러 (원문):

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'
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)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

에러 로그 속성 (3건 모두 동일):

json
{
  "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 쿼리:
text
service:cupixworks-pano-postprocessor-instance "downloadParallel | download failed"
  • mask 처리 실패 모니터링:
text
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos | error code"
  • 전체 run 실패 빈도:
text
service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService::run | end -"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — 다운로드 실패 추적 및 후속 단계 필터링 로직 추가 필요. reject(false) 수정은 trivial.