ES /docs

TransferManager missing socket timeout — TLS read ETIMEDOUT

RCA: TransferManager::downloadFile ETIMEDOUT

Error Log#

Datadog Logs

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:251downloadVideos() 메서드가 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-216transferTask()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-85downloadFile() 메서드에서 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-193retryTask()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

핵심 관찰 사항:

  1. 두 컨테이너 모두 다운로드 시작부터 에러까지 약 17분 17초 경과 — 이는 OS 레벨의 TCP read timeout (일반적으로 net.ipv4.tcp_retries2 기본값 15에 해당하는 ~13-30분)과 일치
  2. 에러 발생 후 약 30초 뒤 ThreeDReconstruction environments 로그가 출력되었으며, region=us-west-2로 표시
  3. 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:55request.get() 호출에 timeout 옵션을 추가해야 합니다. request 라이브러리는 {timeout: ms} 옵션을 지원하며, 대용량 비디오 파일 다운로드를 고려하여 timeout을 적절히 설정하되, 읽기 타임아웃을 별도로 지정하는 것이 좋습니다 (예: connection timeout 30초, read timeout 5분).
  • transfer.manager.ts:76-78on('error') 핸들러에서 부분적으로 다운로드된 파일(fs.createWriteStream으로 생성된 파일)을 정리(삭제)하는 로직이 없습니다. 에러 발생 시 불완전한 파일이 남아 후속 처리에서 문제를 일으킬 수 있습니다.

단기 개선 (1주 이내)#

  • 크로스 리전 다운로드 문제 조사: Reconstruction 인스턴스가 us-west-2에서 실행되면서 me-central-1 S3 버킷에서 대용량 비디오를 다운로드하는 구조가 근본적인 네트워크 지연 및 타임아웃 원인입니다. 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에서 동일한 패턴이 재발할 수 있습니다.