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.py가 cv2.cvtColor assertion 실패 (!_src.empty())로 크래시했고, 그 결과 spawn된 bash 스크립트가 exit code 1로 종료되어 MaskWork.maskPanos가 reject(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#
- 2026-06-24 14:08:58 KST —
PanoPostprocessorService::run | begin— 3개 인스턴스가 동시에 동일 capture 처리를 시작 (Datadog:service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService::run | begin"). - 2026-06-24 14:11:49 KST —
cupix_inference.py가cv2.cvtColorassertion 실패로 stderr에 traceback 출력. mask-work.ts의process.stderr.on('data')핸들러가 이를error레벨로 로깅 (MaskWork::maskPanos | script Traceback ...). - 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건). - 2026-06-24 14:11:50 KST —
PanoPostprocessorService::run의 catch 블록이 capture state를Error로 업데이트하고run | end - false로깅. - 2026-06-24 14:11:52 KST —
PanoPostprocessorService::terminateService | force shutdown after 10 seconds— 정상 종료 경로.
Error Log#
MaskWork::maskPanos | error code:1
선행 stderr traceback (동일 인스턴스, 1초 전):
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 incident2026-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.imread가 None 또는 empty Mat을 반환) OpenCV의 내부 assertion !_src.empty()가 실패해 프로세스가 크래시했다. 자식 프로세스가 non-zero exit code로 종료되자 TypeScript 호출자 MaskWork.maskPanos가 error 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-127—maskTypeOption이 있으면maskWork.maskPanos(downloadDir, maskDir, optionPath)호출 - Spawn point:
applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:29—spawn('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:334—cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)(이 repo에 소스 없음 —face-body-detector외부 의존성, Docker 빌드 시bin/get_dependency.sh face-body-detector로 가져온다)
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);
}
});
});
};
// 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 쿼리 (재현용):
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos | error code:1"
service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService"
시간순 핵심 로그 (한 인스턴스 기준, 다른 2개도 동일 패턴):
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로 잘못 라우팅된 케이스):
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)#
- 실패 컨텍스트 보존 —
applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:53-56의reject(false)를reject(new Error(...))로 변경하여 exit code, stderr 마지막 청크, 입력 디렉터리 경로를 메시지에 포함. 상위 catch 블록의stringifyError(error)가false만 출력하는 현재 동작을 막아 실패 pano를 식별할 수 있게 한다. (방향만 제시 — 구현 코드는 작성하지 않음) - 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주 이내)#
- 다운로드 단계 무결성 검증 —
AwsS3Manager.downloadParallel(cpPanos)후mask-work.ts:23호출 전에inputDir의 각 pano 파일이 0-byte가 아닌지, 유효한 이미지 헤더를 가지는지 검사. 손상된 파일이 발견되면 해당 pano만 erroredPanoIds에 추가하고 mask 입력에서 제외한다. 현재는 1개 파일만 손상되어도 전체 capture가 Error 처리되는 all-or-nothing 동작. - stderr 노이즈 분리 —
mask-work.ts:40-42에서 stderr를 모두logger.error로 출력하는 대신, Python warning (UserWarning,RuntimeWarning) 패턴은logger.warn으로 다운그레이드하여 Datadog error 노이즈를 줄인다. 본 RCA 과정에서도 PyTorch warning과 실제 크래시가 같은 fingerprint group으로 묶이지 않은 것은 다행이지만 동일 service의 error 신호 비율이 왜곡된다.
장기 개선 (재발 방지)#
- per-pano 격리 실행 — 현재 blur 스크립트는 inputDir 전체를 한 번에 처리하며 첫 실패에서 멈춘다. pano 단위로 child process를 나누거나 (
changeMasksByParallel와 유사한 패턴) Python 측에서 파일별 try/except로 격리하여, 1개 손상 pano가 capture 전체를 실패시키지 않도록 한다. - 이미지 무결성 메트릭 — pano S3 업로드 시점의 무결성 체크 (SHA, 디코딩 성공 여부)를 capture 메타에 기록하여 postprocessor가 사전에 거를 수 있게 한다.
Monitoring#
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:"MaskWork::maskPanos | error code:1"}.as_count()
sum:logs.hits{service:cupixworks-pano-postprocessor-instance,status:error,@message:"Assertion failed"}.as_count()
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 협업 필요.