ES /docs

SingleshotService::uploadStitchedPano | image is not uploaded

RCA: SingleshotService::uploadStitchedPano | image is not uploaded

Overview#

What Happened#

2026-06-13 01:02:33 KST 에 cupixworks-capture-singleshot-agent (us-west-2, production) 가 capture id 713544 의 stitched pano (panoId 88300200) 를 S3 presigned URL 로 PUT 업로드하는 단계에서 실패했다. runAlign 은 정상 종료(runAlign | end | success) 되었으나 후속 uploadImage 호출이 200 이 아닌 응답으로 돌아와 image is not uploaded 에러가 발생했고, 그대로 함수가 early-return 되어 stitched 결과가 서버에 반영되지 않았다. 단일 capture(southlandind 테넌트, cupix 환경) 에서 1회 발생한 isolated incident 다.

Quick Facts#

Field Value
exception.class (none — string error log)
exception.message SingleshotService::uploadStitchedPano | image is not uploaded
top_frame packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:125
runtime Node.js (TypeScript agent)
env production, us-west-2
capture id 713544
pano id 88300200
tenant cupix (team: southlandind)

Affected Teams#

Team / Domain Error Count Impact
southlandind (cupix tenant) 1 capture 713544 의 stitched pano 가 서버에 업로드되지 않아 해당 캡처의 후속 처리(원본 panorama format 업데이트 및 stitched 결과물 노출) 가 누락됨

Timeline#

  1. 2026-06-13 01:01:36 KSTBaseService::runByMessage | id: 713544 로 capture 처리 시작
  2. 2026-06-13 01:01:37 KSTrunAlign | begin | captureId: 713544 , pano 88300200 다운로드
  3. 2026-06-13 01:02:06 KSTrunAlign | stitched pano copied | panoId: 88300200, path: /tmp/workspace/713544/input_panos/88300200_stitched.jpg
  4. 2026-06-13 01:02:32 KSTrunAlign | end | success
  5. 2026-06-13 01:02:33 KSTSingleshotService::uploadStitchedPano | image is not uploaded (failure)
  6. 2026-06-13 01:02:34 KSTBaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/713544 (workspace 정리, SQS 메시지 삭제)

Error Log#

Datadog Logs

text
SingleshotService::uploadStitchedPano | image is not uploaded

Impact#

  • Service: cupixworks-capture-singleshot-agent
  • Team: southlandind
  • 발생 횟수: 1
  • 최초 발생: 2026-06-13 01:02:33 KST
  • 최근 발생: 2026-06-13 01:02:33 KST
  • 영향: capture 713544 의 stitched panorama 가 서버 측 S3 에 PUT 되지 않아, 후속 pano.checkUploading / pano.updateStitched 가 호출되지 않거나 일관성 없는 상태로 진행됨. 사용자 관점에서는 해당 캡처의 stitched pano 가 누락된 상태가 된다.

Root Cause Summary#

SingleshotService.uploadStitchedPano 가 호출하는 PanoApiModule.uploadImage 가 presigned URL 로 PUT 한 응답에서 response.statusCode !== 200 인 경우 단순히 false 를 반환한다. 그러나 uploadImage 구현은 (1) 어떤 상태 코드가 반환됐는지, (2) 어떤 에러(error 인자)가 발생했는지, (3) 응답 body 가 무엇인지 전혀 로깅하지 않으며, 또한 네트워크 오류로 responseundefined 일 때 response.statusCode 접근이 던질 수 있어 정확한 실패 원인 파악이 불가능하다. 결과적으로 호출자(uploadStitchedPano) 는 image is not uploaded 한 줄만 남기고 early-return 하므로, 본 incident 에서는 "PUT 이 200 이 아니었다" 외에는 직접 증거가 남아있지 않다. 1회성 발생 + 직전 단계 모두 정상이라는 점에서, presigned URL 의 일시적 실패(예: presigned URL 만료, S3 네트워크 일시 장애, 5xx) 가 가장 유력한 직접 원인이며, 근본적인 코드 결함은 uploadImage 의 에러 가시성 부재(silent failure) 다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:37 (run)
  • Stitched 업로드 진입: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:96 (uploadStitchedPano)
  • Failure point: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:122-126
  • 업로드 실제 구현: packages/api/src/api/pano.api.ts:154-177 (PanoApiModule.uploadImage)

호출자 측 — runAlign 이후 stitched 결과를 서버 presigned URL 로 PUT 한다. 결과가 false 면 한 줄 로그 후 그대로 return:

packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:113-130typescript
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;
    }

    const isUploaded = await this.cupixApi.pano.uploadImage(stitchedUploadUrl, stitchedImagePath);

    if (!isUploaded) {
        logger.error('SingleshotService::uploadStitchedPano | image is not uploaded');
        return;
    }

    await this.cupixApi.pano.checkUploading(panoId);
}

업로드 구현 — 200 만 성공으로 보고, 그 외에는 에러/응답 정보를 모두 버린 채 false 반환:

packages/api/src/api/pano.api.ts:154-177typescript
uploadImage = (url: string, filePath: string): Promise<boolean> => {
    const fileStream = fs.createReadStream(filePath);
    const fileStats = fs.statSync(filePath);

    const options = {
        method: 'PUT',
        url: url,
        body: fileStream,
        headers: {
            'Content-Type': 'binary/octet-stream',
            'Content-Length': fileStats.size
        }
    };

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (response.statusCode == 200) {
                resolve(true);
            }

            resolve(false);
        });
    });
};

기대 동작 vs 실제 동작:

  • 기대: PUT 실패 시 (a) statusCode/응답 body/에러를 로그로 남기고, (b) error 가 truthy 거나 response 가 undefined 인 케이스를 안전하게 처리해야 함.
  • 실제: 단 한 줄 image is not uploaded 만 남고 statusCode/error/응답 본문 정보는 전부 소실됨. 네트워크 오류로 response 가 undefined 인 경우 response.statusCode 접근에서 TypeError 가 발생할 가능성도 있음 (이때는 promise 가 unresolved/unrejected 로 끝나거나 unhandled exception).
  • 또한 PUT 이 실패해도 uploadStitchedPano 는 silent return 하므로 호출자(run) 는 정상 흐름으로 간주하고 singleshot_state = Stopped 까지 마무리한다 (singleshot-service.ts:42-43). 즉 capture 는 사실상 절반만 처리된 상태로 종료됨.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-capture-singleshot-agent @environment:production "uploadStitchedPano"
text
service:cupixworks-capture-singleshot-agent @environment:production

(시간창 2026-06-12T15:50:00Z ~ 2026-06-12T16:10:00Z)

text
"image is not uploaded"

(시간창 now-14d ~ now → 1건)

핵심 로그 (capture 713544 라이프사이클, 시간 역순 → 정순 정렬):

text
2026-06-13 01:01:36 KST  info  BaseService::runByMessage | id: 713544
2026-06-13 01:01:36 KST  info  CupixAuth::setSession | session_id: a0e52065a19df4c7719cc124bdcc7c5d41e8ea72
2026-06-13 01:01:36 KST  info  SingleshotService::getCaptureById | captureId: 713544
2026-06-13 01:01:37 KST  info  SingleshotService::runAlignScript | params: {"workspace":"/tmp/workspace/713544","captureId":713544,"awsRegion":"us-west-2","envName":"production"}
2026-06-13 01:01:37 KST  info  runAlign | begin | captureId: 713544, workspace: /tmp/workspace/713544
2026-06-13 01:01:37 KST  info  runAlign | capture loaded | id: 713544, creation_platform: app, method: singleshot
2026-06-13 01:01:37 KST  info  runAlign | panos loaded | count: 1
2026-06-13 01:01:37 KST  info  runAlign | pano downloaded | id: 88300200, key: 88300200, path: /tmp/workspace/713544/original_panos/88300200.insp
2026-06-13 01:01:37 KST  info  runAlign | processing options downloaded
2026-06-13 01:02:06 KST  info  runAlign | stitched pano copied | panoId: 88300200, path: /tmp/workspace/713544/input_panos/88300200_stitched.jpg
2026-06-13 01:02:06 KST  info  runAlign | preprocess complete | results: 1
2026-06-13 01:02:06 KST  warn  CupixAuth::handleError | Response statusCode: 404, requestUriHref: http://api-tesla.cupix.internal/api/v1/sessions/sagemaker_invoke_credentials...
2026-06-13 01:02:06 KST  warn  runAlign | SageMaker configuration failed, continuing without DNN: HTTP request failed
2026-06-13 01:02:32 KST  info  runAlign | SKAT complete | duration: 25.036s
2026-06-13 01:02:32 KST  error ChildProcessManager::setupEventHandlers | Child process exited
2026-06-13 01:02:32 KST  error ChildProcessManager::setupEventHandlers | Child process closed
2026-06-13 01:02:32 KST  info  runAlign | no clusters, synthesizing from unaligned_panos (singleshot)
2026-06-13 01:02:32 KST  info  applyAlignmentResults | cluster created | id: 1381140, name: default
2026-06-13 01:02:32 KST  info  applyAlignmentResults | geo coordinates flushed | recordId: 132643
2026-06-13 01:02:32 KST  info  runAlign | end | success
2026-06-13 01:02:33 KST  error SingleshotService::uploadStitchedPano | image is not uploaded
2026-06-13 01:02:34 KST  info  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/713544
2026-06-13 01:02:34 KST  info  AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/.../cupix-tesla-singleshot-agent-production
2026-06-13 01:02:34 KST  info  AwsQueueManager::deleteMessage | end - message id: d5959502-d807-4b0e-b319-f491e8017050

추가 관찰:

  • 같은 시간창 동일 service 의 다른 capture (713566, 713543, 713564) 들은 정상적으로 runAlign | end | success 후 cleanUp/deleteMessage 시퀀스로 끝났고, "image is not uploaded" 는 713544 한 건만 발생.
  • runAlign | end | successimage is not uploaded 사이 간격은 약 1초로, stitched 결과 자체는 디스크에 정상 존재했다고 추정.
  • 14일 윈도우 전체에서 동일 메시지는 1건뿐 → 시스템적 패턴이 아닌 isolated event.
  • 직접 증거(HTTP statusCode, S3 응답 body, network error)는 로그에 남지 않음 → 어떤 종류의 PUT 실패였는지 단정 불가. uncertain — needs verification (요청 자체의 실패 원인은 추가 가시성 없이는 확인 불가).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 uploadImage 가 presigned URL 로의 PUT 응답에서 200 이 아닌 코드를 받아 false 를 resolve 했고, 호출자가 silent return 함 (코드 결함: 에러 가시성 부재) pano.api.ts:154-177 의 구현은 statusCode 200 외 모든 케이스를 무로깅 false 처리. 호출자 singleshot-service.ts:124-126 도 한 줄만 남기고 return. Datadog 에 statusCode/응답 body/네트워크 에러 흔적 없음. Confirmed (코드 경로 및 silent failure 메커니즘)
H2 stitched 이미지 파일이 존재하지 않아 업로드 자체를 시도하지 않음 `runAlign stitched pano copied
H3 createUploadUrl 이 빈 upload_url 을 반환해 업로드 URL 자체가 없었음 코드(singleshot-service.ts:117-120) 에 따르면 그 경우 stitched upload url is not provided 로 다른 에러가 로깅됨. 실제 발생 메시지는 image is not uploaded 이므로 다른 분기. Rejected
H4 SageMaker sagemaker_invoke_credentials 404 가 stitched 업로드 실패와 인과 관계 직전 단계에서 동일 session 으로 `CupixAuth::handleError Response statusCode: 404 ... sagemaker_invoke_credentials` warn 발생 해당 404 는 SageMaker DNN credential 조회 실패로, 코드는 `runAlign
H5 일시적 S3/네트워크 장애 또는 presigned URL 만료로 PUT 이 5xx/4xx 또는 connection error 로 실패 14일 윈도우 1회만 발생 (isolated), 직전 단계 모두 정상, 동시간대 다른 capture 정상 처리 직접 증거(statusCode/error 로그) 없음 — silent failure 때문에 확인 불가 Inconclusive (가장 그럴듯한 직접 원인이지만 확정할 증거가 부재. uncertain — needs verification)

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: packages/api/src/api/pano.api.ts:154-177 (PanoApiModule.uploadImage)
  • 방향: silent failure 제거. request 콜백에서
    • error 가 truthy 면 그 내용을 logger.error 로 남기고 false resolve
    • response 가 undefined 인 경우를 안전하게 처리 (현재 코드는 response.statusCode 에서 TypeError 가능)
    • 200 이 아닌 경우 response.statusCode, response 헤더의 x-amz-request-id (있다면), body 일부를 로그로 남길 것
    • 200 분기에서 즉시 return 하지 않으면 resolve(true) 후에도 resolve(false) 가 호출되어 무해하지만, 가독성을 위해 early return 처리
  • 파일: packages/cupix-tesla-singleshot-agent/src/singleshot-service.ts:124-126
  • 방향: image is not uploaded 로그에 capture id, pano id, stitched path, file size 등 컨텍스트를 함께 남기고, 단순 return 대신 호출자(run) 에 실패가 전달되어 updateErrorState 흐름을 타도록 검토 (현재는 정상 종료처럼 마무리되어 capture state 가 Stopped 가 된다 — singleshot-service.ts:42-43).

단기 개선 (1주 이내)#

  • presigned URL PUT 에 대한 retry 정책 도입 (예: 5xx/네트워크 오류 한정 1~2회 재시도, exponential backoff). 본 incident 가 일시적 장애였다면 retry 만으로 자연 해소 가능.
  • pano.api.tsrequest (legacy request 패키지) 사용을 점검. request 는 deprecated 이며 stream 업로드 시 에러 처리가 제한적이다. 점진적으로 axios/got/undici 등으로 교체를 검토.
  • 업로드 실패 시 capture singleshot_state 를 Error 또는 별도 상태로 표기해 후속 재처리를 가능하게 하는 도메인 로직 검토 (현재는 Stopped 로 마무리되어 실패가 운영 메트릭에 드러나지 않음).

장기 개선 (재발 방지)#

  • 외부 의존(외부 API/S3 PUT) 에 대한 표준 retry/timeout/log 정책을 agents BaseService 또는 공용 utility 로 일원화.
  • agents 전반 (packages/api/src/api/*.ts) 의 silent failure 패턴 (resolve(false) after non-200) 을 일괄 점검.
  • stitched pano 업로드 누락을 detect 하는 데이터 정합성 모니터링 (예: capture 처리 종료 후 N분 내 stitched 결과 부재 알림) 추가.

Monitoring#

  • 추가할 알림: 동일 메시지가 1시간 내 재발생 또는 24시간 내 N건 이상 발생 시 alert.
  • presigned URL PUT 실패 메트릭화: 위 즉시 조치를 통해 statusCode/네트워크 에러를 로그화하면 별도 메트릭으로 추출 가능.

Datadog timeseries widget 용 쿼리 (release dashboard 호환 형식):

text
logs("service:cupixworks-capture-singleshot-agent @environment:production \"image is not uploaded\"").index("*").rollup("count").by("environment")
text
logs("service:cupixworks-capture-singleshot-agent @environment:production status:error \"SingleshotService::uploadStitchedPano\"").index("*").rollup("count").by("environment")
text
logs("service:cupixworks-capture-singleshot-agent @environment:production \"runAlign | end | success\"").index("*").rollup("count").by("environment")

(상위 두 쿼리는 실패 카운트, 마지막 쿼리는 정상 종료 카운트로 비교 baseline 으로 사용)

Risk Assessment#

  • Risk level: low (현재 14일간 1회, 단일 capture, 시스템적 장애 아님)
  • 예상 복잡도: trivial — standard
    • 즉시 조치(로깅 보강)은 trivial 한 코드 변경.
    • 단기 retry/상태 처리 변경은 standard 수준.
  • 데이터 영향: capture 713544 의 stitched pano 누락 1건 (수동 재처리 또는 재캡처 필요 여부는 운영팀 확인 필요).