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#
- 2026-06-13 01:01:36 KST —
BaseService::runByMessage | id: 713544로 capture 처리 시작 - 2026-06-13 01:01:37 KST —
runAlign | begin | captureId: 713544, pano88300200다운로드 - 2026-06-13 01:02:06 KST —
runAlign | stitched pano copied | panoId: 88300200, path: /tmp/workspace/713544/input_panos/88300200_stitched.jpg - 2026-06-13 01:02:32 KST —
runAlign | end | success - 2026-06-13 01:02:33 KST —
SingleshotService::uploadStitchedPano | image is not uploaded(failure) - 2026-06-13 01:02:34 KST —
BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/713544(workspace 정리, SQS 메시지 삭제)
Error Log#
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 가 무엇인지 전혀 로깅하지 않으며, 또한 네트워크 오류로 response 가 undefined 일 때 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:
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 반환:
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 쿼리:
service:cupixworks-capture-singleshot-agent @environment:production "uploadStitchedPano"
service:cupixworks-capture-singleshot-agent @environment:production
(시간창 2026-06-12T15:50:00Z ~ 2026-06-12T16:10:00Z)
"image is not uploaded"
(시간창 now-14d ~ now → 1건)
핵심 로그 (capture 713544 라이프사이클, 시간 역순 → 정순 정렬):
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 | success와image 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 resolveresponse가 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.ts의request(legacyrequest패키지) 사용을 점검.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 호환 형식):
logs("service:cupixworks-capture-singleshot-agent @environment:production \"image is not uploaded\"").index("*").rollup("count").by("environment")
logs("service:cupixworks-capture-singleshot-agent @environment:production status:error \"SingleshotService::uploadStitchedPano\"").index("*").rollup("count").by("environment")
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건 (수동 재처리 또는 재캡처 필요 여부는 운영팀 확인 필요).