TransferManager missing socket timeout — TLS read ETIMEDOUT
RCA: TransferManager::downloadFile ETIMEDOUT
Error Log#
text
TransferManager::downloadFile | path: /tmp/workspace/33337/videos/VID_20260410_083554_00_133.insv, error: {"stack":"Error: read ETIMEDOUT
at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)","message":"read ETIMEDOUT","errno":-110,"code":"ETIMEDOUT","syscall":"read"}
Impact#
- Service:
cupixworks-capture-3dreconstruction-instance - Team: hassan-allam
- 발생 횟수: 2
- 최초 발생: 2026-04-13T05:31:42.152Z
- 최근 발생: 2026-04-13T05:31:49.233Z
Root Cause Summary#
3D Reconstruction agent가 S3에서 대용량 .insv 비디오 파일을 다운로드하는 과정에서 TLS 레벨의 read ETIMEDOUT (errno -110)이 발생했습니다. TransferManager::downloadFile 메서드가 request.get()을 호출할 때 timeout 옵션을 설정하지 않아 Node.js의 기본 소켓 타임아웃(약 2분)에 의존하고 있으며, retry 메커니즘은 존재하지만 (MaxRetries=5, RetryInterval=10s), 에러 발생 후 failTask에서 _running=false, _stop=true로 설정하여 전체 전송 큐를 중단시킵니다. 두 건의 에러는 서로 다른 컨테이너(capture 33337, 33404)에서 7초 간격으로 동시 발생했으며, 로그에 따르면 데이터는 me-central-1 (Middle East) S3 리전에 저장되어 있으나 Reconstruction 인스턴스의 환경 로그는 region=us-west-2를 보여주어 크로스 리전 S3 다운로드가 네트워크 불안정성의 원인일 가능성이 높습니다.
Technical Analysis#
Code Path#
- Entry point:
transfer.manager.ts:251—downloadVideos()메서드가DownloadVideosContainer를 생성하고 각 비디오에 대해addTask()를 호출합니다.
typescript
// applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:251-263
downloadVideos = (items: CPVideo[]): Promise<void> => new Promise((resolve, reject) => {
const container = new DownloadVideosContainer(this, items, resolve, reject);
container.init()
.then(() => {
container.tasks.forEach(task => {
this.addTask(task);
});
})
.catch(ec => {
logger.error('TransferManager::downloadVideos | %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
reject();
});
});
transfer.manager.ts:195-216—transferTask()가downloadFile()을 호출하고, 에러 시retryTask()로 전달합니다.
typescript
// applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:195-216
private transferTask = async (task: BaseTask): Promise<void> => {
try {
await task.init();
if (task.url == undefined || task.target == undefined) {
this.failTask(task, {});
return;
}
if (task.action === TaskAction.Download) {
await this.downloadFile(task.url, task.target, task.Headers);
} else if (task.action === TaskAction.Upload) {
await this.uploadFile(task.url, task.target, task.Headers);
}
this.completeTask(task);
} catch (error) {
this.retryTask(task, error);
}
};
- Failure point:
transfer.manager.ts:50-85—downloadFile()메서드에서request.get()에 timeout 옵션이 없습니다. 에러 이벤트 핸들러(:76-78)가 ETIMEDOUT을 잡아reject()합니다.
typescript
// applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:50-85
private downloadFile = (url: string, path: string, headers: any): Promise<void> => new Promise((resolve, reject) => {
this.cupixAuth.checkToken()
.then(() => {
const fileStream = fs.createWriteStream(path);
const sendReq = request.get(url, {
headers: headers
// ← timeout 옵션 없음
});
sendReq
.on('response', res => {
if (res.statusCode === 200) {
sendReq.pipe(fileStream);
} else {
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 => {
reject(err);
});
});
- Retry 로직:
transfer.manager.ts:175-193—retryTask()는checkStatusCode()를 통해 4xx 에러가 아닌 경우 retry를 시도합니다. ETIMEDOUT은statusCode가 없으므로checkStatusCode()가true를 반환하여 retry 대상입니다. 그러나retryTask호출 전에 에러가transferTask의 catch에서 처리되므로 retry가 작동해야 하지만, 로그에서 retry 시도가 관찰되지 않았습니다 — 이는 retry가 발생했으나 동일하게 실패했거나, 다른 경로로 에러가 전파되었을 가능성을 시사합니다.
typescript
// applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:160-168
private failTask = (task: BaseTask, error: any): void => {
this._running = false; // ← 전체 큐 중단
this._stop = true; // ← 전체 큐 중단
const idx = this._transferring.indexOf(task);
this._transferring.splice(idx, 1);
this._failed.push(task);
task.postErrorProcess(error);
};
CupixAuth::handleError(cupix-auth.ts:42-57): ETIMEDOUT 에러는response속성이 없으므로else분기로 진입하여 warn 로그를 남기고 원래 에러 객체를 그대로 반환합니다.
typescript
// applications/agents/packages/api/src/authentication/cupix-auth.ts:42-57
handleError = (ec: any): any => {
const response = ec && CPUtils.isJsonString(ec) ? JSON.parse(ec) : ec.response;
if (response != undefined) {
logger.warn('CupixAuth::handleError | Response statusCode: %d, ...', ...);
} else {
logger.warn('CupixAuth::handleError | Undefined response: %s', JSON.stringify(ec, ...));
}
return ec;
};
Log Evidence#
사용한 Datadog 쿼리:
text
service:cupixworks-capture-3dreconstruction-instance status:error @environment:production "TransferManager::downloadFile"
text
service:cupixworks-capture-3dreconstruction-instance @environment:production (agent.hostname:6e6629c6a7bf OR agent.hostname:ef7cd456e241)
컨테이너 6e6629c6a7bf (capture 33337) 타임라인:
| 시각 (UTC) | 레벨 | 이벤트 |
|---|---|---|
| 05:14:25.126 | info | ThreeDReconstruction::init, authenticate 시작 |
| 05:14:25.182 | info | authenticate 완료, run 시작 |
| 05:14:25.183 | info | JobManager::loadJob (job 89414) |
| 05:14:25.761 | info | loadVideos: 1 video |
| 05:14:25.817 | info | loadClusters: 1 cluster |
| 05:31:42.152 | error | TransferManager::downloadFile ETIMEDOUT |
| 05:31:42.153 | warn | CupixAuth::handleError | Undefined response |
| 05:32:12.467 | info | ThreeDReconstruction environments: region=us-west-2 |
컨테이너 ef7cd456e241 (capture 33404) 타임라인:
| 시각 (UTC) | 레벨 | 이벤트 |
|---|---|---|
| 05:14:32.067 | info | ThreeDReconstruction::init, authenticate 시작 |
| 05:14:32.125 | info | authenticate 완료, run 시작 |
| 05:14:32.685 | info | loadVideos: 1 video |
| 05:31:49.233 | error | TransferManager::downloadFile ETIMEDOUT |
| 05:31:49.234 | warn | CupixAuth::handleError | Undefined response |
| 05:32:19.034 | info | ThreeDReconstruction environments: region=us-west-2 |
핵심 관찰 사항:
- 두 컨테이너 모두 다운로드 시작부터 에러까지 약 17분 17초 경과 — 이는 OS 레벨의 TCP read timeout (일반적으로
net.ipv4.tcp_retries2기본값 15에 해당하는 ~13-30분)과 일치 - 에러 발생 후 약 30초 뒤
ThreeDReconstruction environments로그가 출력되었으며,region=us-west-2로 표시 - S3 다운로드 URL은
s3.me-central-1.amazonaws.com으로 리다이렉트 — 크로스 리전 접근 확인
json
{
"message": "TransferManager::downloadFile | path: /tmp/workspace/33337/videos/VID_20260410_083554_00_133.insv, error: {\"stack\":\"Error: read ETIMEDOUT\\n at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\",\"message\":\"read ETIMEDOUT\",\"errno\":-110,\"code\":\"ETIMEDOUT\",\"syscall\":\"read\"}",
"host": "6e6629c6a7bf",
"service": "cupixworks-capture-3dreconstruction-instance",
"status": "error"
}
json
{
"message": "CupixAuth::handleError | Undefined response: {\"stack\":\"Error: read ETIMEDOUT\\n at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\",\"message\":\"read ETIMEDOUT\",\"errno\":-110,\"code\":\"ETIMEDOUT\",\"syscall\":\"read\"}",
"host": "6e6629c6a7bf",
"service": "cupixworks-capture-3dreconstruction-instance",
"status": "warn"
}
Fix Recommendation#
즉시 조치 (Critical)#
transfer.manager.ts:55—request.get()호출에timeout옵션을 추가해야 합니다.request라이브러리는{timeout: ms}옵션을 지원하며, 대용량 비디오 파일 다운로드를 고려하여timeout을 적절히 설정하되, 읽기 타임아웃을 별도로 지정하는 것이 좋습니다 (예: connection timeout 30초, read timeout 5분).transfer.manager.ts:76-78—on('error')핸들러에서 부분적으로 다운로드된 파일(fs.createWriteStream으로 생성된 파일)을 정리(삭제)하는 로직이 없습니다. 에러 발생 시 불완전한 파일이 남아 후속 처리에서 문제를 일으킬 수 있습니다.
단기 개선 (1주 이내)#
- 크로스 리전 다운로드 문제 조사: Reconstruction 인스턴스가
us-west-2에서 실행되면서me-central-1S3 버킷에서 대용량 비디오를 다운로드하는 구조가 근본적인 네트워크 지연 및 타임아웃 원인입니다. hassan-allam 팀의 데이터가 저장된 리전과 동일한 리전에서 Reconstruction 인스턴스를 실행하도록 라우팅 로직을 검토해야 합니다. - Retry 로그 강화:
retryTask()호출 시 info 레벨 로그를 추가하여 retry 시도 여부와 횟수를 명확히 추적할 수 있도록 해야 합니다. 현재는 debug 레벨이라 Datadog에서 확인이 불가합니다.
장기 개선 (재발 방지)#
- S3 Transfer Acceleration 또는 리전별 인스턴스 배치: 크로스 리전 대용량 파일 전송의 안정성을 위해 S3 Transfer Acceleration을 활성화하거나, 데이터 리전에 맞춰 Reconstruction 인스턴스를 배치하는 아키텍처 변경이 필요합니다.
request라이브러리 마이그레이션:request라이브러리는 2020년에 deprecated 되었습니다.axios,got, 또는 Node.js 내장fetch로 마이그레이션하면 더 나은 타임아웃 제어, retry 내장, 스트리밍 지원을 확보할 수 있습니다.- 다운로드 진행률 모니터링: 대용량 파일 다운로드에 진행률 로깅을 추가하여 다운로드가 멈춘 시점을 정확히 파악할 수 있도록 해야 합니다.
Monitoring#
- ETIMEDOUT 에러 빈도 모니터링:
text
service:cupixworks-capture-3dreconstruction-instance status:error "ETIMEDOUT"
- 크로스 리전 다운로드 패턴 감지 (환경 로그와 S3 리전 비교):
text
service:cupixworks-capture-3dreconstruction-instance "ThreeDReconstruction::environments" "region"
- 다운로드 소요 시간 추적 (debug 로그 begin/complete 쌍의 시간 차이):
text
service:cupixworks-capture-3dreconstruction-instance "TransferManager::downloadFile" ("begin" OR "complete")
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard — timeout 옵션 추가는 간단하지만, 크로스 리전 라우팅 문제는 인프라 레벨 변경이 필요합니다. 현재 발생 빈도가 2건으로 낮지만, hassan-allam 팀(Middle East 리전)의 모든 capture에서 동일한 패턴이 재발할 수 있습니다.