ES /docs

TransferManager::downloadFile | path: /tmp/workspace/722380/videos/680843/original/680843.insv, erro

RCA: TransferManager::downloadFile ECONNRESET during .insv download

Overview#

What Happened#

2026-06-26 14:31 KST, cupixworks-capture-preprocessor-agent (us-west-2)에서 capture 722380의 비디오 파일 680843.insv를 다운로드하는 도중 원격 호스트로부터 TCP 연결이 끊겨 ECONNRESET (errno -104)이 발생했다. 동일 capture 의 다운로드는 약 1분 뒤 재시도되어 성공했고 (PreprocessorService::setVideoImageMatchData | begin - capture_id: 722380 @ 14:32:34 KST), 후속 preprocessing 도 정상 수행되었다. 동일 패턴(ECONNRESET/ETIMEDOUT/503) 의 transient download 실패가 지난 7일간 21건 관측되지만 TransferManager::failTask 로그(최종 실패) 는 한 건도 없다.

Quick Facts#

Field Value
exception.class ECONNRESET (Node.js socket error)
exception.message {"errno":-104,"code":"ECONNRESET","syscall":"read"}
top_frame packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:77
runtime Node.js (request library)
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
cupixworks-capture-preprocessor-agent (chrischoi) 1 (이 클러스터) / 21 (7d, 동일 패턴) 단일 다운로드 시도가 실패했으나 retry 로 복구. 사용자 영향 없음

Timeline#

  1. 2026-06-26 14:31:33 KSTTransferManager::downloadFile 가 video 680843.insv 다운로드 중 ECONNRESET 으로 실패. error 로그 발생.
  2. 2026-06-26 14:32:34 KSTPreprocessorService::setVideoImageMatchData | begin - capture_id: 722380 — retry 후 다운로드가 성공하여 다음 preprocessing 단계로 진입.
  3. 2026-06-26 14:33:43 KSTPreprocessorService::runPreprocessor | photo count: 0, video count: 2 — preprocessor 정상 실행.
  4. 2026-06-26 14:33:54 KSTBaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/722380 — workspace cleanup, 작업 완료.

Error Log#

Datadog Logs

text
TransferManager::downloadFile | path: /tmp/workspace/722380/videos/680843/original/680843.insv, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}

Impact#

  • Service: cupixworks-capture-preprocessor-agent
  • Team: chrischoi
  • 발생 횟수: 1 (이 클러스터). 동일 패턴 (ECONNRESET/ETIMEDOUT/503 transient) 은 지난 7일 21건.
  • 최초 발생: 2026-06-26 14:31:33 KST
  • 최근 발생: 2026-06-26 14:31:33 KST

Root Cause Summary#

원격 다운로드 서버(원본 비디오 저장소) 가 응답을 보내던 중 TCP 연결을 끊어 Node.js 의 request 라이브러리가 ECONNRESET 을 raise 했다. 이는 네트워크/서버 측에서 산발적으로 발생하는 transient I/O 오류이며, TransferManager 의 retry 메커니즘(MaxRetries=5, RetryInterval=10s, checkStatusCode 가 statusCode 없는 에러를 retry 대상으로 분류) 이 정상 동작하여 동일 capture 가 1 분 내 재시도 후 성공했다. 따라서 버그가 아니라 정상적인 transient 장애 이지만, retry 후 회복 가능한 사건이 logger.error 로 기록되고 있어 error-sweeper 같은 alerting 파이프라인이 false positive 로 인식하는 신호 노이즈 문제다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:50 (downloadFile)
  • Failure point: packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:77 (request stream error 이벤트)
  • Recovery point: packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:175 (retryTask) — checkStatusCode 가 true 면 최대 5회 재시도

downloadFilerequest.get(url) 스트림에 error 이벤트 핸들러를 달고, 소켓이 끊기면 다음 라인이 실행되어 logger.error 로 기록 후 reject 된다.

packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:67-79typescript
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));
        reject(this.cupixAuth.handleError(err));
    });

이렇게 reject 된 promise 는 transferTask 의 catch 절에서 잡혀 retryTask 로 전달된다.

packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:195-216typescript
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);
        } else {
            this.failTask(task, {});
            return;
        }

        this.completeTask(task);
    } catch (error) {
        this.retryTask(task, error);
    }
};

retryTaskcheckStatusCode 가 true(즉 4xx 가 아닌 경우 — ECONNRESET 은 statusCode 자체가 없으므로 true) 이고 남은 retry 횟수가 있으면 RetryInterval=10s 후 재시도한다.

packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:170-193typescript
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);
    }
};

MaxRetries=5, RetryInterval=10000ms (packages/shared-config/src/constants.ts:8-9). 최종 실패 시에만 failTasklogger.error 로 기록된다.

기대 동작: 일시적 네트워크 오류는 retry 로 복구되므로 단일 시도 실패를 logger.warn 으로 기록. 실제 동작: 매 시도마다 logger.error 로 기록되어 alerting 파이프라인이 회복 가능한 사건을 incident 로 분류.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-capture-preprocessor-agent "TransferManager::downloadFile"

Capture 722380 의 전체 흐름:

text
2026-06-26 14:31:33 [error] TransferManager::downloadFile | path: /tmp/workspace/722380/videos/680843/original/680843.insv, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-26 14:32:34 [info]  PreprocessorService::setVideoImageMatchData | begin - capture_id: 722380
2026-06-26 14:33:43 [info]  PreprocessorService::runPreprocessor | photo count: 0, video count: 2, app AR file path: /tmp/workspace/722380/app/processing_options.json
2026-06-26 14:33:54 [info]  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/722380

에러 후 약 1분 만에 동일 capture 의 후속 단계가 정상 진행 → retry 성공.

7일치 동일 패턴 (총 21건) 표본:

text
2026-06-26 14:31:33 ECONNRESET errno:-104 path: .../722380/.../680843.insv
2026-06-26 11:18:37 ETIMEDOUT  errno:-110 path: .../722385/.../680852.insv
2026-06-26 01:02:23 ETIMEDOUT  errno:-110 path: .../721904/.../680282.insv
2026-06-25 21:34:07 ECONNRESET            path: .../721843/.../680218.insv
2026-06-24 22:48:08 ETIMEDOUT  errno:-110 path: .../40130/.../50906.insv
2026-06-24 22:18:58 ECONNRESET            path: .../40017/.../50789.insv
... (총 21건, 모두 .insv 비디오 다운로드)

최종 실패 로그 검증:

text
service:cupixworks-capture-preprocessor-agent "TransferManager::failTask"
→ now-7d: 0 logs

7일간 failTask 가 한 건도 없음 → 모든 transient 실패가 retry 로 회복되었음을 확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 원격 비디오 저장소(또는 중간 네트워크) 의 transient TCP reset/timeout — retry 로 회복 에러 후 1분 내 동일 capture(722380) preprocessing 성공 (14:32:34 setVideoImageMatchData); 7일간 21건 발생했으나 failTask 0건; errno:-104, errno:-110 은 OS 레벨 transient I/O 오류 Confirmed
H2 4xx 인증/권한 오류 (만료된 presigned URL 등) checkStatusCode 가 4xx 이면 retry 안 함 → 즉시 failTask 호출되어야 함 에러 객체에 statusCode 필드 없음 ({"errno":-104,"code":"ECONNRESET","syscall":"read"}); failTask 로그 0건 Rejected
H3 다운로드 대상 파일(.insv) 이 너무 커서 매번 끊김 — 구조적 문제 모든 사례가 .insv 비디오; 큰 파일에서 ECONNRESET/ETIMEDOUT 빈도 ↑ 자연스러움 동일 capture 가 retry 로 결국 성공; 7일 21건 분산 발생(특정 시간대 집중 없음); 동일 .insv 파일이 영구 실패한 사례 없음 Rejected
H4 request 라이브러리(deprecated) 의 keep-alive/socket 관리 결함 request 모듈은 2020년 deprecated; 최신 Node 버전에서 알려진 socket 관리 이슈 존재 동일 호출이 retry 로 성공하므로 라이브러리 자체 결함이라기보다 원격 측 일시 장애로 해석 더 합리적. 다만 라이브러리 교체는 장기 개선 항목으로 유효 Inconclusive

Fix Recommendation#

즉시 조치 (Critical)#

없음. 이 사건은 retry 로 자가 회복되었고 사용자 영향이 없으며, 7일간 동일 패턴 21건 중 최종 실패(failTask) 가 0건이다. 코드 변경 불필요.

단기 개선 (1주 이내)#

  • 로그 레벨 다운그레이드packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:77logger.errorlogger.warn 으로 변경. 같은 파일 line 72(non-200 response), line 82(token error), line 101(upload response), line 106(upload error) 도 transient retry 대상이면 warn 으로 강등 검토.
  • 근거: failTask (line 167) 는 retry 모두 소진 후 최종 실패에서만 호출되며 이미 logger.error. 단일 시도 실패는 회복 가능한 사건이므로 warn 이 적절. 본 메모리의 AUTH20022/AUTH20023 episode (cc2e2887) 와 동일한 패턴.
  • 이렇게 변경하면 error-sweeper / Datadog error monitor 에 false positive 가 사라지고, 진짜 incident (= failTask 로그) 만 alerting 된다.

장기 개선 (재발 방지)#

  • request 라이브러리 교체: request 는 2020년 deprecated. undici, got, axios 등으로 마이그레이션하면 keep-alive/socket pool/HTTP2 등 안정성 개선 기대. 다른 download path (download 모듈, line 19) 와도 일관성 확보.
  • 다운로드 안정성 메트릭화: download attempt 수, retry 수, 최종 실패 수, 평균 retry 회수를 metric 으로 emit → SLO 추적. 현재는 retry 가 logger.debug 라 Datadog 에 남지 않아 가시성 부족.

Monitoring#

진짜 실패(= 모든 retry 소진 후 최종 실패) 만 추적:

text
service:cupixworks-capture-preprocessor-agent "TransferManager::failTask"

이 쿼리에 매치가 발생하면 incident — 알림 임계: 1건이라도 발생 시 notify.

Transient 발생률 추세 (참고용, alerting 대상 아님):

text
service:cupixworks-capture-preprocessor-agent "TransferManager::downloadFile" ("ECONNRESET" OR "ETIMEDOUT")

장기 추세가 급증할 경우 원격 저장소 / 네트워크 측 문제 의심.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (로그 레벨 변경 단건)