ES /docs

AwsS3Manager::uploadDirectoryByCredential | upload failed - {"$fault":"server","$metadata":{"httpSta

RCA: AwsS3Manager::uploadDirectoryByCredential upload failed (S3 InternalError 500)

Overview#

What Happened#

2026-08-05 17:14 KST, pano postprocessing agent(cupixworks-pano-postprocessor-instance)이 리사이즈된 pano 타일을 S3에 업로드하다가 S3가 InternalError (HTTP 500, We encountered an internal error. Please try again.)를 반환했다. AWS SDK가 3회 재시도한 뒤에도 실패했고, 이 에러는 파일 단위 catch 블록에서 로그만 남긴 채 삼켜졌다. 단발성 발생(1건)이며, 같은 fingerprint에 묶인 유사 transport 실패가 14일간 총 6건 있었다.

Quick Facts#

Field Value
exception.class InternalError (S3 server fault)
exception.message We encountered an internal error. Please try again.
top_frame applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:139
runtime Node.js agent, aws-sdk S3 client s3.upload().promise()
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
gad (pano postprocessor) 1 리사이즈 타일 1개 이상이 S3에 업로드되지 못했으나, 에러가 삼켜져 pano는 정상 처리로 표시될 수 있음

Timeline#

  1. 2026-08-05 17:14:19 KSTAwsS3Manager::uploadDirectoryByCredential에서 S3 InternalError 500 발생, SDK 3회 재시도 후 실패, logger.error로 기록 (first_seen = last_seen).
  2. 2026-08-05 (RCA) — error-sweeper가 클러스터 감지, RCA 수행.

Error Log#

Datadog Logs

text
AwsS3Manager::uploadDirectoryByCredential | upload failed - {"$fault":"server","$metadata":{"httpStatusCode":500,"requestId":"9C6GEPQYAQ5X0AY5","extendedRequestId":"VKULUAs/N3p2eWYqiw6Bx+YGppfEg7hqHaIzxDtKHOjHn3/Xf4urZNeUUWwyttU1eq0PxpgN+1o=","attempts":3,"totalRetryDelay":95},"name":"InternalError","Code":"InternalError","RequestId":"9C6GEPQYAQ5X0AY5","HostId":"VKULUAs/N3p2eWYqiw6Bx+YGppfEg7hqHaIzxDtKHOjHn3/Xf4urZNeUUWwyttU1eq0PxpgN+1o=","message":"We encountered an internal error. Please try again."}

Impact#

  • Service: cupixworks-pano-postprocessor-instance
  • Team: gad
  • 발생 횟수: 1
  • 최초 발생: 2026-08-05 17:14 KST
  • 최근 발생: 2026-08-05 17:14 KST

Root Cause Summary#

S3가 반환한 InternalError (HTTP 500)는 AWS 측 서버의 일시적 내부 오류다. $fault: "server""attempts": 3이 이를 확증한다. 즉 AWS SDK가 이미 자체 재시도(3회)를 소진한 뒤에도 S3 서버가 계속 500을 반환한 transient 이벤트로, pano postprocessor 코드의 결함은 아니다. 다만 코드 측 resilience gap이 하나 있다. uploadDirectoryByCredential의 파일 단위 catch 블록(aws-s3.manager.ts:138-140)이 실패를 로그만 남기고 re-throw하지 않아서, 개별 파일 업로드가 실패해도 함수는 정상 resolve된다. 그 결과 상위 호출부 uploadResizedImage(resize-work.ts:141-142)가 곧바로 checkTileUploading을 호출해 타일 업로드를 완료로 표시하고, pano-postprocessor-service.ts:171의 상위 catch도 트리거되지 않아 pano가 Error 상태로 기록되지 않는다. 근본 트리거는 외부 의존성(S3)의 일시 장애이나, 이 삼킴 로직이 부분 실패를 조용히 감춘다.

Technical Analysis#

Code Path#

  • Entry point: applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-178 (resize task 루프)
  • resize-work.ts:138-143 uploadResizedImage가 credential을 발급받아 디렉터리를 업로드하고 타일 업로드 완료를 통지
  • Failure point: aws-s3.manager.ts:130-140 s3.upload().promise()가 S3 InternalError 500으로 실패, catch에서 로그만 남김

resize task는 세 단계를 순차 실행한다. 업로드는 uploadResizedImage 안에서 일어난다.

applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:165-176typescript
const resizeTasks = cpPanos.map((cpPano) => {
	return PARALLEL_TASK_LIMIT(async () => {
		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 발급 → 디렉터리 업로드 → 타일 업로드 완료 통지 순으로 진행한다.

applications/agents/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);
};

실제 업로드는 파일 단위 Promise.all 청크로 수행되며, 여기서 S3 InternalError가 발생한다. catch는 로그만 남기고 예외를 다시 던지지 않는다.

applications/agents/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를 Error 상태로 표시하거나 최소한 부분 실패를 상위로 전파해야 한다.

실제 동작: catch가 예외를 삼켜 uploadDirectoryByCredential가 정상 resolve → uploadResizedImagecheckTileUploading으로 완료 통지 → pano-postprocessor-service.ts:171의 상위 catch 미발동 → pano가 성공으로 간주됨. uploadedFileCount는 실패 파일만큼 증가하지 않지만 이 값을 검증하는 로직이 없다.

Log Evidence#

Datadog 쿼리 (재현):

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

이 클러스터의 대표 로그 (2026-08-05 17:14:19 KST). $fault: "server"attempts: 3이 S3 서버 측 일시 장애 + SDK 재시도 소진을 확증한다.

json
{
  "timestamp": "2026-08-05 17:14:19",
  "status": "error",
  "message": "AwsS3Manager::uploadDirectoryByCredential | upload failed - {\"$fault\":\"server\",\"$metadata\":{\"httpStatusCode\":500,\"requestId\":\"9C6GEPQYAQ5X0AY5\",\"attempts\":3,\"totalRetryDelay\":95},\"name\":\"InternalError\",\"Code\":\"InternalError\",\"message\":\"We encountered an internal error. Please try again.\"}"
}

같은 fingerprint(AwsS3Manager::uploadDirectoryByCredential | upload failed prefix로 그룹화)에 묶인 14일간 6건은 서로 다른 3종의 transport 실패다. 모두 attempts: 3으로 SDK 재시도를 소진한 후 표면화됐다. 이는 특정 코드 경로 결함이 아니라 S3/네트워크 transport의 일시 불안정을 가리킨다.

text
2026-08-05 17:14:19  InternalError httpStatusCode:500 attempts:3   (server fault)
2026-08-05 01:11:38  TimeoutError  code:ECONNRESET   attempts:3   (transport reset)
2026-08-05 01:11:38  TimeoutError  code:ECONNRESET   attempts:3   (transport reset)
2026-07-29 01:40:26  Unknown       httpStatusCode:499 attempts:1   (client close)
2026-07-29 01:40:08  Unknown       httpStatusCode:499 attempts:1   (client close)
2026-07-29 01:40:02  Unknown       httpStatusCode:499 attempts:1   (client close)

status-board 조회 결과 이 scope에 진행 중이거나 최근 해소된 인시던트는 없다.

json
{ "scope": "svc:cupixworks-pano-postprocessor-instance::unknown", "active": null, "recent": [] }

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 서버 측 일시 장애(InternalError 500)가 트리거이고, 코드는 파일 단위 실패를 삼켜 부분 실패를 은폐 로그의 $fault:"server", httpStatusCode:500, attempts:3, message We encountered an internal error. Please try again.; catch가 re-throw 없이 로그만 남김 (aws-s3.manager.ts:138-140) Confirmed
H2 pano postprocessor 코드 로직 결함(잘못된 key/bucket/credential)으로 인한 결정론적 실패 에러가 $fault:"server" 500이고 credential/bucket 구성 오류라면 4xx(403/400)로 반환됨. 실제 별건 credential 에러는 별도 fingerprint(403 HttpError on createTileUploadCredentials, 2026-08-04)로 분리됨 Rejected
H3 특정 pano/파일에 대한 반복 재진입 실패(broad 회귀) 14일 6건 6건이 3종 transport 에러(500/ECONNRESET/499)로 분산, 서로 다른 requestId, 저빈도 산발 → 일시적 transport 이벤트 Rejected
H4 SDK 재시도 미설정으로 인한 즉시 실패 로그의 attempts:3이 SDK 기본 재시도(3회)가 이미 동작했음을 확증; 재시도 소진 후에도 S3가 계속 500 반환 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

코드 변경 불필요. 근본 트리거는 S3의 일시적 InternalError 500이며, AWS SDK가 이미 3회 재시도를 수행했다. 단발 transient 이벤트로, 해당 pano의 재처리(재큐잉)로 자연 복구된다.

단기 개선 (1주 이내)#

  • aws-s3.manager.ts:138-140의 파일 단위 catch가 실패를 삼키는 문제를 개선한다. 실패한 파일을 집계해 청크 종료 후 하나라도 실패했으면 uploadDirectoryByCredential가 예외를 re-throw하도록 방향을 잡는다. 그러면 uploadResizedImagecheckTileUploading으로 완료 통지하기 전에 상위 pano-postprocessor-service.ts:171 catch가 pano를 Error 상태로 표시할 수 있다. 이는 부분 업로드 실패로 타일이 누락된 pano가 조용히 성공으로 기록되는 것을 막는다.
  • 로그 직렬화 개선: catchJSON.stringify(error)를 사용하는데, Error의 non-enumerable 속성이 손실될 수 있다. 이 패키지의 cplogger errorSafeFormat 규약(memory: pano-postprocessor 서비스 노트)에 맞춰 error를 splat 인자로 직접 전달하는 방향을 검토한다. 이번 사례는 SDK가 plain object 형태의 에러를 던져 stringify가 동작했지만, 다른 경로에서는 {}로 직렬화될 위험이 있다.

장기 개선 (재발 방지)#

  • 타일 업로드의 무결성 검증을 추가한다. uploadedFileCountfilesToUpload.length와 비교하여 불일치 시 실패로 처리하거나, checkTileUploading 이전에 업로드 완료 개수를 tesla API로 검증한다.
  • transient S3/transport 실패(500/ECONNRESET/499)에 대해 파일 단위 재업로드(idempotent PUT이므로 안전) 백오프 재시도를 SDK 재시도 위에 얇게 추가하는 방안을 검토한다.

Monitoring#

업로드 실패 발생 추이:

text
service:cupixworks-pano-postprocessor-instance status:error "AwsS3Manager::uploadDirectoryByCredential | upload failed"

pano resize 단계 전체 실패 추이 (상위 catch):

text
service:cupixworks-pano-postprocessor-instance status:error "PanoPostprocessorService::run | resize pano"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard (부분 실패 전파 로직 추가 시)