ES /docs

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#

  1. 2026-08-05 01:11:38 KSTuploadDirectoryByCredential 의 S3 PUT 이 ECONNRESET 로 실패, SDK 재시도 3회 소진 후 TimeoutError. 동일 초에 2건 발생 (병렬 chunk 업로드).
  2. 2026-08-05 — error-sweeper collector 가 클러스터 감지 (fingerprint 7c69cd1478524f944b092e9e6cec6452).

Error Log#

Datadog Logs

text
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.uploadDirectoryByCredentialcheckTileUploading
  • Failure point: packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-140s3.upload(...).promise()TimeoutError(ECONNRESET) throw, catch 가 로그만 남김

호출부는 3단계를 순차 await 하며 하나의 try/catch 로 감싼다.

packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:167-176typescript
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 를 순차 호출한다.

packages/cupix-pano-postprocessor/src/work/resize-work.ts:138-143typescript
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 생성 시 maxRetrieshttpOptions.timeout 을 지정하지 않으므로 SDK v2 기본값이 적용된다(기본 maxRetries 로 3회 재시도 → attempts:3 일치).

packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:130-140typescript
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 쿼리 (재현용):

text
service:cupixworks-pano-postprocessor-instance "uploadDirectoryByCredential"

now-14d 범위에서 5건 확인. 클러스터 대표 에러 2건(2026-08-05 01:11:38 KST)은 ECONNRESET/TimeoutError, attempts:3:

json
{
  "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 변형이다:

json
{
  "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 등) bucketKeycredential.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:171 try/catch 가 pano 를 Error 상태로 마킹하도록 전파, 또는 (b) 실패 파일에 대해 짧은 backoff 재시도를 추가한 뒤에도 실패하면 전파. 조용한 tile 유실을 방지하는 것이 목적. (구현 코드는 본 보고서 범위 밖 — 방향만 제시)
  • uploadedFileCountfilesToUpload.length 를 비교하여 부분 실패 시 명시적으로 실패를 알리는 검증 추가 검토(aws-s3.manager.ts:150).

장기 개선 (재발 방지)#

  • pano postprocessor 의 S3 client 에 명시적 timeout/재시도 정책(예: httpOptions.timeout, maxRetries) 및 idempotent 업로드에 한정한 backoff 재시도 표준화.
  • agent 전반의 "catch → log-only" 패턴을 점검하여 최종 실패가 상위로 전파되거나 상태로 기록되도록 정합성 확보.

Monitoring#

업로드 실패 발생 추이:

text
service:cupixworks-pano-postprocessor-instance status:error "uploadDirectoryByCredential"

ECONNRESET/TimeoutError 변형만:

text
service:cupixworks-pano-postprocessor-instance status:error "uploadDirectoryByCredential" "ECONNRESET"

pano postprocessor 전체 에러 추이(무관 MaskWork noise 포함 주의):

text
service:cupixworks-pano-postprocessor-instance status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (단기 개선 = catch re-throw/재시도 한정 변경)