ES /docs

AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/7/114/105.png

RCA: AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/.../tile/...png

Overview#

What Happened#

2026-06-25 새벽(KST) cupixworks-any-floorplan-agentAwsS3Manager::uploadDirectoryToS3 가 floorplan tile 디렉토리를 S3 로 업로드하는 도중 개별 tile 파일별로 failed: <path> error 로그를 폭발적으로 출력했다. 약 6시간 동안 266,828건 발생했으며 실패한 path 만 남고 실제 원인 (예: HTTP 503, ECONNRESET, credentials expired) 은 로그에 누락되어 있다.

Quick Facts#

Field Value
exception.class (없음 — catch (error) 로 삼킨 뒤 message-only 로 기록)
exception.message AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/{floorplanId}/tile/{lod}/{x}/{y}.png
top_frame packages/base/src/manager/aws-s3.manager.ts:156
runtime Node.js (winston logger, AWS SDK v2)
env production, regions: us-west-2

Affected Teams#

Team / Domain Error Count Impact
chrischoi / floorplan-agent 266,828 tile 업로드 단계의 부분 실패. 일부 floorplan은 같은 workspace id 로 SQS 재처리되어 결국 state: done 까지 도달 (FloorplanService::run | floorplan state: done, resource_state: uploaded 로그). 그러나 재시도가 6시간 지속되어 SQS 큐 적체 및 비용/로그량 증가.

Timeline#

  1. 2026-06-25 01:46 KSTfirst_seen. 첫 AwsS3Manager::uploadDirectoryToS3 \| failed 발생 (workspace 89282 외 다수).
  2. 2026-06-25 01:49–01:54 KST — workspace 89282 가 동일하게 SQS 로 재실행됨 (BaseService::runByMessage \| id: 89282 두 번 이상). tile 업로드는 매번 같은 경로에서 실패.
  3. 2026-06-25 01:52 KST — 동일 시간대에 FloorplanService::uploadTile - createTileUploadCredentials error - HttpError: HTTP request failed, HTTP request failed error 로그 동시 발생.
  4. 2026-06-25 07:49 KSTlast_seen. workspace 89292 에서 동일 패턴으로 LOD 7 tile 의 95~99 좌표 PNG 가 실패.

Error Log#

Datadog Logs

text
AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/7/114/105.png

Impact#

  • Service: cupixworks-any-floorplan-agent
  • Team: chrischoi
  • 발생 횟수: 266,828
  • 최초 발생: 2026-06-25 01:46 KST
  • 최근 발생: 2026-06-25 07:49 KST

Root Cause Summary#

AwsS3Manager.uploadDirectoryToS3 는 tile 디렉토리의 모든 파일을 Promise.all chunk 로 병렬 업로드하며, 각 파일별로 try/catch 후 logger.error 만 호출하고 throw 하지 않는다 (packages/base/src/manager/aws-s3.manager.ts:155-157). 따라서 S3 측 일시 오류 (token 만료, 503/throttle, ECONNRESET 등) 가 발생하면 한 batch 안의 여러 파일이 동시에 실패하고, 그 모든 실패가 path 단위로 error 로그를 찍는다. 한 floorplan (LOD 6, 16384×16384) 의 tile 디렉토리는 수천~수만 개 PNG 파일을 포함하므로, 한 번의 transient 장애가 곧바로 만 단위 error 로그로 증폭된다. 또한 호출 측인 FloorplanService.uploadTile 은 이 함수의 정상 종료 후 checkTileUploading 을 호출하므로 부분 실패가 상위로 신호되지 않고 다음 단계로 넘어간다. 즉 본 cluster 는 부분 실패 처리와 에러 증폭 로깅이 결합된 결과이며, 같은 시간대의 createTileUploadCredentials error - HttpError: HTTP request failed (warn/error, 01:52 KST 등) 로그가 동일 윈도우에서 발생한 점이 transient downstream 원인을 시사한다.

Technical Analysis#

Code Path#

  • Entry point: packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:309uploadTile
  • credentials 발급: floorplan-service.ts:323cupixApi.floorplan.createTileUploadCredentials
  • 디렉토리 업로드 진입: floorplan-service.ts:350awsS3Manager.uploadDirectoryToS3({...})
  • 파일 단위 실패 지점: packages/base/src/manager/aws-s3.manager.ts:155-157 — per-file catch 에서 logger.error 만 호출
packages/base/src/manager/aws-s3.manager.ts:130-159typescript
const uploadChunk = async (filesChunk: string[]) => {
    await Promise.all(filesChunk.map(async (filePath) => {
        try {
            await this.checkToken();
            if (this.s3 == undefined) {
                logger.error('AwsS3Manager::uploadDirectoryToS3 | s3 is not initialized');
                return;
            }

            let bucketKey = path.posix.join(params.bucketKeyPath, path.relative(targetDirectoryPath, filePath));
            if (params.excludeFileExtension && params.excludeFileExtension == true) bucketKey = bucketKey.replace(path.extname(bucketKey), '');
            logger.silly('AwsS3Manager::uploadDirectoryToS3 | uploading %s to %s', filePath, bucketKey);

            const contentEncoding = filePath.endsWith('pbz') ? 'gzip' : undefined;
            const fileStream = fs.createReadStream(filePath);
            await this.s3.upload({
                Bucket: params.bucketName,
                Key: bucketKey,
                ACL: params.acl,
                Body: fileStream,
                ContentEncoding: contentEncoding
            }).promise();

            uploadedFileCount++;
            logger.silly('AwsS3Manager::uploadDirectoryToS3 | done: %s, uploaded: %d/%d', bucketKey, uploadedFileCount, filesToUpload.length);
        } catch (error) {
            logger.error('AwsS3Manager::uploadDirectoryToS3 | failed: %s', filePath, error);
        }
    }));
};
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:341-357typescript
const awsS3Manager = new AwsS3Manager(s3Credentials.bucket_region);
awsS3Manager.setApiCreateCredentials(() => this.cupixApi.floorplan.createTileUploadCredentials(cpFloorplan.id));
awsS3Manager.setCredentials({
    accessKeyId: s3Credentials.aws_access_key_id,
    secretAccessKey: s3Credentials.aws_secret_access_key,
    sessionToken: s3Credentials.aws_session_token,
    expires_at: s3Credentials.expires_at,
    endpoint: s3Credentials.endpoint
});
await awsS3Manager.uploadDirectoryToS3({
    bucketName: s3Credentials.bucket_name,
    bucketKeyPath: s3Credentials.basepath,
    targetDirectoryPath: resultDir,
    acl: s3Credentials.acl,
    excludeFileExtension: true
});
await this.cupixApi.floorplan.checkTileUploading(cpFloorplan.id);

기대 동작 vs 실제 동작

  • 기대: 일부 파일이 transient 사유로 실패하면 retry/abort 로직을 통해 상위에 에러 신호 전달, 한 batch 의 실패는 묶어서 단일 진단성 로그로 남긴다.
  • 실제: 각 파일을 독립적으로 catch 하여 logger.error 만 호출하고 정상 종료. 한 batch 의 transient 장애가 batch 의 모든 파일 만큼 곱해진 error 로그를 만들고, 상위는 실패를 인지하지 못한 채 checkTileUploading 으로 진행.
  • 추가 문제: logger.error('...failed: %s', filePath, error) 는 format 문자열에 placeholder 가 1개뿐이라 error 인자가 메시지에 포함되지 않는다. winston splaterror 를 메타 필드로 보존하긴 하지만 (packages/utils/src/cplogger.ts:21-34), Datadog 에 수집된 message 에는 실패한 path 만 남고 어떤 sdk error (CredentialsError, NetworkingError, NoSuchKey 등) 인지 식별 불가.

Log Evidence#

Datadog 쿼리 (cluster file 의 ## Datadog 섹션과 동일):

text
service:cupixworks-any-floorplan-agent status:error @environment:production "AwsS3Manager::uploadDirectoryToS3"

대표 에러 메시지 (동일한 burst):

text
2026-06-25 01:49:28 [error] AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/7/99/98.png
2026-06-25 01:49:28 [error] AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/blank.png
2026-06-25 01:49:28 [error] AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/7/99/95.png
2026-06-25 01:49:28 [error] AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/7/99/97.png

동일 시간대 transient downstream 신호:

text
2026-06-25 01:52:32 [error] HTTP request failed
2026-06-25 01:54:31 [error] FloorplanService::uploadTile - createTileUploadCredentials error - HttpError: HTTP request failed
2026-06-25 01:56:51 [error] FloorplanService::uploadTile - createTileUploadCredentials error - HttpError: HTTP request failed
2026-06-25 00:01:13 [warn]  read ECONNRESET
2026-06-25 00:01:13 [warn]  CupixAuth::handleError | Undefined response: {"stack":"Error: read ECONNRESET ...","message":"read ECONNRESET","errno":-104,"code":"ECONNRESET","syscall":"read"}

재처리 패턴 (같은 workspace 가 SQS 메시지로 두 번 이상 처리됨):

text
2026-06-25 01:49:28 [error] AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/89282/tile/...
2026-06-25 01:49:29 [info]  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/89282
2026-06-25 01:53:25 [info]  BaseService::runByMessage | id: 89282
2026-06-25 01:54:31 [info]  FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89282/tile`
2026-06-25 01:54:31 [info]  BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/89282

성공한 floorplan 예시 (부분 실패가 상위로 신호되지 않아 done 상태로 진행):

text
2026-06-25 03:55:32 [info] FloorplanService::run | floorplan state: resource_uploaded, resource_state: uploaded
2026-06-25 03:55:33 [info] FloorplanService::run | floorplan state: done, resource_state: uploaded
2026-06-25 03:59:33 [info] FloorplanService::run | floorplan state: done, resource_state: uploaded

logger 의 splat 처리 — Error 객체는 메타 필드로 옮겨지지만 message string 에는 포함되지 않음:

packages/utils/src/cplogger.ts:19-37typescript
// example: catch (err) { logger.error("Failed to process:", err); }
// example: catch (err) { logger.error("message %s", err); }
const splatKey = Symbol.for('splat');
const splat = info[splatKey];
if (Array.isArray(splat)) {
    for (let i = 0; i < splat.length; i++) {
        const v = splat[i];
        if (v instanceof Error) {
            splat[i] = {
                name: v.name,
                message: v.message,
                stack: v.stack,
            };
        }
    }
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 tile 파일별 transient S3/네트워크 실패가 Promise.all chunk 안에서 batch 단위로 발생하고, per-file catch 가 throw 없이 logger.error 만 호출하여 batch 의 모든 파일이 error 로그로 증폭된다 동일 timestamp(초 단위)에 같은 workspace 의 수십~수백 path 가 failed: 로 동시에 찍힘 (01:49:28 burst). 같은 시간대 createTileUploadCredentials error - HttpError: HTTP request failed, ECONNRESET 등 downstream transient 신호. aws-s3.manager.ts:155-157 에서 throw 없이 logger.error 만 호출. 일부 floorplan 은 state: done 까지 진행되어 상위에 부분 실패가 전파되지 않음 Confirmed
H2 로컬 파일 시스템에서 tile 파일이 누락되어 fs.createReadStream 단계에서 실패 path 가 /tmp/workspace/{id}/tile/{lod}/{x}/{y}.png 로 LOD 디렉토리 패턴과 일치하며 tiling 직후 같은 workspace 에서 발생 tiling 직전 FloorplanService::tileFloorplan | tiling done and now save to /tmp/workspace/{id}/tile 가 같은 workspace 에서 성공 로그를 남김. 동일 path 패턴이 여러 workspace 에서 같은 좌표(7/99/9x)에서 반복되어 파일 누락보다 batch 동시 실패에 가까움 Rejected
H3 floorplan 외부 의존성 (S3 me-central-1 등) 전체 outage 동시 시간대 HTTP request failed 가 있음 status-board for-cluster 결과 active: null, recent: []. 영향 region 은 us-west-2 단일, 같은 시간대 다른 floorplan 은 done 상태로 성공 Rejected
H4 S3 credentials 만료/갱신 race (checkToken 이 Promise.all 안에서 동시에 갱신 시도) checkToken 이 lock 없이 _createCredentials() 호출, Promise.all 내부에서 동시 진입 가능 (aws-s3.manager.ts:61-80) 직접 증거 없음 — sdk error 메시지가 로그에 누락되어 CredentialsError 확정 불가. 별도 hypothesis 로 검증 필요 Inconclusive

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: packages/base/src/manager/aws-s3.manager.ts:155-157
  • 방향: logger.error('...failed: %s', filePath, error) 의 format 문자열에 error 포맷 placeholder 를 추가하거나 ('...failed: %s, error: %j') Error 객체를 logger.error(error, 'failed: %s', filePath) 형태로 전달해 winston splat/errorSafeFormat 가 stack/message 를 보존하도록 한다. memory.md 의 "fix logger pattern" 규칙에 따라 JSON.stringify(error) 수동 직렬화 대신 Error 객체를 그대로 넘기는 방식을 선호. 우선 진단을 살리는 것이 본 cluster 의 근본 분류 (network vs auth vs S3 API) 에 필수.

단기 개선 (1주 이내)#

  • aws-s3.manager.ts:130-159 에 retry-with-backoff 도입: per-file Promise.all 안에서 transient 분류 (5xx, throttle, network) 만 N회 재시도. AWS SDK v2 maxRetries: 5 가 이미 설정되어 있으나 (aws-s3.manager.ts:19-23) s3.upload 의 retry 는 sdk 가 처리하더라도 fs stream 단계 실패는 별도 처리 필요.
  • per-file catch 에서 batch 단위 실패 집계 후 단일 요약 로그 + 임계치 초과 시 uploadDirectoryToS3 자체를 throw 하여 상위 (FloorplanService.uploadTile) 가 checkTileUploading 호출을 막을 수 있도록 한다. 현재는 부분 실패가 신호되지 않아 tile 누락 floorplan 이 done 으로 표시될 위험이 있음.
  • floorplan-service.ts:357 직전에 업로드 성공/실패 카운트를 받아 임계치 미달 시 에러 throw → SQS 재처리 유도.

장기 개선 (재발 방지)#

  • cupix-tesla-floorplan-agent (및 동일 패턴을 가진 cupix-tesla-potree-agent, cupix-tesla-bim-revision-agent, cupix-tesla-forge-agent, cupix-tesla-voxel-agent, siteinsights/base-postprocessor-runner) 에서 AwsS3Manager.uploadDirectoryToS3 를 사용하는 호출자 모두 부분 실패 처리가 동일한 위험을 가짐. 공용 manager 측에서 retry + abort 정책을 표준화.
  • checkToken 의 동시 갱신 race 가능성을 in-flight promise singleton 으로 보호 (H4 검증 후 적용).

Monitoring#

업로드 실패율 추세 (timeseries widget — count_by_* facets):

text
logs("service:cupixworks-any-floorplan-agent status:error \"AwsS3Manager::uploadDirectoryToS3 | failed\"").index("*").rollup("count").by("environment")

같은 burst 에서 영향받은 floorplan(workspace) 수:

text
logs("service:cupixworks-any-floorplan-agent status:error \"AwsS3Manager::uploadDirectoryToS3 | failed\"").index("*").rollup("cardinality", "@workspace_id")

credentials 발급 실패 추세 (downstream signal):

text
logs("service:cupixworks-any-floorplan-agent status:error \"createTileUploadCredentials error\"").index("*").rollup("count")

(현재 @workspace_id tag 가 없다면 fix 시 message 에 workspace_id 를 추가하고 facet 등록 권장)

알림 후보:

  • 같은 floorplan id 에 대해 5분 내 동일 path 가 1,000개 이상 error 로그 → tile 업로드 partial failure 의심.
  • FloorplanService::uploadTile - createTileUploadCredentials error warn/error 1분당 5회 이상 → cupixworks-api credentials endpoint 장애 알림.

Risk Assessment#

  • Risk level: medium — 직접 데이터 손실은 미관찰이나 floorplan 의 부분 tile 결손이 done 상태로 진행될 수 있어 사용자 보기 화질/누락 가능성 있음. 로그량/비용 증폭이 즉시 영향.
  • 예상 복잡도: standard — logger 포맷 1줄 수정은 trivial, retry/abort 도입은 standard.