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#
- 2026-06-26 14:31:33 KST —
TransferManager::downloadFile가 video680843.insv다운로드 중 ECONNRESET 으로 실패. error 로그 발생. - 2026-06-26 14:32:34 KST —
PreprocessorService::setVideoImageMatchData | begin - capture_id: 722380— retry 후 다운로드가 성공하여 다음 preprocessing 단계로 진입. - 2026-06-26 14:33:43 KST —
PreprocessorService::runPreprocessor | photo count: 0, video count: 2— preprocessor 정상 실행. - 2026-06-26 14:33:54 KST —
BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/722380— workspace cleanup, 작업 완료.
Error Log#
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(requeststreamerror이벤트) - Recovery point:
packages/cupix-capture-preprocessor-agent/src/manager/transfer.manager.ts:175(retryTask) —checkStatusCode가 true 면 최대 5회 재시도
downloadFile 는 request.get(url) 스트림에 error 이벤트 핸들러를 달고, 소켓이 끊기면 다음 라인이 실행되어 logger.error 로 기록 후 reject 된다.
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 로 전달된다.
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);
}
};
retryTask 는 checkStatusCode 가 true(즉 4xx 가 아닌 경우 — ECONNRESET 은 statusCode 자체가 없으므로 true) 이고 남은 retry 횟수가 있으면 RetryInterval=10s 후 재시도한다.
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). 최종 실패 시에만 failTask 가 logger.error 로 기록된다.
기대 동작: 일시적 네트워크 오류는 retry 로 복구되므로 단일 시도 실패를 logger.warn 으로 기록.
실제 동작: 매 시도마다 logger.error 로 기록되어 alerting 파이프라인이 회복 가능한 사건을 incident 로 분류.
Log Evidence#
Datadog 쿼리:
service:cupixworks-capture-preprocessor-agent "TransferManager::downloadFile"
Capture 722380 의 전체 흐름:
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건) 표본:
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 비디오 다운로드)
최종 실패 로그 검증:
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:77의logger.error를logger.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 소진 후 최종 실패) 만 추적:
service:cupixworks-capture-preprocessor-agent "TransferManager::failTask"
이 쿼리에 매치가 발생하면 incident — 알림 임계: 1건이라도 발생 시 notify.
Transient 발생률 추세 (참고용, alerting 대상 아님):
service:cupixworks-capture-preprocessor-agent "TransferManager::downloadFile" ("ECONNRESET" OR "ETIMEDOUT")
장기 추세가 급증할 경우 원격 저장소 / 네트워크 측 문제 의심.
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (로그 레벨 변경 단건)