ES /docs

MaskWork::maskPanos | error code:1

RCA: MaskWork::maskPanos | error code:1

Overview#

What Happened#

2026-06-24 14:11:50 KST에 cupixworks-pano-postprocessor-instance (ap-southeast-2, production)에서 동일 capture에 대한 3개의 동시 처리 인스턴스가 모두 mask 단계에서 실패했다. 자식 프로세스로 실행된 face/body blur 스크립트 내부의 Python inference 스크립트 cupix_inference.pycv2.cvtColor assertion 실패 (!_src.empty())로 크래시했고, 그 결과 spawn된 bash 스크립트가 exit code 1로 종료되어 MaskWork.maskPanosreject(false)를 호출, PanoPostprocessorService.run이 capture state를 Error로 마킹했다.

Quick Facts#

Field Value
exception.class cv2.error (OpenCV 4.10.0)
exception.message (-215:Assertion failed) !_src.empty() in function 'cvtColor'
top_frame /tmp/lib/face-body-detector/cupix_inference.py:334 (called from :453)
upstream code applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:54
env production, ap-southeast-2

Affected Teams#

Team / Domain Error Count Impact
naylorlove / pano postprocessing (ap-southeast-2) 3 1개 capture의 pano mask 처리 실패 → capture가 pano_postprocessor_state=Error로 마킹됨. blur, infer, resize 후속 단계까지 모두 중단되어 mask/blurriness/resize 결과가 누락된다.

Timeline#

  1. 2026-06-24 14:08:58 KSTPanoPostprocessorService::run | begin — 3개 인스턴스가 동시에 동일 capture 처리를 시작 (Datadog: service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService::run | begin").
  2. 2026-06-24 14:11:49 KSTcupix_inference.pycv2.cvtColor assertion 실패로 stderr에 traceback 출력. mask-work.ts의 process.stderr.on('data') 핸들러가 이를 error 레벨로 로깅 (MaskWork::maskPanos | script Traceback ...).
  3. 2026-06-24 14:11:50 KST — bash blur 스크립트가 exit code 1로 종료. process.on('close', code=1)MaskWork::maskPanos | error code:1을 로깅하고 reject(false) 호출 (3건).
  4. 2026-06-24 14:11:50 KSTPanoPostprocessorService::run의 catch 블록이 capture state를 Error로 업데이트하고 run | end - false 로깅.
  5. 2026-06-24 14:11:52 KSTPanoPostprocessorService::terminateService | force shutdown after 10 seconds — 정상 종료 경로.

Error Log#

Datadog Logs

text
MaskWork::maskPanos | error code:1

선행 stderr traceback (동일 인스턴스, 1초 전):

Datadog log @ 2026-06-24 14:11:49 KSTtext
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)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: naylorlove
  • 발생 횟수: 3
  • 최초 발생: 2026-06-24 14:11:50 KST
  • 최근 발생: 2026-06-24 14:11:50 KST
  • Region: ap-southeast-2
  • Related cluster: aee7c647-f7f9-4d1d-8241-3a59b751ebca (같은 service의 동시 발생 인시던트 — status-board incident 2026-06-24-svc-cupixworks-pano-postprocessor-instance--unknown-1)

Root Cause Summary#

face/body blur 추론용 Python 스크립트 cupix_inference.py가 입력 pano 이미지를 OpenCV로 읽은 직후 cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)를 호출했는데, img_original이 비어 있어서 (cv2.imreadNone 또는 empty Mat을 반환) OpenCV의 내부 assertion !_src.empty()가 실패해 프로세스가 크래시했다. 자식 프로세스가 non-zero exit code로 종료되자 TypeScript 호출자 MaskWork.maskPanoserror code:1을 로깅하고 Promise를 reject했으며, 상위 PanoPostprocessorService.run이 이를 받아 capture 전체를 Error 상태로 마킹했다. 즉, root cause는 face-body-detector에 전달된 pano 입력 파일 중 최소 하나가 cv2가 디코딩하지 못하는 상태 (파일 누락, 0-byte, 또는 손상된 이미지)였다는 것이고, Node.js 측 코드는 자식 프로세스의 실제 실패 원인 (어떤 pano 파일이 문제였는지)을 보존하지 않은 채 일반화된 exit-code 에러로만 보고한다.

Technical Analysis#

Code Path#

  • Entry point: applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:80 (PanoPostprocessorService::run)
  • Mask step: pano-postprocessor-service.ts:123-127maskTypeOption이 있으면 maskWork.maskPanos(downloadDir, maskDir, optionPath) 호출
  • Spawn point: applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:29spawn('stdbuf', ['-oL', 'bash', BlurScriptPath], { env: { INPUT_DIRPATH, OUTPUT_DIRPATH, OPTION_FILE_PATH } })
  • BlurScriptPath: /tmp/lib/face-body-detector/run.sh (src/config/constants.ts:5)
  • Failure point in TypeScript: mask-work.ts:53-56 — child가 non-zero exit하면 reject(false)
  • Failure point in Python: /tmp/lib/face-body-detector/cupix_inference.py:334cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB) (이 repo에 소스 없음 — face-body-detector 외부 의존성, Docker 빌드 시 bin/get_dependency.sh face-body-detector로 가져온다)
applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:23-58typescript
maskPanos(inputDir: string, outputDir: string, optionPath: string): Promise<boolean> {
    return new Promise((resolve, reject) => {
        if (!fs.existsSync(outputDir)) {
            fs.mkdirSync(outputDir, { recursive: true });
        }

        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.stdout.on('data', (data) => {
            logger.debug('MaskWork::maskPanos | script %s', data.toString());
        });

        process.stderr.on('data', (data) => {
            logger.error('MaskWork::maskPanos | script %s', data.toString());
        });

        process.on('error', (err) => {
            logger.error('MaskWork::maskPanos | error %s', stringifyError(err));
            reject(err);
        });

        process.on('close', (code) => {
            if (code === 0) {
                logger.debug('MaskWork::maskPanos | success code:%d', code);
                resolve(true);
            } else {
                logger.error('MaskWork::maskPanos | error code:%d', code);
                reject(false);
            }
        });
    });
};
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:122-144typescript
// mask
if (maskTypeOption) {
    this.maskWork.createOptionFile(optionPath, maskTypeOption);
    start = Date.now();
    await this.maskWork.maskPanos(downloadDir, maskDir!, optionPath);
    this.panoPostprocessorManager.logElapsedTime(start, 'blur_panos');

    start = Date.now();
    const maskTasks = cpPanos.map((cpPano) => {
        return PARALLEL_TASK_LIMIT(async () => {
            try {
                await this.maskWork.changeMask(cpPano, maskTypeOption);
            } catch (error) {
                logger.error('PanoPostprocessorService::run | mask pano id:%d | error %s', cpPano.panoId, stringifyError(error));
                await this.panoPostprocessorManager.updatePanoState(cpPano.panoId!, TESLA.UpdatePanoRequest.StateEnum.Error);
                erroredPanoIds.add(cpPano.panoId!);
            }
        });
    });
    await Promise.all(maskTasks);
    ...
}

기대 동작: maskPanos는 입력 디렉터리의 모든 pano를 mask 처리한 뒤 exit 0으로 종료, await 다음 줄에서 logElapsedTime(start, 'blur_panos')가 로깅된다.

실제 동작: 자식 프로세스의 Python 스크립트가 첫 번째 (또는 N번째) pano 이미지를 처리하다가 cv2.cvtColor에서 크래시 → bash run.sh가 exit 1 → reject(false) → 상위 catch 블록이 capture 전체를 Error로 마킹. blur_panos Elapsed Time 로그가 누락된다는 점이 이를 뒷받침한다 (해당 시간대 Datadog 결과에 dd_step:blur_panos 없음).

또한 reject(false)는 안티패턴이다 — Error 객체가 아닌 false를 reject하기 때문에 상위 stringifyError(error) 호출 시 false 문자열만 남고 (실제 로그: PanoPostprocessorService::run | end - false), 실패한 pano ID, exit code, stderr 내용 등 진단에 필요한 컨텍스트가 전부 사라진다.

Log Evidence#

Datadog 쿼리 (재현용):

text
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos | error code:1"
text
service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService"

시간순 핵심 로그 (한 인스턴스 기준, 다른 2개도 동일 패턴):

Datadog logs — 14:08:58 ~ 14:11:52 KSTtext
14:08:58  info   PanoPostprocessorService::init
14:08:58  info   PanoPostprocessorService::authenticate | begin
14:08:58  info   PanoPostprocessorService::authenticate | end
14:08:58  info   PanoPostprocessorService::run | begin
...
14:11:49  error  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)
                   cv2.error: OpenCV(4.10.0) ... (-215:Assertion failed) !_src.empty() in function 'cvtColor'
14:11:50  error  MaskWork::maskPanos | error code:1
14:11:50  error  PanoPostprocessorService::run | end - false
14:11:52  info   PanoPostprocessorService::run | end
14:11:52  info   PanoPostprocessorService::terminateService | force shutdown after 10 seconds

해당 시간대 같은 service의 다른 stderr (참고로 cluster fingerprint와는 별도로 다수 발생 — 주로 PyTorch warning이 stderr에 출력되어 logger.error로 잘못 라우팅된 케이스):

text
MaskWork::maskPanos | script /tmp/lib/face-body-detector/cupix_inference.py:266: RuntimeWarning: overflow encountered in exp
  sigmoid_mask = 1.0 / (1.0 + np.exp(-logits_resized))

MaskWork::maskPanos | script /tmp/lib/face-body-detector/sam3/model/encoder.py:358: UserWarning: No PYTORCH_KERNEL_CACHE_PATH or HOME environment variable set! This disables kernel caching.

이 warning들은 fingerprint가 다른 별도 클러스터로 분류되지만, mask-work.ts:40-42가 stderr 전체를 logger.error로 출력하기 때문에 Datadog에서 error로 인덱싱된다 — 본 클러스터(error code:1)와는 직접 관련이 없지만 동일 코드 경로의 노이즈 원인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Python cv2.imread/cvtColor 단계에서 입력 pano 이미지가 비어 있어 OpenCV assertion 실패 → 자식 프로세스 exit 1 → error code:1 14:11:49 KST stderr traceback: (-215:Assertion failed) !_src.empty() in function 'cvtColor' at cupix_inference.py:334. 직후 14:11:50 KST error code:1 로깅. blur_panos elapsed time 로그가 누락됨 → maskPanos가 await 다음 줄까지 진행하지 못함. Confirmed
H2 BlurScriptPath(/tmp/lib/face-body-detector/run.sh)가 존재하지 않아 spawn 자체가 실패 validateBlurScriptPath()pano-postprocessor-service.ts:105에서 사전 검증, 실패하면 BlurScriptPath not found 로그가 남음. 해당 로그 없음. 또한 stderr에 Python traceback이 찍혔다는 것은 자식 프로세스가 정상적으로 실행되었음을 뜻함. Rejected
H3 spawn'error' 이벤트 (자식 프로세스 시작 실패) 'error' 핸들러는 `MaskWork::maskPanos error %s로 로깅하는데, 실제 로그는 error code:%d포맷이므로'close'` 핸들러가 작동한 것 — 자식이 정상적으로 실행되어 exit code를 반환했음을 의미.
H4 PyTorch UserWarning/RuntimeWarning이 stderr로 출력되어 logger.error로 라우팅된 것이 root cause 같은 시간대에 다수의 PyTorch warning이 error 레벨로 로깅됨. warning은 process exit code에 영향을 주지 않으며, error code:1은 자식 프로세스가 비정상 종료했음을 명확히 지시한다. 또한 본 클러스터의 fingerprint는 PyTorch warning과 별개. Rejected
H5 외부 dependency 장애 (S3, voxel-service 등) status-board는 svc: scope(서비스 내부 인시던트)로 분류, dep:* 활성 인시던트 없음. 에러 메시지에 외부 호출 흔적 없음. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  1. 실패 컨텍스트 보존applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:53-56reject(false)reject(new Error(...))로 변경하여 exit code, stderr 마지막 청크, 입력 디렉터리 경로를 메시지에 포함. 상위 catch 블록의 stringifyError(error)false만 출력하는 현재 동작을 막아 실패 pano를 식별할 수 있게 한다. (방향만 제시 — 구현 코드는 작성하지 않음)
  2. Python 측 입력 검증face-body-detector 저장소의 cupix_inference.py:333 직전에 cv2.imread 결과가 None이거나 빈 Mat인지 확인하고 (a) 해당 pano만 skip + 명확한 stderr 메시지 출력, (b) 비-zero exit 시에는 어떤 파일 때문인지 stderr에 기록하도록 보강. 이 부분은 별도 repo (face-body-detector, 본 작업트리에 소스 없음) 변경이 필요하므로 owner 팀에 위임. uncertain — needs verification: face-body-detector repo 위치 및 owner.

단기 개선 (1주 이내)#

  1. 다운로드 단계 무결성 검증AwsS3Manager.downloadParallel(cpPanos)mask-work.ts:23 호출 전에 inputDir의 각 pano 파일이 0-byte가 아닌지, 유효한 이미지 헤더를 가지는지 검사. 손상된 파일이 발견되면 해당 pano만 erroredPanoIds에 추가하고 mask 입력에서 제외한다. 현재는 1개 파일만 손상되어도 전체 capture가 Error 처리되는 all-or-nothing 동작.
  2. stderr 노이즈 분리mask-work.ts:40-42에서 stderr를 모두 logger.error로 출력하는 대신, Python warning (UserWarning, RuntimeWarning) 패턴은 logger.warn으로 다운그레이드하여 Datadog error 노이즈를 줄인다. 본 RCA 과정에서도 PyTorch warning과 실제 크래시가 같은 fingerprint group으로 묶이지 않은 것은 다행이지만 동일 service의 error 신호 비율이 왜곡된다.

장기 개선 (재발 방지)#

  1. per-pano 격리 실행 — 현재 blur 스크립트는 inputDir 전체를 한 번에 처리하며 첫 실패에서 멈춘다. pano 단위로 child process를 나누거나 (changeMasksByParallel와 유사한 패턴) Python 측에서 파일별 try/except로 격리하여, 1개 손상 pano가 capture 전체를 실패시키지 않도록 한다.
  2. 이미지 무결성 메트릭 — pano S3 업로드 시점의 무결성 체크 (SHA, 디코딩 성공 여부)를 capture 메타에 기록하여 postprocessor가 사전에 거를 수 있게 한다.

Monitoring#

MaskWork exit-code-1 발생 빈도 (timeseries)text
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:"MaskWork::maskPanos | error code:1"}.as_count()
cv2.cvtColor assertion 실패 (Python traceback) 발생 빈도text
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:"Assertion failed"}.as_count()
capture 단위 mask 실패 — run | end - falsetext
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:"PanoPostprocessorService::run | end - false"}.as_count()

알림: error code:1 1시간 누적이 10건 초과 또는 region별 비율이 5% 초과 시 naylorlove 팀 알림.

Risk Assessment#

  • Risk level: medium — 사용자 영향은 capture별이고 occurrence_count가 3 (단일 capture의 3개 동시 처리)에 그치지만, all-or-nothing 동작 때문에 1개 손상 파일이 전체 capture를 실패시킨다. 입력 파일 손상이 재발하면 동일 패턴으로 다시 실패한다.
  • 예상 복잡도: standard — Node.js 측 reject(false) 수정과 입력 무결성 검증은 routine. Python 측 (face-body-detector) 변경은 별도 repo이므로 owner 협업 필요.