TransferManager::downloadFile | response path: /tmp/workspace/680672/videos/VID_20260414_134128_00_4
RCA: TransferManager::downloadFile 504 Gateway Time-out
Overview#
What Happened#
2026-04-23 08:06~08:27 UTC 사이에 cupixworks-capture-3dreconstruction-instance 서비스에서 비디오 파일 다운로드 중 504 Gateway Time-out 에러가 3건 발생했다. 2개의 workspace(680672, 684875)에서 .insv 비디오 파일을 Tesla API(/videos/:id/download)를 통해 다운로드하는 과정에서, nginx 리버스 프록시가 upstream 응답 시간 초과로 504를 반환했다. 같은 시간대에 upload 쪽에서도 동일한 504 nginx 에러가 20건 이상 관찰되어, Tesla API 전체적으로 일시적인 응답 지연이 발생한 것으로 판단된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.message | TransferManager::downloadFile | response path: ..., code: 504, message: Gateway Time-out |
| top_frame | transfer.manager.ts:72 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| nhc-sa (3D reconstruction) | 3 (downloadFile) + 20+ (uploadNewPointclouds) | 비디오 다운로드 및 pointcloud 업로드 실패, 3D 재구성 파이프라인 중단 |
Timeline#
- 2026-04-23 08:06:11 UTC — workspace 680672에서 첫 downloadFile 504 에러 발생 (
VID_20260414_134128_00_454.insv) - 2026-04-23 08:10:22 UTC — failTask 로그 발생 (별도 workspace 685301, ref_planes upload 실패)
- 2026-04-23 08:15:34~08:43:55 UTC — uploadNewPointclouds 504 에러 다수 발생 (workspace nhc-sa.679783)
- 2026-04-23 08:27:19 UTC — workspace 684875에서 downloadFile 504 에러 2건 동시 발생 (마지막 발생)
- 2026-04-23 11:01 UTC — RCA 착수
Error Log#
TransferManager::downloadFile | response path: /tmp/workspace/680672/videos/VID_20260414_134128_00_454.insv, code: 504, message: Gateway Time-out
Impact#
- Service:
cupixworks-capture-3dreconstruction-instance - Team: nhc-sa
- 발생 횟수: 3 (downloadFile 에러만 해당; 동시간대 504 에러 전체는 20건 이상)
- 최초 발생: 2026-04-23T08:06:11.750Z
- 최근 발생: 2026-04-23T08:27:19.605Z
Root Cause Summary#
3D reconstruction agent가 Tesla API의 /videos/:id/download endpoint를 통해 .insv 비디오 파일을 다운로드할 때, Tesla API 앞단의 nginx 리버스 프록시가 upstream 응답 시간을 초과하여 504 Gateway Time-out을 반환했다. 동시간대에 upload (pointcloud) 요청에서도 동일한 504 nginx 에러가 다수 관찰되어, 이는 특정 API endpoint 문제가 아닌 Tesla API 전체의 일시적인 응답 지연 또는 과부하 상황이었다. cupixworks-api 서비스 자체에서는 504/timeout 관련 에러 로그가 없으므로, Rails 프로세스까지 요청이 도달하기 전 또는 처리 시간이 nginx의 proxy_read_timeout을 초과하여 nginx 레벨에서 끊어진 것으로 판단된다.
Technical Analysis#
Code Path#
- Entry point:
TransferManager.downloadVideos()attransfer.manager.ts:251 DownloadVideosContainer가 각 비디오 파일에 대해DownloadVideosTask를 생성TransferManager.transferTask()가 각 task를 실행 (transfer.manager.ts:195)TransferManager.downloadFile()이request.get()으로 Tesla API 호출 (transfer.manager.ts:50)- Failure point:
transfer.manager.ts:72— 응답 statusCode가 200이 아닌 504를 받아 에러 로그 출력 후 reject
비디오 다운로드 URL은 CPVideo.initialize()에서 구성된다:
private initialize = (): void => {
this._downloadUrl = Environment.CPX_API_ENDPOINT + '/videos/' + this.id.toString() + '/download';
};
downloadFile 메서드에서 request.get()을 호출하나, timeout 옵션이 설정되지 않았다:
private downloadFile = (url: string, path: string, headers: any): Promise<void> => new Promise((resolve, reject) => {
this.cupixAuth.checkToken()
.then(() => {
logger.debug('TransferManager::downloadFile | begin path: %s, url: %s', path, url);
const fileStream = fs.createWriteStream(path);
const sendReq = request.get(url, {
headers: headers
});
sendReq
.on('response', res => {
if (res.statusCode === 200) {
sendReq.pipe(fileStream);
} else {
logger.error('TransferManager::downloadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
reject(this.cupixAuth.handleError(res));
}
})
.on('error', err => {
logger.error('TransferManager::downloadFile | path: %s, error: %s', path, JSON.stringify(err, Object.getOwnPropertyNames(err)));
reject(this.cupixAuth.handleError(err));
});
})
.catch(err => {
logger.error('TransferManager::downloadFile | checkToken error: %s', JSON.stringify(err, Object.getOwnPropertyNames(err)));
reject(err);
});
});
reject 이후 retryTask가 호출되며, 504는 5xx 범위이므로 retry 대상이다:
private checkStatusCode = (error: any): boolean => {
if (error?.statusCode != undefined && error.statusCode > 400 && error.statusCode < 500) return false;
return true;
};
retryTask = (task: BaseTask, error: any): void => {
if (this.checkStatusCode(error) && task.retry()) {
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.renew()
.then(() => {
this.addTask(task);
})
.catch(error => {
this.retryTask(task, error);
});
}, Constants.RetryInterval);
} else {
this.failTask(task, error);
}
};
retry 설정: 최대 5회, 10초 간격 (고정, exponential backoff 없음):
export const MaxRetries = 5;
export const RetryInterval = 10000; // ms
그러나 downloadFile의 reject에서 this.cupixAuth.handleError(res)가 반환하는 객체의 형태가 중요하다. handleError 메서드는 res.response 경로에서 statusCode를 추출하려 하나, request 라이브러리의 response 객체에는 res.response가 없어 response가 undefined로 처리된다:
handleError = (ec: any): any => {
const response = ec && CPUtils.isJsonString(ec) ? JSON.parse(ec) : ec.response;
if (response != undefined) {
const statusCode = response.status || response.statusCode;
// ...
} else {
logger.warn('CupixAuth::handleError | Undefined response: %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
}
return ec;
};
Datadog 로그에서 handleError가 "Undefined response" 경로를 타는 것이 확인되었다. handleError는 원래 ec 객체를 그대로 반환하므로, retryTask의 checkStatusCode(error)에서 error.statusCode가 504로 확인되어 retry는 정상 동작한다. 다만 retry 5회 모두 실패할 경우 failTask가 호출되어 전체 transfer pipeline이 중단된다 (_running = false, _stop = true).
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::downloadFile"
3건의 downloadFile 504 에러:
{"timestamp": "2026-04-23 17:06:11 KST", "status": "error", "message": "TransferManager::downloadFile | response path: /tmp/workspace/680672/videos/VID_20260414_134128_00_454.insv, code: 504, message: Gateway Time-out"}
{"timestamp": "2026-04-23 17:27:19 KST", "status": "error", "message": "TransferManager::downloadFile | response path: /tmp/workspace/684875/videos/VID_20260420_164532_00_317.insv, code: 504, message: Gateway Time-out"}
{"timestamp": "2026-04-23 17:27:19 KST", "status": "error", "message": "TransferManager::downloadFile | response path: /tmp/workspace/684875/videos/VID_20260420_164532_10_317.insv, code: 504, message: Gateway Time-out"}
handleError의 warn 로그 (504 응답 구조 확인):
service:cupixworks-capture-3dreconstruction-instance status:warn "handleError"
{"timestamp": "2026-04-23 17:06:11 KST", "status": "warn", "message": "CupixAuth::handleError | Undefined response: {\"statusCode\":504,\"request\":{\"method\":\"GET\"}}"}
{"timestamp": "2026-04-23 17:27:19 KST", "status": "warn", "message": "CupixAuth::handleError | Undefined response: {\"statusCode\":504,\"request\":{\"method\":\"GET\"}}"}
동시간대 upload 504 에러 (nginx 원인 확인):
service:cupixworks-capture-3dreconstruction-instance status:error "504"
{"timestamp": "2026-04-23 17:43:55 KST", "status": "error", "message": "TransferManager::uploadNewPointclouds | {\"message\":\"HTTP request failed\",\"response\":{\"body\":\"<html>\\r\\n<head><title>504 Gateway Time-out</title></head>\\r\\n<body>\\r\\n<center><h1>504 Gateway Time-out</h1></center>\\r\\n<hr><center>nginx</center>\\r\\n</body>\\r\\n</html>\\r\\n\",\"statusCode\":504},\"statusCode\":504,\"name\":\"HttpError\"}"}
Tesla API 측 에러 부재 확인:
service:cupixworks-api status:error "504"
service:cupixworks-api status:error "timeout"
위 쿼리로 동일 시간대(08:00~08:30 UTC) 검색 시 결과 0건 — Rails 프로세스에서 에러를 로깅하지 않았으므로, 요청이 Rails에 도달하기 전 nginx 레벨에서 timeout이 발생했거나, Rails 처리 시간이 nginx proxy_read_timeout을 초과한 것으로 판단.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Nginx 리버스 프록시의 proxy_read_timeout 초과로 인한 504 — Tesla API(Rails)의 비디오 다운로드 처리가 느려 nginx가 먼저 연결을 끊음 |
upload/download 모두 동일한 nginx 504 HTML 응답 반환, cupixworks-api에 에러 로그 없음, 504 응답 body에 <center>nginx</center> 포함 |
— | Confirmed |
| H2 | 대용량 .insv 파일(360 카메라 영상)의 S3 → Tesla API → Agent 전송 중 처리 시간 초과 |
.insv 파일은 일반적으로 수백 MB~수 GB 크기, /videos/:id/download endpoint는 스토리지에서 파일을 읽어 전달하는 과정이 필요 |
cupixworks-api에 timeout/에러 로그 없음 — Rails 프로세스 문제인지 확인 불가 |
Inconclusive |
| H3 | Agent 측 request.get() timeout 미설정으로 인한 자체 timeout |
코드에 request.get(url, { headers }) — timeout 옵션 없음 (transfer.manager.ts:55-57) |
504는 서버 측 응답이므로 클라이언트 timeout이 아닌 nginx 응답. handleError 로그에 statusCode: 504 확인 |
Rejected |
| H4 | Tesla API 인스턴스 과부하 또는 일시적 불안정 | 08:06~08:43 UTC 동안 download + upload 모두 504 발생 (20건 이상), 여러 workspace에서 동시 발생 | 08:43 UTC 이후 에러 없음 — 지속적 문제가 아닌 일시적 현상 | Confirmed (contributing) |
Fix Recommendation#
즉시 조치 (Critical)#
- 이 에러는 Tesla API 앞단 nginx의 일시적인 upstream timeout으로, agent 코드 자체의 버그는 아니다.
- 504에 대한 retry 로직은 이미 존재하며 정상 동작한다 (
checkStatusCode에서 504는 retry 대상). - 현재 retry 설정(5회, 10초 고정)이 충분한지 확인 필요 —
.insv파일 다운로드에 대해서는 더 긴 retry interval 또는 exponential backoff가 적절할 수 있다.
단기 개선 (1주 이내)#
transfer.manager.ts:55-57의request.get()호출에 명시적인timeout옵션 추가 — 현재 timeout 미설정으로 nginx timeout에만 의존. agent 측에서도 합리적인 timeout(예: 300초)을 설정하여 hang 상태를 방지.- retry 간격을 exponential backoff로 변경 — 현재 10초 고정 간격(
Constants.RetryInterval)이지만, 일시적 과부하 시 exponential backoff(예: 10s, 20s, 40s, 80s, 160s)가 서버 부하 완화에 더 효과적. - nginx의
proxy_read_timeout설정 확인 — 대용량 파일 전송 endpoint에 대해 적절한 timeout 값이 설정되어 있는지 인프라팀에 확인 요청.
장기 개선 (재발 방지)#
- 대용량 비디오 다운로드 시 Tesla API를 거치지 않고 S3 presigned URL을 직접 사용하는 방식 검토 — nginx/Rails를 경유하지 않으면 proxy timeout 문제를 근본적으로 회피 가능.
- agent의
requestnpm 패키지는 deprecated 상태이므로,axios또는undici로 전환 시 더 세밀한 timeout/retry 제어 가능.
Monitoring#
- 504 에러 빈도 모니터링:
service:cupixworks-capture-3dreconstruction-instance status:error "504"
- nginx upstream timeout 메트릭 확인 (cupix-infrastructure 측 nginx access log 기반):
avg:nginx.upstream_response_time{service:cupixworks-api}
- downloadFile 실패 후 retry 성공률 추적을 위한 메트릭 추가 권장.
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 사유: 일시적인 인프라 레벨 timeout으로 agent 코드 버그는 아님. retry 로직이 존재하여 대부분 자동 복구됨. 그러나 대용량 파일에서 반복 발생 가능성이 있으므로, timeout 설정과 backoff 개선이 권장됨.