ES /docs

PanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1:

RCA: PanoPostprocessorService::run | Blur script exited with code 1 (OpenCV CV_IO_MAX_IMAGE_PIXELS)

Overview#

What Happened#

2026-08-01 03:04 KST 에 cupixworks-pano-postprocessor-instance (production, us-west-2) 에서 pano 마스킹 파이프라인이 blur script 종료 코드 1 로 실패했다. 원인은 bundled Python 스크립트 /tmp/lib/face-body-detector/cupix_inference.py 에서 cv2.imread 호출 시 OpenCV 의 pixel 상한 (CV_IO_MAX_IMAGE_PIXELS, 기본 2^30 ≈ 1.073B pixels) 을 초과하는 pano 이미지가 있었기 때문이다. MaskWork::maskPanos 는 배치 스크립트 하나로 전체 pano 를 처리하므로 단 하나의 대용량 이미지가 스크립트를 중단시키고, 상위 PanoPostprocessorService::run 의 catch 블록이 capture 전체를 Error 상태로 전이시킨다.

Quick Facts#

Field Value
exception.class cv2.error (OpenCV assertion) — Node 측에서는 Error: Blur script exited with code 1
exception.message OpenCV(4.10.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:79: error: (-215:Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize'
top_frame /tmp/lib/face-body-detector/cupix_inference.py:333 in detect (img_original = cv2.imread(image_filepath))
runtime Python 3.11, opencv-python 4.10.0.84 (Dockerfile pin)
env production, us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cadscans (pano postprocessor) 2 (동일 capture 내 중복 emit) 해당 capture 의 pano_postprocessor_state 가 Error 로 전이되어 마스킹/blurriness/resize 단계 전체가 미완료 상태로 남는다 (pano-postprocessor-service.ts:190)

Timeline#

  1. 2026-08-01 03:04:50 KSTPanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1: ...cv2.error ..."} 두 번 기록됨 (동일 순간, 두 로그 라인 emit) — cluster first_seen == last_seen
  2. 2026-08-01 03:04:50 KSTrun() catch 블록이 updatePanoPostprocessorState(captureId, Error)updateJobState(Stopped) 실행 (pano-postprocessor-service.ts:188-197)

Error Log#

Datadog Logs

text
PanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1:
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 333, in detect
    img_original = cv2.imread(image_filepath)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:79: error: (-215:Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize'
    at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:7088:18)
    at ChildProcess.emit (node:events:524:28)
    at maybeClose (node:internal/child_process:1104:16)
    at ChildProcess._handle.onexit (node:internal/child_process:304:5)","message":"Blur script exited with code 1: ..."}

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: cadscans
  • 발생 횟수: 2 (동일 시각의 duplicate emit — 실제 capture 1건)
  • 최초 발생: 2026-08-01 03:04 KST
  • 최근 발생: 2026-08-01 03:04 KST

동일 서비스에서 최근 (2026-07-28) resolved incident 2026-07-28-svc-cupixworks-pano-postprocessor-instance--unknown-1 가 존재 (4개 cluster). 상태 보드에는 active incident 없음.

Root Cause Summary#

Bundled face-body-detector Python 스크립트가 cv2.imread 로 pano 원본을 로드할 때, 이미지의 총 픽셀 수가 OpenCV 의 컴파일 시 상수 CV_IO_MAX_IMAGE_PIXELS (기본값 2^30 = 1,073,741,824 pixels) 를 초과했다. OpenCV 는 이 조건에서 assertion 을 던지고 프로세스를 종료(exit 1) 시킨다. Node 측 MaskWork::maskPanos 는 자식 프로세스의 close 이벤트에서 code !== 0 이면 reject(false) 하고 (mask-work.ts:49-56), 상위 PanoPostprocessorService::run 의 mask 단계는 try/catch 로 감싸져 있지 않아 (pano-postprocessor-service.ts:126) 예외가 outer catch (error) 로 전파되며 capture 전체가 Error 상태로 마킹된다. 즉 (1) 입력 pano 하나가 OpenCV pixel 상한을 넘겼고, (2) 이 실패가 pano 단위로 격리되지 않아 capture 전체가 실패한 이중 문제다.

Technical Analysis#

Code Path#

Entry point: applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:79 (PanoPostprocessorService.run)

  1. run() 이 시작되어 capture 의 pano 목록을 가져오고 mask type 이 존재하면 maskWork.maskPanos(downloadDir, maskDir, optionPath) 를 호출한다.
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:123-127typescript
if (maskTypeOption) {
    this.maskWork.createOptionFile(optionPath, maskTypeOption);
    start = Date.now();
    await this.maskWork.maskPanos(downloadDir, maskDir!, optionPath);
    this.panoPostprocessorManager.logElapsedTime(start, 'blur_panos');

await 는 개별 try/catch 로 감싸져 있지 않다 — 아래 changeMask/infer/resize 는 pano 단위 try/catch 로 감싸져 있어 pano 하나가 실패해도 다른 pano 는 계속 처리되지만, maskPanos 는 batch 스크립트라 예외가 outer catch (188) 까지 그대로 propagate 된다.

  1. maskPanos 는 bash blur 스크립트를 spawn 하고, non-zero exit 시 reject(false) 한다.
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.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);
            }
        });
    });
};
  1. Blur 스크립트는 bundled Python cupix_inference.py 를 호출하며, 문제 라인은:
/tmp/lib/face-body-detector/cupix_inference.py:333text
img_original = cv2.imread(image_filepath)

OpenCV 는 loadsave.cpp:79validateInputImageSize 에서 pixels <= CV_IO_MAX_IMAGE_PIXELS assertion 을 실행한다. 기본값은 1 << 30 (약 1.07B pixels). 예: 32768×32768 이면 1.07B pixel 이므로 초과, 40000×40000 (1.6B pixels) 은 확실히 실패한다.

  1. Failure point (전파): pano-postprocessor-service.ts:188 catch 블록. 로그 문자열 PanoPostprocessorService::run | end - <stack> 이 이 클러스터의 대표 에러다.
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:188-199typescript
} catch (error) {
    hasError = true;
    await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Error);
    logger.error('PanoPostprocessorService::run | end - %s', stringifyError(error));
} finally {
    if (!hasError) {
        await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Done);
    }
    await this.panoPostprocessorManager.updateJobState(jobId!, TESLA.UpdateJobRequest.StateEnum.Stopped);
    this.panoPostprocessorManager.removeWorkspaceDir(this.workspaceDir);
    logger.info('PanoPostprocessorService::run | end');
}

Docker 이미지의 OpenCV 는 4.10.0.84 로 pinned 되어 있고 CV_IO_MAX_IMAGE_PIXELS 를 우회하는 환경 변수 (OPENCV_IO_MAX_IMAGE_PIXELS) 설정이 없다 — Dockerfile 및 constants.ts, agent 코드베이스 전체에 관련 참조 없음 (grep CV_IO_MAX_IMAGE_PIXELS|OPENCV_IO_MAX → no matches).

applications/agents/packages/cupix-pano-postprocessor/Dockerfile:47-71dockerfile
###############################################################################
# YOLO & SAM3
###############################################################################
RUN pip install --no-cache-dir \
  numpy==2.1.1 \
  opencv-python==4.10.0.84 \
  opencv-python-headless==4.10.0.84 \
  ...

기대 동작: 대용량 pano 하나가 있어도 해당 pano 만 skip/error 로 처리되고 나머지 pano 및 capture 파이프라인은 완료되어야 한다. 실제 동작: 배치 스크립트가 중단되어 mask 단계 전체가 실패하고, capture 가 통째로 Error 상태가 된다.

Log Evidence#

Datadog query (cluster URL 그대로):

text
service:cupixworks-pano-postprocessor-instance status:error @environment:production "PanoPostprocessorService::run"

Time window: 2026-07-31T17:04:00Z ~ 2026-07-31T19:05:00Z

핵심 error log (2건, 동일 순간 emit — 상단 Error Log 섹션 참조).

주변 컨텍스트 로그 (동일 서비스, service:cupixworks-pano-postprocessor-instance @environment:production 로 검색):

  • 인접 시간대 (같은 서비스의 다른 run 들) 는 정상 처리되어 PanoPostprocessor Metric - dd_metric:pano.postprocess.summary&dd_total:300&dd_completed:300&dd_errored:0 처럼 completed=total 로 종료.
  • Mask 스크립트의 다른 로그들 (같은 인프라, warn 성격):
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))
text
MaskWork::maskPanos | script /tmp/lib/face-body-detector/sam3/model/encoder.py:358: UserWarning: Specified kernel cache directory could not be created! ...

이들은 오늘 문제와 무관한 별개 warning (stderr → error 레벨로 승격) 이며, cluster 의 fingerprint (62dc74b90cde9fac79fa5d8553c39790) 와 다르다. cluster 자체는 Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS 로 국한된다.

Related recent svc:* incident (from status-board):

text
2026-07-28-svc-cupixworks-pano-postprocessor-instance--unknown-1 (resolved 2026-07-28T16:40:26Z)

cluster_ids: be9c3f17, 4dfce574, a5ec76bc, ee047ed2 — 같은 서비스의 별개 근본원인 여부는 이 RCA 범위 밖.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 입력 pano 하나가 OpenCV CV_IO_MAX_IMAGE_PIXELS (2^30) 를 초과해 assertion 발생, blur 스크립트 exit 1, 상위에서 capture 전체 Error 로 전파 stack trace 명시적: pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize' at cv2.imread. mask-work.ts:53-55 가 non-zero exit → reject. pano-postprocessor-service.ts:126await maskPanos 는 pano 단위 격리 없음 Confirmed
H2 Corrupt/malformed 이미지로 인해 cv2.imread 가 실패 (일반 IO 에러) cv2.imread 는 실패 시 None 반환이 일반적 assertion 메시지가 명확히 pixel count 조건. loadsave.cpp:79validateInputImageSize — IO 파싱이 아니라 사이즈 검증 단계 Rejected
H3 Face-body-detector 버전 업그레이드 회귀 face-body-detector.json 은 production stable: 1.2.3 최근 버전 변경 로그 없음. 다른 capture 는 정상 처리 (total:300 completed:300 errored:0 metric 확인) — 동일 이미지가 아니면 재현 안됨 Rejected
H4 GPU/CUDA OOM 또는 kernel cache 실패 encoder.py 의 Specified kernel cache directory could not be created warning 존재 이 warning 은 stderr 로 나오지만 스크립트를 종료시키지 않음. Cluster 의 실제 종료 원인은 명시적 assertion trace Rejected
H5 MaskWork::maskPanos 배치 실패가 capture 전체를 Error 로 만드는 격리 부족 (2차 원인) pano-postprocessor-service.ts:123-128changeMask/infer/resize 와 달리 개별 try/catch 로 감싸져 있지 않음. Batch bash script 특성상 첫 실패 이미지에서 전체 중단 Confirmed (contributory)

Fix Recommendation#

즉시 조치 (Critical)#

  • 환경변수로 OpenCV pixel 상한 상향: packages/cupix-pano-postprocessor/Dockerfile 또는 run.sh / bundled blur run.sh 에서 OPENCV_IO_MAX_IMAGE_PIXELS 를 명시적으로 지정. OpenCV 는 이 env var 로 컴파일 시 상수를 런타임 오버라이드할 수 있다. Pano 는 equirectangular 로 흔히 16384×8192 (134M) ~ 32768×16384 (537M) 이지만, 초고해상도 capture 는 2^30 을 넘길 수 있으므로 예: 2^33 = 8,589,934,592 정도로 여유롭게 설정. 주의: 값이 커질수록 malformed/adversarial 이미지에 대한 메모리 안전 마진이 줄어드는 tradeoff — QA 팀과 상한 값 협의 필요.
  • face-body-detector 팀에 사전 사이즈 체크 도입 요청: cupix_inference.py:333 이전에 PIL.Image.open 또는 imghdr 로 dimension 을 확인해 상한 초과 시 skip 하고 non-fatal 리턴 (예: exit code 0 이되 결과 파일에 skip marker). 이 저장소 (face-body-detector) 는 별도 repo 이므로 자동 code-fix 스코프에서 제외 — cadscans 팀과 조율 필요.

단기 개선 (1주 이내)#

  • MaskWork::maskPanos 실패 격리: pano-postprocessor-service.ts:123-128try/catch 로 감싸고 mask 단계 실패 시 mask 를 스킵하되 infer/resize 는 계속 진행하도록 변경. 또는 batch script 대신 pano 단위 spawn 으로 리팩터링해 실패 이미지만 error 처리. 후자는 blur 스크립트 인터페이스 변경이 필요하므로 face-body-detector 팀과 조율 필요.
  • 재현 데이터 확보: 이번 실패 capture 의 pano 이미지 dimension 을 조사해 실제 어떤 크기가 문제인지 확인 (S3 원본 metadata 조회). Cluster 에는 capture_id/pano_id 가 로그로 남지 않아 자동 추적이 어려움 — logger.error 문에 captureId/panoId 를 반드시 포함하도록 관측성 개선.

장기 개선 (재발 방지)#

  • 업로드 단계 pre-validation: capture 업로드 파이프라인에서 pano dimension 을 검증하고 상한 초과 이미지는 사전에 리사이즈 또는 거부. Postprocessor 에서 사후 실패보다 사용자에게 즉시 피드백이 낫다.
  • 관측성: PanoPostprocessorService::run 로그에 captureId, 실패 panoId, 이미지 dimension 을 구조화 필드로 포함. 현재 대표 에러 로그에는 어떤 이미지/capture 인지 확인할 방법이 없다.

Monitoring#

  • Alert: pano-postprocessor 에서 OpenCV pixel 상한 위반 발생 시 즉시 알림.
  • Trend: capture 실패율 및 실패 원인 분류 dashboard.

Datadog queries (writing-datadog-monitoring-queries 규칙 준수 — timeseries 위젯 호환 형태):

text
service:cupixworks-pano-postprocessor-instance status:error @environment:production "CV_IO_MAX_IMAGE_PIXELS"
text
service:cupixworks-pano-postprocessor-instance status:error @environment:production "Blur script exited"
text
service:cupixworks-pano-postprocessor-instance status:error @environment:production "PanoPostprocessorService::run | end"

Risk Assessment#

  • Risk level: medium — 개별 capture 처리 실패로 유저가 postprocess 완료를 못 받음. 데이터 손실은 없고 원본은 S3 에 존재. 하지만 실패 격리가 미흡해 blast radius 가 pano 1개 → capture 1개 전체로 확대되는 구조적 문제 포함.
  • 예상 복잡도: standard — env var 추가는 trivial 이나, 근본적 격리 개선 및 face-body-detector 팀과의 조율이 필요해 표준 복잡도로 분류.