TransferManager::uploadFile | path: /tmp/workspace/722907/videos/681372/original/681372_00587.jpg, e
RCA: TransferManager::uploadFile ECONNRESET (me-central-1 S3 PUT)
Overview#
What Happened#
cupixworks-capture-postprocessor-agent (us-west-2 fleet)가 capture 722907의 원본 영상 프레임을 me-central-1 리전의 S3 버킷(cupixworks-source-...-mece1)에 PUT 업로드하던 중 TCP 연결이 원격에서 끊어져(ECONNRESET) 4건의 에러 로그가 19:25–19:44 UTC 사이에 발생했습니다. 동일 워크스페이스(722907)는 cleanUpAnythingRelatedModel 까지 정상 진행되어 retry 로직(MaxRetries=5, 10s interval)에 의해 자동 복구된 것으로 확인됩니다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error (Node.js ECONNRESET) |
| exception.message | {"code":"ECONNRESET"} / {"errno":-104,"code":"ECONNRESET","syscall":"read"} |
| top_frame | packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:104 |
| runtime | Node.js (request library PUT stream) |
| env | production, agent region us-west-2, S3 bucket region me-central-1 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| flintco / cupixworks-capture-postprocessor-agent | 4 (in cluster window); 20+ similar in 3d | Retry로 자동 복구된 케이스 다수, 일부 capture (예: 722511/680984, 722514/680987, 722508/680982, 722429/680900)는 count: 5/5 까지 도달해 task 실패 |
Timeline#
- 2026-06-27 04:25 KST — capture 722907
PostprocessorService::getCqaAssignment직후 첫 ECONNRESET (frame 00587) - 2026-06-27 04:25 KST — frame 00593 추가 ECONNRESET
- 2026-06-27 04:27 KST — 동일 workspace
BaseService::cleanUpAnythingRelatedModel호출 → retry 성공, 정상 종료 - 2026-06-27 04:44 KST — 클러스터 window 마지막 ECONNRESET 관측 (
last_seen) - 2026-06-24 23:18–23:29 UTC — 동일 service 의
svc:cupixworks-capture-postprocessor-agent::unknown인시던트가 자동 해소된 이력 있음 (status-board)
Error Log#
TransferManager::uploadFile | path: /tmp/workspace/722907/videos/681372/original/681372_00587.jpg, error: {"code":"ECONNRESET"}
추가 표본 (Datadog 쿼리 service:cupixworks-capture-postprocessor-agent "TransferManager::uploadFile" "ECONNRESET", last 3d):
2026-06-26 22:21:11 TransferManager::uploadFile | path: /tmp/workspace/722508/videos/680982/original/680982_00184.jpg, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-26 22:54:01 TransferManager::uploadFile | path: /tmp/workspace/722514/videos/680987/original/680987_00195.jpg, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-26 23:01:03 TransferManager::uploadFile | path: /tmp/workspace/722511/videos/680984/original/680984_01668.jpg, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
최종 실패(=재시도 5회 모두 소진) 로그 표본:
2026-06-26 22:59:52 TransferManager::failTask | path: /tmp/workspace/722514/videos/680987/original/680987_00762.jpg, url: https://s3.me-central-1.amazonaws.com/cupixworks-source-b169da1a0187-mece1/... count: 5/5
2026-06-26 23:35:49 TransferManager::failTask | path: /tmp/workspace/722511/videos/680984/original/680984_02339.jpg, url: https://s3.me-central-1.amazonaws.com/... count: 5/5
Impact#
- Service:
cupixworks-capture-postprocessor-agent - Team: flintco
- 발생 횟수: 4 (클러스터 window 기준)
- 최초 발생: 2026-06-27 04:25 KST
- 최근 발생: 2026-06-27 04:44 KST
대부분의 ECONNRESET은 retry로 자동 복구되어 사용자 가시적 실패로 이어지지 않습니다. 다만 동일 시간대(3d 윈도우)에 me-central-1 리전 대상으로 5/5 retry 소진 후 failTask 처리된 케이스가 최소 7건 관측되어, postprocessing pipeline 단계에서 일부 capture가 실패 상태로 종료됐을 가능성이 있습니다 (확인은 capture/video 모델 검증 필요 — uncertain).
Root Cause Summary#
us-west-2에서 실행되는 postprocessor-agent가 request.put() 으로 Middle East (me-central-1) 리전의 S3 presigned URL에 대용량 파일(영상 프레임 JPG)을 cross-region PUT 업로드하던 중, 원격 측에서 TCP 연결을 reset 하여 socket hang up (ECONNRESET) 이 stream .on('error') 핸들러에서 잡혀 TransferManager::uploadFile 에러 로그가 남았습니다. ECONNRESET은 retry-safe 한 네트워크 일시 장애로, 코드 상 retryTask 가 status code 4xx 가 아니면 재시도하므로 대부분 자동 복구됩니다. 클러스터의 4건은 cross-region 경로의 통상적인 packet/TCP 변동성에 기인하는 transient 에러이며, capture 722907의 후속 cleanUpAnythingRelatedModel 로그가 그 직후에 남은 점으로 보아 retry 후 성공한 케이스입니다.
Technical Analysis#
Code Path#
- Entry point (PUT 시작):
packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:81 - Failure point (에러 로깅):
packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:104 - Retry decision:
transfer.manager.ts:173(retryTask) →transfer.manager.ts:168(checkStatusCode) - 재시도 카운트 정의:
packages/shared-config/src/constants.ts:8(MaxRetries = 5,RetryInterval = 10000ms)
업로드 코어:
uploadFile = (url: string, path: string, headers: any): Promise<void> => new Promise((resolve, reject) => {
const fileSize = CPUtils.getFileSize(path);
headers = { ...headers, 'Content-Length': fileSize };
logger.debug('TransferManager::uploadFile | begin path: %s, url: %s, size: %d', path, url, fileSize);
const fileStream = fs.createReadStream(path);
const sendReq = request.put(url, {
headers: headers,
body: fileStream
});
sendReq
.on('response', async res => {
if (res.statusCode === 200) {
logger.debug('TransferManager::uploadFile | complete path: %s', path);
await CPUtils.sleep(100);
resolve();
} else {
logger.error('TransferManager::uploadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
reject(this.cupixAuth.handleError(res));
}
})
.on('error', err => {
logger.error('TransferManager::uploadFile | path: %s, error: %s', path, JSON.stringify(err));
reject(this.cupixAuth.handleError(err));
});
});
호출자(transferTask)와 retry 분기:
private transferTask = async (task: BaseTask): Promise<void> => {
try {
await task.init();
if (task.url == undefined || task.target == undefined) {
throw new Error('TransferManager::transferTask | url or target is undefined');
}
if (task.action === TaskAction.Download) {
await this.File(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;
}
task.setTransferDone();
await task.intermediate();
this.completeTask(task);
} catch (error) {
this.retryTask(task, error);
}
};
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.isRetry()) {
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.retry()
.then(() => { this.addTask(task); })
.catch(error => { this.retryTask(task, error); });
}, Constants.RetryInterval);
} else {
this.failTask(task, error);
}
};
기대 동작: ECONNRESET은 statusCode 가 없는 network error → checkStatusCode 가 true 반환 → task.isRetry() 가 true 인 동안 10초 간격으로 최대 5회 재시도. 실제로도 capture 722907 은 두 차례 ECONNRESET 발생 후 cleanUpAnythingRelatedModel 까지 도달하여 retry 가 성공한 것으로 보입니다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-capture-postprocessor-agent "TransferManager::uploadFile" "ECONNRESET"
service:cupixworks-capture-postprocessor-agent "722907"
capture 722907 워크스페이스 로그(시간 정렬, 동일 쿼리 결과):
2026-06-27 04:08:49 info PostprocessorService::getCqaAssignment | capture: 722907, levelId: 73928, total floorplans: 1, bim: 0, published non-bim: 1
2026-06-27 04:09:00 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/722907
2026-06-27 04:20:30 info PostprocessorService::getCqaAssignment | capture: 722907, levelId: 73928, total floorplans: 1, bim: 0, published non-bim: 1
2026-06-27 04:25:40 error TransferManager::uploadFile | path: /tmp/workspace/722907/videos/681372/original/681372_00587.jpg, error: {"code":"ECONNRESET"}
2026-06-27 04:25:41 error TransferManager::uploadFile | path: /tmp/workspace/722907/videos/681372/original/681372_00593.jpg, error: {"code":"ECONNRESET"}
2026-06-27 04:27:45 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/722907
→ 첫 ECONNRESET 이후 약 2분 뒤 workspace 정리(cleanup)가 성공적으로 실행되어, retry 로직이 동작했음을 확인 (failTask 로그 없음).
타 capture의 최종 실패 사례 (모두 s3.me-central-1.amazonaws.com 도메인):
2026-06-26 22:59:52 error TransferManager::failTask | path: /tmp/workspace/722514/videos/680987/original/680987_00762.jpg, url: https://s3.me-central-1.amazonaws.com/cupixworks-source-b169da1a0187-mece1/... count: 5/5
2026-06-26 22:56:38 error TransferManager::failTask | path: /tmp/workspace/722508/videos/680982/original/680982_00209.jpg, url: https://s3.me-central-1.amazonaws.com/... count: 5/5
2026-06-26 23:35:49 error TransferManager::failTask | path: /tmp/workspace/722511/videos/680984/original/680984_02339.jpg, url: https://s3.me-central-1.amazonaws.com/... count: 5/5
서명 URL 의 X-Amz-Credential=.../me-central-1/s3/aws4_request 로 bucket region 이 me-central-1 임을 확인. 클러스터 frontmatter regions: - us-west-2 는 agent 실행 리전을 의미하므로, us-west-2 → me-central-1 cross-region PUT 경로가 ECONNRESET의 공통 분모입니다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | us-west-2 → me-central-1 cross-region 장거리 TCP 경로에서 일시적인 RST/idle timeout 으로 PUT 스트림이 끊김 (transient network) | 모든 표본 PUT URL이 s3.me-central-1.amazonaws.com. 같은 capture 가 retry 후 cleanup 까지 도달 (722907). 에러 페이로드가 {"code":"ECONNRESET"} 또는 {"errno":-104,"syscall":"read"} 로 socket 레벨 reset임 |
— | Confirmed |
| H2 | S3 me-central-1 리전 자체 장애 | status-board 에 6/24 동일 svc 인시던트가 있으나 자동 해소됨. dep:s3-me-central-1 active 인시던트 없음 | 동시 시간대 다른 리전 ECONNRESET 보고 없음. AWS health 페이지 별도 미확인 (uncertain) | Rejected (insufficient evidence for vendor outage) |
| H3 | Presigned URL 만료(7200s) 후 업로드 시도로 4xx 응답 | URL X-Amz-Expires=7200 |
로그는 response code 4xx 가 아니라 socket error 핸들러로 진입 (statusCode 필드 없음). checkStatusCode 가 retry 분기로 통과한 흐름과 일치 |
Rejected |
| H4 | 파일 누락/권한 오류(EACCES, ENOENT) | — | error payload 가 명시적으로 ECONNRESET (네트워크 계열) |
Rejected |
| H5 | postprocessor-agent 자체 버그(예: stream backpressure) | request lib의 stream PUT 흐름은 일반적 | 동일 코드가 다른 리전(us-*)에서는 같은 빈도로 보이지 않음 (3d 표본 모두 me-central-1) | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
없음. ECONNRESET은 retry 로직(MaxRetries=5, 10s interval)으로 대부분 흡수되며, 본 클러스터(4건)는 모두 자동 복구되었습니다. 추가 코드 변경 없이 모니터링만 강화하면 충분합니다.
대신 로그 레벨 검토가 유효합니다. retry가 성공한 시도까지 error 레벨로 남기면 alert 노이즈가 됩니다. transfer.manager.ts:104 의 첫 N-1 회 시도는 warn 으로 낮추고, failTask (count == MaxRetries) 만 error 로 두는 방안을 고려합니다. (재발 시 가시성 유지 + 노이즈 감소)
단기 개선 (1주 이내)#
- cross-region 업로드에 대한 keep-alive / connection 재사용 검토:
request라이브러리는 deprecated 이며, HTTP keep-alive agent 설정이 명시적이지 않습니다. me-central-1 PUT 에 한해agent옵션으로https.Agent({ keepAlive: true, timeout: ... })를 적용하면 idle TCP reset 빈도를 줄일 수 있습니다. - 재시도 백오프 강화: 현재 retry 간격은 고정 10초. 지수 백오프(예: 5s, 10s, 20s, 40s, 80s) + jitter 적용 시 동일 시간대 spike 의 retry 성공률이 올라갈 가능성.
shared-config/src/constants.ts의RetryInterval운용 정책 검토. - failTask 시 추가 컨텍스트 로깅:
failTask로그에 마지막 error 페이로드(code, syscall)를 함께 포함해 vendor outage vs idle reset 구분이 가능하도록 합니다. 현재failTask로그(165 line)에는 error 객체가 빠져 있어 사후 분석 시 정보가 부족합니다.
장기 개선 (재발 방지)#
request→@aws-sdk/client-s3또는undici마이그레이션: presigned URL PUT 은 AWS SDK 의PutObjectCommand+ signed URL 패턴으로 대체 가능하며, 내부적으로 multipart upload + 자동 재시도(SDK middleware) + keep-alive 가 표준화되어 있어 ECONNRESET 노이즈를 근본적으로 줄일 수 있습니다.- postprocessor 실행 리전과 capture 저장 리전 정합성 검토: us-west-2 agent 가 me-central-1 bucket에 직접 PUT 하는 패턴이 의도된 설계인지(데이터 주권 요건 등) 확인하고, 의도된 것이라면 dedicated VPC endpoint 또는 S3 Transfer Acceleration / multi-region access point 적용을 검토합니다.
Monitoring#
- ECONNRESET 발생 추이 (서비스/리전별):
service:cupixworks-capture-postprocessor-agent "TransferManager::uploadFile" "ECONNRESET"
- 재시도 5회 소진 실패만 추적 (실제 사용자 영향):
service:cupixworks-capture-postprocessor-agent "TransferManager::failTask" "count: 5/5"
- me-central-1 대상 PUT 실패율 점검:
service:cupixworks-capture-postprocessor-agent "failTask" "s3.me-central-1"
- Alert 임계 권장:
failTask count: 5/5가 1시간 내 5건 이상 또는 동일 capture 가 2회 이상이면 alert (transient noise 제외, 실제 데이터 손실 가능 케이스만)
Risk Assessment#
- Risk level: low (retry 로직이 정상 동작하며 자동 복구됨; 본 클러스터 4건은 모두 성공으로 종결됨)
- 예상 복잡도: trivial (즉시 조치는 로그 레벨 조정만; 코드 변경 없이 모니터링 강화로 대응 가능). 단기/장기 개선(SDK 마이그레이션) 은 별개 작업으로 standard 범위.