ES /docs

MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)

RCA: MaskWork::maskPanos | cv2.cvtColor assertion failure

Overview#

What Happened#

2026-05-08 20:20~20:55 UTC 사이에 cupixworks-pix-genie-preprocessor-instance 서비스에서 pano 이미지 마스킹 처리 중 OpenCV cvtColor 함수가 빈 이미지를 받아 assertion failure가 발생했다. 총 9건의 에러가 기록되었으며, 다수의 capture가 병렬 처리 중이었다.

Quick Facts#

Field Value
exception.class cv2.error
exception.message OpenCV(4.10.0) error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
top_frame /tmp/lib/face-body-detector/cupix_inference.py:334
runtime Python 3.11, OpenCV 4.10.0, Node.js (agent)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
smartcoms / pano-postprocessor 9 마스킹 실패한 pano는 face/body blur 처리가 적용되지 않음

Timeline#

  1. 2026-05-08T20:20:29Z — 첫 번째 cvtColor assertion failure 발생 (error code:1)
  2. 2026-05-08T20:55:36Z — 두 번째 batch에서 동일 에러 재발생
  3. 2026-05-09 — Error-sweeper 감지 및 RCA 수행

Error Log#

Datadog Logs

text
MaskWork::maskPanos | script     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-pix-genie-preprocessor-instance
  • Team: smartcoms
  • 발생 횟수: 9
  • 최초 발생: 2026-05-08T20:55:36.081Z
  • 최근 발생: 2026-05-08T20:55:36.081Z

Root Cause Summary#

AwsS3Manager::downloadParallel에서 S3 NoSuchKey 에러("The specified key does not exist.")로 개별 pano 이미지 다운로드가 실패했다. 에러를 catch하고 로그만 남긴 후 계속 진행하기 때문에, 다운로드 실패한 pano 파일이 디스크에 존재하지 않는 상태에서 maskPanos가 Python face-body-detector 스크립트를 실행했다. cv2.imread()가 존재하지 않는 파일에 대해 None을 반환하고 이것이 cv2.cvtColor()에 전달되어 !_src.empty() assertion failure가 발생했다.

Kibana(Watch) debug 로그 분석 결과, 두 capture 모두 v0 버전 키를 가진 pano가 각 1건씩 존재했으며, 해당 S3 key가 존재하지 않아 다운로드가 실패한 것이 확인되었다:

  • capture_694181resources/hyypt8/uswe1/v0pano_83949969.jpg
  • capture_694180resources/obszdo/uswe1/v0pano_83954507.jpg

v0 키의 의미: Tesla API의 Resource 모델에서 v0은 리소스가 생성만 되고 아직 이미지가 업로드되지 않은 초기 상태(revision=0)를 의미한다. 업로드는 v1(revision+1) 경로로 수행되며, 업로드 완료 확인(check_uploading) 후 revision이 1로 증가한다. 두 pano 모두 Kibana에서 resource_state: "created", state: "abandoned", filesize: null로 확인되어, 이미지가 S3에 업로드된 적이 없다.

abandoned 상태의 의미: Tesla state machine(statable/pano.rb:83-85)에서 abandoned 이벤트는 created 또는 resource_missing 상태에서만 전이 가능하다. CaptureInvoker#create_capture(capture_invoker.rb:29-31)가 capture 처리 시작 시점에 not_processible pano(state: created | resource_missing)를 일괄 abandoned로 마킹한다. 즉 abandoned는 업로드 미완료로 인해 처리 불가능한 pano를 정리하는 상태이다. default_scope(statable/pano.rb:8)가 abandoned pano를 API 쿼리에서 자동 제외하지만, pano postprocessor가 실행되는 시점에 아직 created 상태인 pano가 API 응답에 포함될 수 있다(abandoned 마킹 전 race condition).

근본 원인은 업로드 미완료 pano가 후처리 파이프라인에 포함된 것이며, pix-genie에서 이러한 pano를 다운로드 전에 필터링하여 제외하고 나머지만 처리하는 것이 올바른 접근이다.

Technical Analysis#

Code Path#

  • Entry point: pano-postprocessor-service.ts:79run() 메서드
  • Download step: pano-postprocessor-service.ts:117awsS3Manager.downloadParallel(cpPanos) 호출
  • Download failure handling: aws-s3.manager.ts:75-77 — 에러 catch 후 로그만 남기고 진행 (re-throw 없음)
  • Mask step: pano-postprocessor-service.ts:126maskWork.maskPanos(downloadDir, maskDir!, optionPath) 호출
  • Spawn: mask-work.ts:29-33 — Python 스크립트를 child process로 실행
  • Failure point: /tmp/lib/face-body-detector/cupix_inference.py:334cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)

1단계: 다운로드 실패 무시

applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:60-84typescript
const tasks = cpPano.map(cpPano => {
    return limit(async () => {
        try {
            // ... download logic
            if (cpPano.panoBucketEndpoint) {
                await this.downloadByPanoApi(cpPano);
            } else {
                await this.download(
                    cpPano.panoBucketRegion!,
                    cpPano.panoBucketName!,
                    cpPano.panoBucketKey!,
                    cpPano.downloadImagePath!
                );
            }
        } catch (err) {
            logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s',
                cpPano.downloadImagePath, (err as Error).message);
            // 에러를 삼키고 계속 진행 — 파일이 디스크에 없는 상태로 남음
        }
    });
});
await Promise.all(tasks);

기대 동작: 다운로드 실패 시 해당 pano를 마스킹 대상에서 제외하거나 전체 프로세스를 중단해야 한다. 실제 동작: 에러를 삼키고 모든 다운로드가 성공한 것처럼 다음 단계(maskPanos)로 진행한다.

2단계: 마스킹 스크립트가 디렉토리 내 모든 파일을 순회

applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:23-33typescript
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 }
        });

maskPanosinputDir (download 디렉토리)을 Python 스크립트에 전달한다. Python 스크립트(cupix_inference.py)는 해당 디렉토리의 모든 이미지 파일을 순회하며 cv2.imread()로 로드한다.

부가 이슈: env 옵션에 ...process.env가 없어 PATH, HOME, LD_LIBRARY_PATH 등 부모 환경변수가 전달되지 않는다. 그러나 stdbufbash가 절대 경로에 있고 Python 의존성이 시스템 전역으로 설치되어 있으므로 실행 자체는 가능하다. 이 문제는 향후 다른 환경변수 의존성 추가 시 잠재적 위험이다.

3단계: Python 스크립트에서 assertion failure

/tmp/lib/face-body-detector/cupix_inference.py:334 (from stack trace)text
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)

detect() 함수의 line 334에서 cv2.imread()로 읽은 결과를 cv2.cvtColor()에 바로 전달한다. cv2.imread()는 파일이 없거나 읽기 실패 시 예외를 발생시키지 않고 None을 반환하는 것이 OpenCV의 알려진 동작이다. NonecvtColor에 전달되면 C++ 레벨에서 !_src.empty() assertion이 실패한다.

4단계: Tesla Resource 버전 모델 — v0은 업로드 미완료 상태

tesla/app/models/concerns/storagable/resource.rb:21-23ruby
def upload_revision
  self.revision + 1
end

리소스 생성 시 default_initial_revision은 0이다 (revisionable/pano.rb:15-17). 업로드 URL은 항상 revision + 1 (즉 v1) 경로로 발급된다.

tesla/app/models/concerns/storagable/resource.rb:163-183ruby
def check_uploading
  _revision = self.revision
  _object = self.object(_revision + 1)

  if _object.exists?
    # ... etag, size 확인
    self.increase_revision  # revision 0 → 1
    self.done
    true
  else
    self.missing
    false
  end
end

업로드 확인 시 v1 경로에 S3 객체가 존재하면 revision을 1로 증가시킨다. v0 키가 API에서 반환된다는 것은 check_uploading이 한 번도 성공하지 못했음을 의미한다 — 즉 S3에 v0에도 v1에도 실제 이미지 데이터가 없다.

tesla/app/models/concerns/storagable/resource.rb:185-188ruby
def download_url(opts = {})
  ver = opts[:ver] || self.revision
  raise Cupix::Errors::Resource.new(code: 'ENT10011', reason: "Resource does not uploaded: #{ver}") if ver.zero?

download_url 메서드는 ver.zero?일 때 명시적으로 에러를 발생시킨다 — v0 리소스는 다운로드가 허용되지 않는 설계이다.

Kibana DB 확인 결과:

Pano ID resource_state state filesize 결론
83949969 created abandoned null 업로드 미완료 → abandoned 처리됨
83954507 created abandoned null 업로드 미완료 → abandoned 처리됨

두 pano 모두 resource_state: "created" (업로드 시작조차 안 됨), filesize: null이다. v1 경로로 fallback 다운로드를 시도해도 S3에 객체가 없으므로 동일한 NoSuchKey 에러가 발생한다.

5단계: abandoned 상태 전이 경로

tesla/app/models/concerns/statable/pano.rb:83-85ruby
event :abandoned do
  transition %i[created resource_missing] => :abandoned
end

abandoned 이벤트는 created 또는 resource_missing 상태에서만 발생 가능하다. 호출 지점은 두 곳:

tesla/app/invokers/capture_invoker.rb:29-31ruby
if @model.panos.not_processible.exists?
  @model.panos.not_processible.find_each(&:abandoned_state!)
end
tesla/app/models/concerns/processible_capture/resumable.rb:71-77ruby
if self.panos.not_processible.exists?
  not_processible_panos = self.panos.not_processible
  _panos_count = not_processible_panos.count
  not_processible_panos.each(&:abandoned_state!)
end

not_processible 스코프는 where(state: %i[created resource_missing])(statable/pano.rb:15)로 정의된다. 즉 업로드가 완료되지 않은 pano를 capture 처리/재개 시점에 abandoned로 마킹하는 것이다. resource.rb:78-82check_uploading 클래스 메서드가 15분 이상 created/uploading 상태인 리소스를 missing으로 전환한다.

6단계: pix-genie의 pano 필터링 부재

applications/agents/packages/cupix-pano-postprocessor/src/manager/pano-postprocessor-manager.ts:29-32typescript
async getCPPanosByCaptureId(captureId: number): Promise<CPPano[]> {
    const srvPanos = await this.cupixApi.pano.getAll(captureId);
    return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));
};

현재 필터링 조건은 state !== 'done'뿐이다. API 응답에 resource_state 필드가 포함되어 있으나(cupix-fields.ts:153), 이를 기반으로 업로드 미완료 pano를 제외하는 로직이 없다. CPPano 모델(cppano.ts:23-40)도 resource_state를 저장하지 않고 S3 bucket 정보만 추출한다.

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-pix-genie-preprocessor-instance status:error @environment:production "MaskWork::maskPanos"

에러 로그 (20:20:29Z):

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)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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 | error code:1

활성 capture 정보 (info 로그):

text
PreprocessorService::run | loaded capture - id: 694020, name: eval_denison_37775 (20:13:44Z)
PixGenieManager::executePixGenieProcessing | begin - capture: 694033 (20:12:01Z)
PreprocessorService::run | loaded capture - id: 41024, name: eval_denison_37793 (20:37:06Z)
PreprocessorService::run | loaded capture - id: 694032, name: eval_melonba_hs_43013 (20:40:55Z)

다운로드 실패 관련 에러 로그 (Kibana/Watch — logstash-processing* 인덱스):

text
[2026-05-08T20:13:42.983Z] [error] capture:694181, job:1059761
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."
text
[2026-05-08T20:42:40.407Z] [error] capture:694180, job:1059860
AwsS3Manager::downloadParallel | begin - "The specified key does not exist."

v0 키를 가진 pano의 debug 로그 (다운로드 시도):

text
[2026-05-08T20:13:42.893Z] [debug] capture_694181
AwsS3Manager::downloadParallel | buket region, name, key, endpoint, download path us-west-1 cupixworks-source-888a512bf858-uswe1 resources/hyypt8/uswe1/v0 null /tmp/workspace/capture_694181/download/pano_83949969.jpg
text
[2026-05-08T20:42:40.274Z] [debug] capture_694180
AwsS3Manager::downloadParallel | buket region, name, key, endpoint, download path us-west-1 cupixworks-source-888a512bf858-uswe1 resources/obszdo/uswe1/v0 null /tmp/workspace/capture_694180/download/pano_83954507.jpg

참고: Kibana의 error 로그 메시지 포맷(downloadParallel | begin - "The specified key does not exist.")이 코드의 catch 블록 포맷(download failed - pano: %s, error: %s)과 다르다. 이는 로깅 프레임워크의 메시지 interpolation 이슈이거나 프로덕션 배포 코드의 차이로 추정된다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 다운로드 실패한 pano 파일이 디스크에 없는 상태에서 Python 스크립트가 해당 파일을 imread하여 None 반환 aws-s3.manager.ts:75-77에서 다운로드 에러를 삼키는 코드 확인. cv2.imread()는 파일 미존재 시 None 반환하는 것이 OpenCV 문서화된 동작. 스크립트가 exit code 1로 종료. Kibana에서 S3 NoSuchKey 에러 로그 2건 확인capture_694181(20:13:42Z), capture_694180(20:42:40Z). 각 capture에 v0 버전 키 pano가 1건씩 존재하여 S3에서 key not found 발생. 동일 시간대에 명시적 download failed 에러 로그는 Datadog에서 미확인 → Kibana error 레벨에서 확인됨 (Datadog에는 미전송) Confirmed
H2 이미지 파일이 다운로드되었으나 손상(corrupt)되어 imread가 None 반환 cv2.imread()는 디코딩 실패 시에도 None 반환 가능 9건 모두 동일 에러로 corrupt 파일이 여러 건 동시에 발생하기 어려움. 네트워크 이슈로 partial download 시 파일이 0 byte이거나 미존재하는 것이 더 일반적 Inconclusive
H3 env 옵션에서 process.env 누락으로 Python 라이브러리 로딩 실패 mask-work.ts:32에서 process.env 미포함 확인. pixgenie.process.ts:66과 비교하면 올바른 패턴은 ...process.env spread SAM3 모델이 정상 로드되어 UserWarning을 출력한 것이 로그에서 확인됨 — Python 및 라이브러리 자체는 정상 동작 Rejected
H4 디스크 공간 부족으로 다운로드된 파일이 0 byte 다수의 capture가 병렬 처리 중이어서 workspace가 찰 수 있음 workspace는 capture별로 분리되고, 다른 capture는 정상 완료됨 Rejected

Fix Recommendation#

즉시 조치 (Critical) — pix-genie에서 업로드 미완료 pano 제외#

pano-postprocessor-manager.ts:29-32getCPPanosByCaptureId()에서 resource_stateuploaded가 아닌 pano를 다운로드 대상에서 제외한다. API 응답에 이미 resource_state 필드가 포함되어 있으므로(cupix-fields.ts:153) 추가 API 호출 없이 클라이언트 사이드 필터링으로 구현 가능하다.

typescript
// 현재: state !== 'done' 만 필터링
return srvPanos.filter((srvPano) => srvPano.state !== 'done').map((srvPano) => new CPPano(srvPano));

// 변경: resource_state가 uploaded가 아닌 pano도 제외
return srvPanos
    .filter((srvPano) => srvPano.state !== 'done' && srvPano.resource_state === 'uploaded')
    .map((srvPano) => new CPPano(srvPano));

이렇게 하면 resource_state: "created" (업로드 미완료), resource_state: "missing" (업로드 확인 실패), resource_state: "uploading" (업로드 진행 중) pano가 모두 다운로드 및 마스킹 대상에서 제외되어, S3 NoSuchKey 에러가 원천 차단된다. 나머지 정상 pano는 그대로 프로세싱을 진행한다.

  • v0→v1 fallback은 해결책이 아님: v0은 업로드 미완료 상태를 의미하며 v1에도 S3 객체가 없다.
  • abandoned 상태 기반 필터링은 불충분: Tesla default_scope(statable/pano.rb:8)가 abandoned pano를 API에서 이미 제외하지만, postprocessor 실행 시점에 아직 created 상태인 pano가 포함될 수 있다. resource_state 기반 필터링이 더 신뢰성 있다.

단기 개선 (1주 이내)#

  • aws-s3.manager.ts:75-77 — 다운로드 실패 시 해당 pano를 실패 목록에 추가하고, 후속 단계(mask, infer, resize)에서 제외하는 방어 로직 추가. 현재는 에러를 삼키고 진행하기 때문에 resource_state 필터링을 우회하는 다른 다운로드 실패 케이스에도 대응 가능.
  • mask-work.ts:32env 옵션에 ...process.env를 spread하여 부모 환경변수를 자식 프로세스에 전달. pixgenie.process.ts:66의 패턴을 참조.
  • Python 스크립트(cupix_inference.py:334)에 cv2.imread() 결과 None 체크를 추가하여, 개별 파일 로드 실패 시 skip하고 나머지 파일은 정상 처리하도록 방어 코드 추가.

장기 개선 (재발 방지)#

  • 개별 pano 실패 시 해당 pano만 에러 처리하고 나머지는 계속 진행하는 partial failure 패턴 적용 (현재 maskPanos는 전체 directory를 일괄 처리하므로 한 건 실패 시 전체 실패)

Monitoring#

  • AwsS3Manager::downloadParallel | download failed 로그에 대한 알림 추가:
text
service:cupixworks-pix-genie-preprocessor-instance "download failed"
  • MaskWork::maskPanos | error code 발생 빈도 모니터링:
text
service:cupixworks-pix-genie-preprocessor-instance "MaskWork::maskPanos | error code"

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — 다운로드 실패 추적 로직 추가 및 Python 스크립트 방어 코드 필요

Revision History#

Revision 1#

Feedback: 동일 시간대 debug 레벨 로그를 Kibana(Watch)에서 검색하여 AwsS3Manager::downloadParallel | download failed 로그의 실제 에러 메시지를 확인 요청

판정:

피드백 항목 판정 근거
Kibana debug 로그에서 download failed 실제 에러 메시지 확인 수용 Kibana logstash-processing* 인덱스에서 error 레벨 로그 2건 확인: `AwsS3Manager::downloadParallel

변경 사항:

  • ## Root Cause Summary: 일반적 "다운로드 실패"에서 구체적 S3 NoSuchKey 에러로 업데이트. 실패한 v0 키 pano 2건(pano_83949969, pano_83954507) 명시.
  • ### Log Evidence: Kibana 에러 로그 2건 + debug 로그(v0 키 pano 다운로드 시도) 추가. 기존 "uncertain" 문구를 실제 증거로 대체.
  • ## Hypotheses Considered: H1 판정 근거를 Kibana 증거로 보강. "Datadog에서 미확인" → "Kibana error 레벨에서 확인됨"으로 수정.

추가 조사 내용:

  • Kibana logstash-processing* 인덱스에서 cupixworks-pix-genie-preprocessor-instance 서비스의 2026-05-08 20:00~21:00 UTC 구간 로그 검색
  • downloadParallel 키워드로 debug/error 레벨 로그 총 1,401건 중 error 2건 식별
  • "specified key does not exist" 정확한 문구 검색으로 S3 NoSuchKey 에러 2건 확인
  • v0 버전 키 pano 검색으로 실패 원인 pano 특정 (capture_694181: resources/hyypt8/uswe1/v0, capture_694180: resources/obszdo/uswe1/v0)
  • aws-s3.manager.ts 소스 코드의 downloadParallel 메서드 catch 블록 확인 — 코드상 로그 포맷(download failed - pano: %s, error: %s)과 Kibana 실제 로그 포맷(downloadParallel | begin - "...")이 불일치하나 에러 내용은 동일

Revision 2#

Feedback: v0 인걸 v1 으로 판단해서 다운로드를 시도하면 되는지 확인

판정:

피드백 항목 판정 근거
v0 키를 v1로 변환하여 다운로드 시도하면 해결되는지 거부 Tesla API의 Resource 버전 모델에서 v0은 업로드 미완료 초기 상태를 의미. upload_revisionself.revision + 1로 항상 v1 경로에 업로드됨 (storagable/resource.rb:21-23). check_uploading (resource.rb:163-183)이 v1에서 S3 객체를 확인 후 revision을 1로 증가시키는데, API가 v0 키를 반환한다는 것은 check_uploading이 성공한 적이 없음을 의미 — v1에도 S3 객체가 없다. Kibana DB 확인: 두 pano 모두 resource_state: "created", filesize: null — 이미지 업로드 자체가 수행되지 않음. 또한 download_url 메서드(resource.rb:188)가 ver.zero?일 때 명시적으로 에러를 raise하는 것도 v0이 다운로드 불가 상태임을 입증.

변경 사항:

  • ## Root Cause Summary: v0 키의 의미 설명 추가 — v0은 업로드 미완료 상태이며, v0→v1 fallback도 불가함을 명시.
  • ### Code Path: "4단계: Tesla Resource 버전 모델" 섹션 추가 — upload_revision, check_uploading, download_url의 코드 경로와 Kibana DB 확인 결과를 포함.
  • ## Fix Recommendation: v0→v1 fallback이 해결책이 아닌 이유 명시, resource_state 필터링을 올바른 접근법으로 제시.

추가 조사 내용:

  • Tesla API 코드 탐색: storagable/resource.rb (upload_revision, check_uploading, download_url), decorators/resource.rb (object_key), revisionable/pano.rb (default_initial_revision), revisionable.rb (increase_revision)
  • pano_serializer.rb:100-102 — API 응답에서 storage_bucket_keyresource.object_key (현재 revision 기반)로 동적 생성됨을 확인
  • cppano.ts:34 — agent가 pano.resources[0]['storage_bucket_key']를 그대로 사용하여 버전 선택 로직 없음을 확인
  • Kibana panos 인덱스에서 pano_83949969, pano_83954507 조회 — 둘 다 resource_state: "created", state: "abandoned", filesize: null

Revision 3#

Feedback: state: "abandoned" 이 언제 발생하는지 확인. 파일 업로드 실패 케이스라면 pix-genie에서 다운로드 받지 않고, 해당 pano를 빼고 나머지 프로세싱을 진행하도록 변경.

판정:

피드백 항목 판정 근거
state: "abandoned" 발생 시점 확인 수용 Tesla state machine(statable/pano.rb:83-85)에서 abandoned 이벤트는 created 또는 resource_missing 상태에서만 전이 가능. 호출 지점 2곳 확인: (1) capture_invoker.rb:29-31 — capture 처리 시작 시 not_processible pano를 일괄 abandoned 마킹, (2) resumable.rb:71-77 — capture resume 시 cleanup_materials에서 동일 처리. not_processible 스코프는 where(state: %i[created resource_missing])(statable/pano.rb:15). 리소스 레벨에서는 resource.rb:78-82check_uploading 클래스 메서드가 15분 이상 created/uploading 상태인 리소스를 missing으로 전환. 즉 abandoned는 업로드 미완료(파일 업로드 실패/타임아웃) pano를 처리 불가로 정리하는 상태임이 확인됨.
pix-genie에서 해당 pano를 빼고 나머지 프로세싱 진행 수용 pano-postprocessor-manager.ts:29-32getCPPanosByCaptureId()에서 현재 state !== 'done'만 필터링. API 응답에 resource_state 필드가 이미 포함(cupix-fields.ts:153). resource_state === 'uploaded' 조건 추가로 업로드 미완료 pano를 다운로드 전에 제외 가능. Tesla default_scope(statable/pano.rb:8)가 abandoned pano를 API에서 제외하지만, postprocessor 실행 시점에 아직 created 상태인 pano가 포함될 수 있으므로 resource_state 기반 필터링이 더 신뢰성 있음.

변경 사항:

  • ## Root Cause Summary: abandoned 상태의 의미와 발생 경로 설명 추가. resource_state 기반 필터링으로 pix-genie에서 미업로드 pano를 제외하는 접근법 명시.
  • ### Code Path: "5단계: abandoned 상태 전이 경로" 추가 — statable/pano.rb, capture_invoker.rb, resumable.rb의 코드 경로. "6단계: pix-genie의 pano 필터링 부재" 추가 — getCPPanosByCaptureId()의 현재 필터링 로직과 resource_state 미활용 문제.
  • ## Fix Recommendation: 즉시 조치를 getCPPanosByCaptureId()에서 resource_state === 'uploaded' 필터링 추가로 변경. 구체적 코드 변경안 포함. 기존 다운로드 실패 추적 로직은 단기 개선으로 이동.

추가 조사 내용:

  • Tesla state machine 탐색: statable/pano.rbabandoned 이벤트 정의(line 83-85), not_processible 스코프(line 15), default_scope(line 8)
  • capture_invoker.rb:29-31 — capture 처리 시작 시 not_processible pano abandoned 마킹
  • resumable.rb:67-96 — capture resume 시 cleanup_materials에서 not_processible pano/video abandoned 마킹
  • storagable/resource.rb:78-82check_uploading 클래스 메서드: 15분 타임아웃 후 missing 전환
  • pano-postprocessor-manager.ts:29-32getCPPanosByCaptureId() 필터링 로직 확인
  • cupix-fields.ts:151-158 — API 요청 시 resource_state 필드 포함 확인
  • cppano.ts:23-40CPPano 모델이 resource_state를 저장하지 않음 확인
  • pano-postprocessor-service.ts:96-117 — pano 목록 fetch → download → mask 전체 흐름 확인