AwsS3Manager::uploadDirectoryByCredential | upload failed - {"code":"ECONNRESET","name":"TimeoutErro
RCA: AwsS3Manager::uploadDirectoryByCredential upload failed (ECONNRESET / TimeoutError)
Overview#
What Happened#
2026-08-05 01:11 KST, cupixworks-pano-postprocessor-instance (pano postprocessor agent, cupixworks applications/agents)의 resize 결과물 tile S3 업로드 단계에서 S3 PUT 요청이 ECONNRESET(TCP connection reset)로 실패했다. AWS SDK v2 가 기본 재시도 3회를 모두 소진한 뒤($metadata.attempts:3) TimeoutError 를 던졌고, uploadDirectoryByCredential 의 per-file catch 블록이 이를 로그만 남기고 삼켰다. 2건 발생, us-west-2, tenant cupix. 일시적 네트워크 이벤트이며 tesla/agent 코드 결함은 아니지만, 실패한 파일 업로드가 재시도·재전파 없이 조용히 유실되는 resilience gap 이 존재한다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | TimeoutError (AWS SDK v2, code ECONNRESET) |
| exception.message | AwsS3Manager::uploadDirectoryByCredential | upload failed - {"code":"ECONNRESET","name":"TimeoutError","$metadata":{"attempts":3,"totalRetryDelay":229}} |
| top_frame | packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:139 |
| runtime | Node.js / TypeScript, aws-sdk 2.1599.0 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad (pano postprocessor) | 2 | resize tile 일부 파일이 S3 에 업로드되지 않은 채로 postprocessing 이 성공 처리됨 — 해당 pano 의 특정 tile 리소스 누락 가능 |
Timeline#
- 2026-08-05 01:11:38 KST —
uploadDirectoryByCredential의 S3 PUT 이ECONNRESET로 실패, SDK 재시도 3회 소진 후TimeoutError. 동일 초에 2건 발생 (병렬 chunk 업로드). - 2026-08-05 — error-sweeper collector 가 클러스터 감지 (fingerprint
7c69cd1478524f944b092e9e6cec6452).
Error Log#
AwsS3Manager::uploadDirectoryByCredential | upload failed - {"code":"ECONNRESET","name":"TimeoutError","$metadata":{"attempts":3,"totalRetryDelay":229}}
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: gad
- 발생 횟수: 2
- 최초 발생: 2026-08-05 01:11:38 KST
- 최근 발생: 2026-08-05 01:11:38 KST
Root Cause Summary#
Pano postprocessor 가 resize 된 tile 이미지를 tesla 발급 임시 credential 로 S3 에 업로드하는 과정(s3.upload(...).promise())에서 S3 로의 TCP 연결이 ECONNRESET 으로 끊겼다. aws-sdk v2 S3 client 는 기본 maxRetries 로 3회 재시도했으나($metadata.attempts:3, totalRetryDelay:229) 모두 실패하여 TimeoutError 를 던졌다. 이는 전형적인 일시적 네트워크 이벤트(keep-alive 재사용 경합, LB/proxy idle timeout, 원격 조기 종료)로 코드 로직 결함이 아니다. 근본 원인 자체는 transient infra 이므로 noise 성격이지만, uploadDirectoryByCredential 의 per-file catch 블록(aws-s3.manager.ts:138-140)이 실패를 logger.error 로만 기록하고 re-throw 하지 않아 실패한 파일이 재시도·재전파 없이 조용히 유실되는 resilience gap 이 함께 존재한다. 상위 호출부(uploadResizedImage, pano-postprocessor-service.ts:167-176)의 try/catch 는 이 삼켜진 에러를 볼 수 없으므로 해당 pano 는 Error 상태로 마킹되지 않고 정상 완료 처리된다.
Technical Analysis#
Code Path#
- Entry point:
packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:169— resize 후uploadResizedImage(cpPano)호출 packages/cupix-pano-postprocessor/src/work/resize-work.ts:138-143— tesla 에서 tile 업로드 credential 발급 →awsS3Manager.uploadDirectoryByCredential→checkTileUploading- Failure point:
packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-140—s3.upload(...).promise()가TimeoutError(ECONNRESET) throw,catch가 로그만 남김
호출부는 3단계를 순차 await 하며 하나의 try/catch 로 감싼다.
try {
await this.resizeWork.resizePano(cpPano);
await this.resizeWork.uploadResizedImage(cpPano);
await this.resizeWork.checkStitched(cpPano);
} catch (error) {
logger.error('PanoPostprocessorService::run | resize pano id:%d | error %s', cpPano.panoId, stringifyError(error));
await this.panoPostprocessorManager.updatePanoState(cpPano.panoId!, TESLA.UpdatePanoRequest.StateEnum.Error);
erroredPanoIds.add(cpPano.panoId!);
}
uploadResizedImage 는 credential 발급 → 업로드 → checkTileUploading 를 순차 호출한다.
uploadResizedImage = async (cpPano: CPPano): Promise<void> => {
if (!cpPano.panoId) return;
const credentials = await this.cupixApi.pano.createTileUploadCredentials(cpPano.panoId);
await this.awsS3Manager.uploadDirectoryByCredential({ credential: credentials, directory: cpPano.resizeDir!});
await this.cupixApi.pano.checkTileUploading(cpPano.panoId);
};
실제 업로드는 credential 로 생성한 S3 client 로 파일을 10개씩 병렬 PUT 한다. S3 client 생성 시 maxRetries 나 httpOptions.timeout 을 지정하지 않으므로 SDK v2 기본값이 적용된다(기본 maxRetries 로 3회 재시도 → attempts:3 일치).
await s3.upload({
Bucket: credential.bucket_name!,
Key: bucketKey,
Body: fs.createReadStream(filePath)
}).promise();
logger.debug('AwsS3Manager::uploadDirectoryByCredential | uploaded file - %s', filePath);
uploadedFileCount++;
} catch (error) {
logger.error('AwsS3Manager::uploadDirectoryByCredential | upload failed - %s', JSON.stringify(error));
}
- 기대 동작: 일시적 네트워크 오류로 업로드가 최종 실패하면, 최소한 해당 pano 를 실패로 표시하거나(상위 try/catch 로 전파) 재시도하여 tile 누락을 방지해야 한다.
- 실제 동작:
catch가 에러를 로그만 남기고 삼켜(re-throw없음)uploadChunk가 정상 resolve 된다.uploadDirectoryByCredential는 예외 없이 반환하고, 이어서checkTileUploading이 실행되어 pano 가 정상 완료로 처리된다. 상위 try/catch 는 이 에러를 보지 못하므로updatePanoState(..., Error)도 호출되지 않는다. 결과적으로 일부 tile 파일이 유실된 채 성공으로 마감될 수 있다.
이 catch-and-log-only 패턴은 pano postprocessor 최초 구현(commit 36ecf93ff, TSLA-9784, 2025-07-04)부터 존재했으며 최근 회귀가 아니다. packages/cupix-pano-postprocessor/src 내에 ECONNRESET/retry/maxRetries 관련 재시도 헬퍼는 없다(grep 0건).
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-pano-postprocessor-instance "uploadDirectoryByCredential"
now-14d 범위에서 5건 확인. 클러스터 대표 에러 2건(2026-08-05 01:11:38 KST)은 ECONNRESET/TimeoutError, attempts:3:
{
"timestamp": "2026-08-05 01:11:38",
"status": "error",
"message": "AwsS3Manager::uploadDirectoryByCredential | upload failed - {\"code\":\"ECONNRESET\",\"name\":\"TimeoutError\",\"$metadata\":{\"attempts\":3,\"totalRetryDelay\":229}}"
}
동일 함수의 다른 실패 변형(2026-07-29, httpStatusCode:499 = client 연결 조기 종료, attempts:1)도 존재하나 error_type/원인이 다르다 — 같은 fingerprint 로 묶여있지 않으며 별개 transient 변형이다:
{
"timestamp": "2026-07-29 01:40:26",
"status": "error",
"message": "AwsS3Manager::uploadDirectoryByCredential | upload failed - {\"$fault\":\"client\",\"$metadata\":{\"httpStatusCode\":499,\"requestId\":\"18C681A0192AE2DF\",\"...\":\"...\",\"attempts\":1,\"totalRetryDelay\":0},\"name\":\"Unknown\",\"message\":\"UnknownError\"}"
}
전체 service:cupixworks-pano-postprocessor-instance status:error 검색 시 대부분은 무관한 MaskWork::maskPanos python UserWarning/RuntimeWarning 이 error 로 계측된 별개 noise 이다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3 PUT 중 transient TCP reset(ECONNRESET)로 SDK 기본 재시도 3회 소진 후 TimeoutError 발생, per-file catch 가 삼킴 → 일시적 network + resilience gap |
로그 code:ECONNRESET name:TimeoutError attempts:3 totalRetryDelay:229 (2026-08-05 01:11:38 KST). 코드 aws-s3.manager.ts:130-140 s3.upload().promise() throw → catch 로그만, re-throw 없음. client 에 maxRetries/timeout override 없음 → SDK 기본(3회)과 attempts:3 일치 |
— | Confirmed |
| H2 | tesla 발급 credential 만료/권한 오류로 인한 인증 실패 | — | 에러가 ECONNRESET/TimeoutError(transport-level)이지 403/SignatureDoesNotMatch/ExpiredToken(auth) 아님. $metadata 에 httpStatusCode 없음(연결 자체가 reset) |
Rejected |
| H3 | agent/tesla 코드 로직 결함(잘못된 bucket key, nil path 등) | — | bucketKey 는 credential.basepath + relative path 로 정상 구성, 에러가 결정론적이지 않고 5건/14d 저빈도 산발 transient. 코드 경로 자체는 정상 |
Rejected |
| H4 | 2026-07-29 499 UnknownError 와 동일 근본 원인(같은 클러스터) |
같은 함수 uploadDirectoryByCredential 에서 발생 |
error_type 다름(TimeoutError/ECONNRESET vs Unknown/499 client), attempts 다름(3 vs 1). fingerprint 상 별개 변형 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 근본 원인(transient
ECONNRESET)은 코드 결함이 아니므로 즉시 수정할 서버 버그는 없다. 저빈도(5건/14d) 일시적 네트워크 이벤트로 noise 로 분류 가능하다. 필요 시 error-sweeper 에서 IGNORE 처리.
단기 개선 (1주 이내)#
packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:138-140— per-file catch 가 실패를 삼키는 resilience gap 개선. 방향(택1): (a) 최종 실패 시 catch 에서 re-throw 하여 상위uploadResizedImage/pano-postprocessor-service.ts:171try/catch 가 pano 를Error상태로 마킹하도록 전파, 또는 (b) 실패 파일에 대해 짧은 backoff 재시도를 추가한 뒤에도 실패하면 전파. 조용한 tile 유실을 방지하는 것이 목적. (구현 코드는 본 보고서 범위 밖 — 방향만 제시)uploadedFileCount와filesToUpload.length를 비교하여 부분 실패 시 명시적으로 실패를 알리는 검증 추가 검토(aws-s3.manager.ts:150).
장기 개선 (재발 방지)#
- pano postprocessor 의 S3 client 에 명시적 timeout/재시도 정책(예:
httpOptions.timeout,maxRetries) 및 idempotent 업로드에 한정한 backoff 재시도 표준화. - agent 전반의 "catch → log-only" 패턴을 점검하여 최종 실패가 상위로 전파되거나 상태로 기록되도록 정합성 확보.
Monitoring#
업로드 실패 발생 추이:
service:cupixworks-pano-postprocessor-instance status:error "uploadDirectoryByCredential"
ECONNRESET/TimeoutError 변형만:
service:cupixworks-pano-postprocessor-instance status:error "uploadDirectoryByCredential" "ECONNRESET"
pano postprocessor 전체 에러 추이(무관 MaskWork noise 포함 주의):
service:cupixworks-pano-postprocessor-instance status:error
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (단기 개선 = catch re-throw/재시도 한정 변경)