ES /docs

SingleshotService::uploadStitchedPano | stitched upload url is not provided

RCA: SingleshotService::uploadStitchedPano | stitched upload url is not provided

Error Log#

Datadog Logs

text
SingleshotService::uploadStitchedPano | stitched upload url is not provided

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • 발생 횟수: 2
  • 최초 발생: 2026-04-13T07:34:12.642Z
  • 최근 발생: 2026-04-13T07:56:10.691Z
  • 영향 범위: 동일 사용자(sean.lee@cupix.com, team: demokr)의 Insta360 ONE X2 카메라로 촬영한 singleshot capture 2건 (679736, 679740). 에러 발생에도 불구하고 후속 postprocessor 파이프라인은 정상 완료됨. stitched pano 재업로드가 실패했으나 모바일 앱이 이미 업로드한 stitched 이미지가 존재하므로 실질적 데이터 손실 없음.

Root Cause Summary#

모바일 앱(Insta360 ONE X2)이 pano 생성 시 원본 이미지와 stitched 이미지를 모두 API upload_url 엔드포인트를 통해 업로드한 후, singleshot agent가 동일 pano에 대해 다시 createUploadUrl을 호출하면, API 서버의 resource_uploadable? 체크에서 S3에 이미 revision 2 객체가 존재함을 확인하고 false를 반환한다. 이로 인해 resource_stateuploading으로 전환되지 않고, serializer가 upload_url 필드를 nil로 반환하여 agent 측에서 "stitched upload url is not provided" 에러가 발생한다.

Technical Analysis#

Code Path#

  • Entry point: singleshot-service.ts:37SingleshotService.run()
  • SKAT alignment 실행: singleshot-service.ts:41this.runAlignScript(serverCapture)
  • Stitched pano 업로드 시도: singleshot-service.ts:42this.uploadStitchedPano(targetId)
  • Failure point: singleshot-service.ts:115-119createUploadUrl 호출 후 upload_urlnil

Agent 측 코드에서 createUploadUrl을 호출하고 응답에서 upload_url을 추출하는 부분:

typescript
// cupix-tesla-singleshot-agent/src/singleshot-service.ts:111-119
const stitchedImagePath = path.join(Environment.CPX_WORKSPACE_PATH, captureId.toString(), 'input_panos', `${panoId}_stitched.jpg`);

if (fs.existsSync(stitchedImagePath)) {
    logger.debug('SingleshotService::uploadStitchedPano | original image has been stitched');
    const stitchedUploadUrl = await this.cupixApi.pano.createUploadUrl(panoId, {}).then(res => res.upload_url);

    if (!stitchedUploadUrl) {
        logger.error('SingleshotService::uploadStitchedPano | stitched upload url is not provided');
        return;  // <-- 여기서 조기 반환
    }

API 서버의 upload_url 엔드포인트에서 resource_uploadable? 체크:

ruby
# tesla/app/repositories/pano_repository.rb:434-442
def upload_url(params)
    @model.uploading_resource_state if @model.resource_uploadable?
    # resource_uploadable?가 false이면 state 전환 안 됨
    @model
end

resource_uploadable? 메서드에서 S3 객체 존재 여부 체크:

ruby
# tesla/app/models/concerns/statable/pano.rb:222-228
def resource_uploadable?
    return true if revision.zero?
    return true if resource_state_uploaded? && !resource.object(revision + 1).exists? && revision == 1
    # ↑ revision 2 S3 객체가 이미 존재하면 false 반환
    return true if tile_state_uploaded?
    false
end

Serializer에서 resource_state_name[:created, :uploading, :missing]에 포함되지 않으면 nil 반환:

ruby
# tesla/app/serializers/pano_serializer.rb:110-116
attribute :upload_url do |pano, params|
    if %i[created uploading missing].include?(pano.resource_state_name)
        pano.resource_upload_url
    else
        nil  # <-- resource_state가 uploaded 상태이면 nil 반환
    end
end

기대 동작: createUploadUrl 호출 시 resource_stateuploading으로 전환하고 presigned upload URL을 반환해야 함.

실제 동작: 모바일 앱이 이미 revision 2를 업로드했으므로 resource_uploadable?false → state 전환 없음 → serializer가 nil 반환 → agent 에러.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api 80940841
Time: 2026-04-13T07:20:00Z to 2026-04-13T07:34:13Z

Pano 80940841 (capture 679736) — 모바일 앱의 사전 업로드 흔적:

모바일 앱이 agent 실행 전에 이미 upload_url을 2회 호출하고 check_uploading으로 업로드를 확인한 로그:

text
16:23:57 KST [200] GET /api/v1/panos/80940841 (PanosController#show)
16:23:58 KST [200] PUT /api/v1/panos/80940841/check_uploading (PanosController#check_uploading)
16:23:59 KST [200] POST /api/v1/panos/80940841/upload_url (PanosController#upload_url)  ← 1차 upload
16:23:59 KST [200] GET /api/v1/panos/80940841 (PanosController#show)
16:24:00 KST [200] POST /api/v1/panos/80940841/upload_url (PanosController#upload_url)  ← 2차 upload (stitched)
16:24:01 KST [200] PUT /api/v1/panos/80940841/check_uploading (PanosController#check_uploading)

첫 번째 upload_url + check_uploading 호출은 원본 이미지 업로드(revision 1), 두 번째 upload_url + check_uploading은 stitched 이미지 업로드(revision 2)로 추정. 이후 S3에 revision 2 객체가 존재하게 됨.

Agent 실행 및 에러 발생 타임라인 (capture 679736):

text
07:33:09 UTC  BaseService::runByMessage | id: 679736
07:33:10 UTC  SingleshotService::getCaptureById | captureId: 679736
07:33:10 UTC  SingleshotService::runAlignScript | params: {...captureId:679736...}
07:34:09 UTC  [Scene-Mapper] warning - vp[80940841_stitched] has no visible viewpoint
07:34:12 UTC  SingleshotService::uploadStitchedPano | stitched upload url is not provided  ← ERROR
07:34:13 UTC  [API] [200] POST /api/v1/panos/80940841/upload_url  ← API는 200 반환했으나 upload_url 필드 nil
07:34:16 UTC  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/679736

Pano 80940842 (capture 679740) — 동일 패턴:

text
service:cupixworks-api 80940842
Time: 2026-04-13T07:30:00Z to 2026-04-13T08:00:00Z
text
16:51:18 KST [200] POST /api/v1/panos/80940842/upload_url  ← 1차 (앱 또는 첫 agent 실행)
16:51:19 KST [200] PUT /api/v1/panos/80940842/check_uploading
16:51:20 KST [200] POST /api/v1/panos/80940842/upload_url  ← 2차 (stitched 업로드)
16:51:21 KST [200] GET /api/v1/panos/80940842
16:51:22 KST [200] PUT /api/v1/panos/80940842/check_uploading
... (agent 두 번째 실행)
16:56:12 KST [200] POST /api/v1/panos/80940842/upload_url  ← agent가 호출, 200이지만 upload_url nil

DB 상태 (Kibana):

text
Pano 80940841: resource_state=uploaded, tile_state=uploaded, state=done
Pano 80940842: resource_state=uploaded, tile_state=uploaded, state=done
카메라: Insta360 ONE X2, 파일: .insp 형식

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: cupix-tesla-singleshot-agent/src/singleshot-service.ts:115
  • 방향: createUploadUrlupload_url을 반환하지 않을 경우를 정상 흐름으로 처리해야 함. 모바일 앱이 이미 stitched 이미지를 업로드한 상황이므로, agent가 재업로드를 시도할 필요가 없음. logger.errorlogger.info 또는 logger.warn으로 변경하여 정상적인 skip 동작으로 처리.
  • 현재 코드는 에러 후 return으로 함수를 빠져나가는데, 이때 updateStitched API 호출(line 132-134)도 건너뛰게 됨. upload_url이 없어도 updateStitched는 호출되어야 하는지 확인 필요.

단기 개선 (1주 이내)#

  • uploadStitchedPano 메서드의 로직을 개선: stitched 이미지가 이미 업로드된 경우(upload_url이 nil)를 명시적으로 처리하는 분기 추가. 이미 revision 2가 존재하면 업로드를 skip하고 바로 updateStitched로 진행하도록 변경.
  • 또는 API 서버 측에서 resource_uploadable? 조건을 완화하여, 이미 uploaded 상태여도 agent의 재업로드 요청에 대해 presigned URL을 발급하는 방안 검토 (force 파라미터 활용).

장기 개선 (재발 방지)#

  • singleshot capture의 stitching 책임 분리를 명확히 해야 함: 모바일 앱이 stitched 이미지를 업로드하는 경우와 agent가 서버사이드 stitching을 수행하는 경우의 흐름을 분리. agent 시작 전에 pano의 현재 revision과 stitched 상태를 확인하여 불필요한 stitched 업로드를 건너뛰는 pre-check 로직 도입.

Monitoring#

  • stitched upload skip 이벤트를 info 레벨로 로깅하여 빈도 추적
  • 추천 Datadog 쿼리:
text
service:cupixworks-capture-singleshot-agent "uploadStitchedPano" "stitched upload url is not provided"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 에러가 발생해도 후속 postprocessor 파이프라인은 정상 완료되며, 모바일 앱이 이미 stitched 이미지를 업로드했으므로 실질적 데이터 손실 없음. 다만 불필요한 에러 로그가 모니터링 노이즈를 발생시킴.