ES /docs

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#

  1. 2026-06-27 04:25 KST — capture 722907 PostprocessorService::getCqaAssignment 직후 첫 ECONNRESET (frame 00587)
  2. 2026-06-27 04:25 KST — frame 00593 추가 ECONNRESET
  3. 2026-06-27 04:27 KST — 동일 workspace BaseService::cleanUpAnythingRelatedModel 호출 → retry 성공, 정상 종료
  4. 2026-06-27 04:44 KST — 클러스터 window 마지막 ECONNRESET 관측 (last_seen)
  5. 2026-06-24 23:18–23:29 UTC — 동일 service 의 svc:cupixworks-capture-postprocessor-agent::unknown 인시던트가 자동 해소된 이력 있음 (status-board)

Error Log#

Datadog Logs

text
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):

text
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회 모두 소진) 로그 표본:

text
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 = 10000 ms)

업로드 코어:

packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:81-107typescript
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 분기:

packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:193-215typescript
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);
    }
};
packages/cupix-capture-postprocessor-agent/src/manager/transfer.manager.ts:168-191typescript
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 → checkStatusCodetrue 반환 → task.isRetry() 가 true 인 동안 10초 간격으로 최대 5회 재시도. 실제로도 capture 722907 은 두 차례 ECONNRESET 발생 후 cleanUpAnythingRelatedModel 까지 도달하여 retry 가 성공한 것으로 보입니다.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-capture-postprocessor-agent "TransferManager::uploadFile" "ECONNRESET"
text
service:cupixworks-capture-postprocessor-agent "722907"

capture 722907 워크스페이스 로그(시간 정렬, 동일 쿼리 결과):

text
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 도메인):

text
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.tsRetryInterval 운용 정책 검토.
  • 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 발생 추이 (서비스/리전별):
text
service:cupixworks-capture-postprocessor-agent "TransferManager::uploadFile" "ECONNRESET"
  • 재시도 5회 소진 실패만 추적 (실제 사용자 영향):
text
service:cupixworks-capture-postprocessor-agent "TransferManager::failTask" "count: 5/5"
  • me-central-1 대상 PUT 실패율 점검:
text
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 범위.