TransferManager::uploadFile | response path: /tmp/workspace/33745/videos/44851/original/44851_00041.
RCA: TransferManager::uploadFile | HTTP 500 Internal Server Error
Overview#
What Happened#
2026-04-20 08:07~08:42 UTC 사이 cupixworks-capture-postprocessor-agent 서비스에서 pano 이미지 업로드 중 upstream 스토리지 서비스가 HTTP 500을 반환하여 19건의 에러가 발생했다. EU-central-1 리전의 workspace 33745(video 44851), workspace 33696(video 44805) 두 건이 영향을 받았으며, 동일 시간대에 cupixworks-worker의 VoxelService 호출에서도 503 에러가 다수 발생하여 백엔드 인프라 전반의 일시적 장애로 판단된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | TransferManager::uploadFile |
| exception.message | response path: /tmp/workspace/33745/videos/44851/original/44851_00041.jpg, code: 500, message: Internal Server Error |
| top_frame | transfer.manager.ts:99 |
| env | production, eu-central-1 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| hassan-allam (EU) | 19 | Pano 이미지 업로드 실패로 캡처 후처리 중단. workspace 33745, 33696의 비디오 프레임 업로드가 완료되지 않음 |
Timeline#
- 07:20:00 UTC — 최초 ETIMEDOUT 신호 (workspace 33746/video 44852, 별도 건)
- 08:07:16 UTC — Workspace 33745/video 44851에서 첫 번째 HTTP 500 에러 발생
- 08:07~08:12 UTC — 14건의 연속 500 에러 (bulk JPG 업로드 실패)
- 08:25~08:29 UTC — ETIMEDOUT 에러 발생 (upstream 연결 자체 타임아웃)
- 08:28 UTC —
cupixworks-worker에서 VoxelService 503 에러 시작 - 08:41~08:43 UTC — Workspace 33696/video 44805로 500 에러 확산
- 08:43:15 UTC — 마지막 관측 에러
Error Log#
TransferManager::uploadFile | response path: /tmp/workspace/33745/videos/44851/original/44851_00041.jpg, code: 500, message: Internal Server Error
Impact#
- Service:
cupixworks-capture-postprocessor-agent - Team: hassan-allam
- 발생 횟수: 19
- 최초 발생: 2026-04-20T08:07:16.595Z
- 최근 발생: 2026-04-20T08:42:33.538Z
업로드 실패 시 failTask가 전체 transfer queue를 중단시키므로(_running = false, _stop = true), 하나의 파일 업로드 실패가 해당 캡처의 전체 pano 업로드 작업을 중단시킨다. Workspace 33745와 33696의 캡처 후처리가 완료되지 않았을 가능성이 높다.
Root Cause Summary#
Upstream 스토리지 서비스(presigned URL 기반 PUT 업로드 대상)가 일시적으로 HTTP 500 Internal Server Error를 반환하면서 pano 이미지 업로드가 실패했다. 동일 시간대에 cupixworks-worker의 VoxelService도 503을 반환하고 있어, 백엔드 인프라(스토리지/컴퓨트 계층)의 일시적 장애가 근본 원인이다. cupixworks-api 자체는 정상 동작하고 있었으므로, API 서버 문제가 아닌 스토리지 계층의 문제로 확인된다. Agent의 retry 로직(5회, 10초 간격)이 존재하지만, 인프라 장애가 ~35분간 지속되어 retry 예산을 소진한 것으로 보인다.
Technical Analysis#
Code Path#
- Entry point:
TransferManager.uploadNewPanos()—transfer.manager.ts:230에서UploadNewPanosContainer를 생성하고 각 task를 queue에 추가 - Task 초기화:
UploadNewPanosTask.init()—upload-new-panos.task.ts:23에서 Tesla API를 호출하여 pano를 생성하고upload_url(presigned URL)을 획득 - 업로드 실행:
TransferManager.transferTask()—transfer.manager.ts:193에서 task의 action이Upload이면uploadFile()호출 - Failure point:
TransferManager.uploadFile()—transfer.manager.ts:81-107에서 PUT 요청의 응답이 200이 아닌 경우 reject
uploadFile = (url: string, path: string, headers: any): Promise<void> => new Promise((resolve, reject) => {
const fileSize = CPUtils.getFileSize(path);
headers = { ...headers, 'Content-Length': fileSize };
logger.debug('TransferManager::uploadFile | begin path: %s, url: %s, size: %d', path, url, fileSize);
const fileStream = fs.createReadStream(path);
const sendReq = request.put(url, {
headers: headers,
body: fileStream
});
sendReq
.on('response', async res => {
if (res.statusCode === 200) {
logger.debug('TransferManager::uploadFile | complete path: %s', path);
await CPUtils.sleep(100);
resolve();
} else {
logger.error('TransferManager::uploadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
reject(this.cupixAuth.handleError(res)); // ← 에러 로깅 후 reject
}
})
.on('error', err => {
logger.error('TransferManager::uploadFile | path: %s, error: %s', path, JSON.stringify(err));
reject(this.cupixAuth.handleError(err));
});
});
- Retry 흐름:
TransferManager.retryTask()—transfer.manager.ts:173-191에서checkStatusCode()가 500을 retry 대상으로 판단(4xx만 제외)하고,isRetry()로MaxRetries(5) 미만인지 확인 후 10초 후 재시도
private checkStatusCode = (error: any): boolean => {
if (error?.statusCode != undefined && error.statusCode > 400 && error.statusCode < 500) return false;
return true; // 500은 retry 대상 (4xx만 제외)
};
retryTask = (task: BaseTask, error: any): void => {
if (this.checkStatusCode(error) && task.isRetry()) {
const idx = this._transferring.indexOf(task);
this._transferring.splice(idx, 1);
logger.debug('TransferManager::retryTask | add task after %d seconds - path: %s, url: %s, count: %d/%d',
Constants.RetryInterval/1000, task.target, task.url, task.retryCount, Constants.MaxRetries);
setTimeout(() => {
task.retry()
.then(() => {
this.addTask(task);
})
.catch(error => {
this.retryTask(task, error);
});
}, Constants.RetryInterval);
} else {
this.failTask(task, error); // retry 소진 시 전체 queue 중단
}
};
- 전체 중단:
TransferManager.failTask()—transfer.manager.ts:158-166에서_running = false,_stop = true로 설정하여 전체 transfer queue를 즉시 중단
private failTask = (task: BaseTask, error: any): void => {
this._running = false; // 전체 queue 중단
this._stop = true; // 새 task 처리 방지
const idx = this._transferring.indexOf(task);
this._transferring.splice(idx, 1);
this._failed.push(task);
task.finalize(TaskType.Failed, error);
logger.error('TransferManager::failTask | path: %s, url: %s, count: %d/%d', task.target, task.url, task.retryCount, Constants.MaxRetries);
};
기대 동작: presigned URL로 PUT 요청 시 스토리지 서비스가 200을 반환하고 파일이 저장됨.
실제 동작: 스토리지 서비스가 500을 반환. 5회 retry 후에도 실패하여 failTask가 호출되고, 해당 캡처의 모든 남은 pano 업로드가 중단됨.
Log Evidence#
Datadog에서 사용한 주요 쿼리:
service:cupixworks-capture-postprocessor-agent status:error @environment:production "TransferManager"
service:cupixworks-capture-postprocessor-agent status:warn @environment:production "500"
service:cupixworks-worker status:error @environment:production "503"
HTTP 500 에러 로그 (대표 샘플):
2026-04-20T08:07:16.595Z TransferManager::uploadFile | response path: /tmp/workspace/33745/videos/44851/original/44851_00041.jpg, code: 500, message: Internal Server Error
2026-04-20T08:08:23.000Z TransferManager::uploadFile | response path: /tmp/workspace/33745/videos/44851/original/44851_00723.jpg, code: 500, message: Internal Server Error
2026-04-20T08:09:45.000Z TransferManager::uploadFile | response path: /tmp/workspace/33745/videos/44851/original/44851_00870.jpg, code: 500, message: Internal Server Error
CupixAuth::handleError 경고 로그 (500 에러와 쌍으로 발생):
{
"message": "CupixAuth::handleError | Undefined response: {\"statusCode\":500,\"request\":{\"method\":\"PUT\"}}"
}
ETIMEDOUT 에러 (연결 타임아웃):
{
"message": "CupixAuth::handleError | Undefined response: {\"stack\":\"Error: read ETIMEDOUT\\n at TLSWrap.onStreamRead...\",\"errno\":-110,\"code\":\"ETIMEDOUT\",\"syscall\":\"read\"}"
}
동시간대 VoxelService 503 에러 (cupixworks-worker):
2026-04-20T08:28:XX.XXXZ Cupix::VoxelService::captured_area! — 503 Service Unavailable
총 50건의 VoxelService 503 에러가 08:28 UTC 이후 발생하여, 단일 서비스가 아닌 백엔드 인프라 전반의 일시적 장애임을 확인.
cupixworks-api 상태: 동일 시간대 service:cupixworks-api status:error 쿼리 결과 0건. API 서버 자체는 정상이었으며, PUT /api/v1/captures/{id}/meta/video 요청도 모두 200으로 응답. 업로드 대상 서비스는 API 서버가 아닌 별도의 스토리지 서비스(presigned URL 기반).
영향받은 파일 목록 (14건, workspace 33745/video 44851): 파일 번호: 00041, 00723, 00787, 00870, 01115, 01212, 01285, 01364, 01546, 01762, 02011, 02036, 02048, 02239 — 순차적이지 않아 일부 파일만 간헐적으로 실패한 것으로 확인.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Upstream 스토리지 서비스의 일시적 인프라 장애 | 동시간대 VoxelService 503 에러 50건 발생 (cupixworks-worker), cupixworks-api는 정상(에러 0건), 500/ETIMEDOUT 혼재 패턴, 35분간 지속 후 자연 해소 | — | Confirmed |
| H2 | Presigned URL 만료로 인한 업로드 실패 | presigned URL은 시간 제한이 있음 | HTTP 500은 인증 실패가 아닌 서버 내부 오류를 의미. presigned URL 만료 시 403이 반환되어야 하나, 실제로는 500 반환 | Rejected |
| H3 | Agent 측 파일 손상 또는 Content-Length 불일치 | 대용량 파일 스트리밍 시 size 불일치 가능성 | 14개 파일이 동시에 같은 에러를 반환하므로 개별 파일 문제가 아닌 서버 측 문제. ETIMEDOUT 에러도 혼재하여 네트워크/서버 계층 문제 확인 | Rejected |
| H4 | Rate limiting에 의한 업로드 거부 | _maxParallelTransfer = 5로 동시 업로드. 스토리지 서비스가 rate limit 초과 시 500 반환 가능 |
일반적으로 rate limit은 429를 반환. 또한 VoxelService도 동시에 503을 반환하고 있어 단일 서비스의 rate limit이 아닌 인프라 전반 문제 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 이 에러는 upstream 인프라의 일시적 장애로 인한 것이므로, agent 코드의 즉각적인 수정은 불필요하다.
- 인프라 팀에 2026-04-20 08:07~08:43 UTC 시간대의 스토리지 서비스 및 VoxelService 장애 원인을 확인 요청해야 한다.
단기 개선 (1주 이내)#
failTask의 전체 중단 로직 완화 (transfer.manager.ts:158-166): 현재 하나의 task 실패 시 전체 transfer queue가 중단된다. 개별 task 실패를 격리하고 나머지 task는 계속 진행하도록 변경하면, 간헐적 실패 시 성공 가능한 파일들은 업로드를 완료할 수 있다.- Exponential backoff 도입 (
transfer.manager.ts:173-191): 현재 고정 10초 간격 retry인데, 인프라 장애 시에는 exponential backoff (10s → 20s → 40s → 80s → 160s)로 변경하면 총 retry 시간이 ~5분으로 늘어나 일시적 장애 복구를 기다릴 수 있다.
장기 개선 (재발 방지)#
- Dead letter queue / 재처리 메커니즘: 업로드 실패한 캡처를 별도 큐에 넣어 인프라 복구 후 자동 재처리하는 구조를 도입.
- 스토리지 서비스 health check: 업로드 시작 전 스토리지 서비스의 health를 확인하여, 장애 중에는 불필요한 시도를 줄이고 큐 메시지를 visibility timeout으로 보존.
- 에러 레벨 세분화: upstream 일시적 장애(500, ETIMEDOUT)는
warn레벨로 로깅하고, retry 소진 후 최종 실패만error로 로깅하면 알림 노이즈를 줄일 수 있다.
Monitoring#
- 업로드 실패율 모니터링:
service:cupixworks-capture-postprocessor-agent status:error "TransferManager::uploadFile" "code: 500"
- Retry 소진 모니터링:
service:cupixworks-capture-postprocessor-agent status:error "TransferManager::failTask"
- 상관 서비스 장애 동시 모니터링:
(service:cupixworks-capture-postprocessor-agent OR service:cupixworks-worker) status:error ("500" OR "503")
Risk Assessment#
- Risk level: medium — 일시적 인프라 장애이나, 재발 시 캡처 후처리 완전 중단으로 이어짐
- 예상 복잡도: standard — retry 로직 개선 및 failTask 격리는 기존 코드 구조 내에서 수정 가능