AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin
RCA: AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin
Overview#
What Happened#
2026-06-24 15:06 KST에 cupixworks-any-potree-agent (eu-central-1) 가 pointcloud id 125328 의 Potree 변환 결과를 S3 에 업로드하는 도중 octree.bin 파일 1개의 업로드가 실패했다. 에러는 AwsS3Manager::uploadDirectoryToS3 의 per-file catch 블록에서 swallow 되었기 때문에, agent 는 그대로 후속 단계(checkUploading, cleanUpAnythingRelatedModel)를 계속 진행했고 실패한 파일의 원인 정보는 로그에 남지 않았다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | (없음 — caught error 가 로그 메시지에 포함되지 않음) |
| exception.message | AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin |
| top_frame | applications/agents/packages/base/src/manager/aws-s3.manager.ts:156 |
| env | production, eu-central-1 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| crcc-sama / Potree pipeline | 1 | Pointcloud 125328 의 Potree octree 데이터가 S3 에 부분적으로 누락된 채 업로드 완료로 처리되었을 가능성. 뷰어에서 해당 노드만 로드 실패 가능. |
Timeline#
- 2026-06-24 15:04 KST —
PotreeService::runByMessage | id: 125328— pointcloud 125328 처리 시작 - 2026-06-24 15:06:11 KST —
AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin—octree.bin업로드 중 예외 발생, per-filecatch가 삼킴 - 2026-06-24 15:06:12 KST —
PotreeService::cleanUpAnythingRelatedModel | path: /tmp/workspace/125328— 실패에도 불구하고 정상 종료 경로 진입 (workspace 삭제)
Error Log#
AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin
Impact#
- Service:
cupixworks-any-potree-agent - Team: crcc-sama
- 발생 횟수: 1
- 최초 발생: 2026-06-24 15:06 KST
- 최근 발생: 2026-06-24 15:06 KST
부수 영향:
- pointcloud 125328 의
octree.bin이 S3 에 누락되었을 수 있다.octree.bin은 Potree 의 모든 옥트리 노드 페이로드를 담는 핵심 바이너리이므로 누락 시 뷰어 로드가 사실상 실패한다. - 그럼에도 agent 는 후속
checkUploading호출과 cleanup 까지 정상 흐름으로 진행했고,PotreeService::handlingMessageErrors가 호출되지 않았다 (같은 시각대 로그 검색에서 해당 메시지 없음). 즉 pointcloud 상태가 잘못Uploaded로 마킹되었을 수 있다.
Root Cause Summary#
AwsS3Manager.uploadDirectoryToS3 의 per-file 업로드 루프(uploadChunk)는 개별 파일 업로드 실패를 try/catch 로 잡아 logger.error 만 호출하고 그대로 진행한다(throw 하지 않음). 따라서 일부 파일이 실패해도 디렉터리 업로드 전체는 성공으로 종료되고, 호출자인 PotreeService.uploadPotreeFiles → runByMessage 도 정상 경로(checkUploading, updatePotreeState(Uploaded) 등)를 계속 진행한다. 추가로, catch 블록의 로그 호출은 logger.error('...failed: %s', filePath, error) 형태이며, 포맷 문자열에 %s 가 하나뿐이라서 실제 error 인자는 winston splat 잔여로 남았다가 JSON 트랜스포트의 filterNumericKeys 단계에서 빠지거나 Symbol splat 키에 들어가 직렬화에서 제거된다(packages/utils/src/cplogger.ts:170-183). 결과적으로 Datadog 에는 파일 경로만 남고 에러의 원인(예: 403 access denied, ECONNRESET, NoSuchKey 등)이 사라진다.
Technical Analysis#
Code Path#
- Entry point:
applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:180(runByMessage) - 업로드 호출:
applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:337-379(uploadPotreeFiles) - Failure point:
applications/agents/packages/base/src/manager/aws-s3.manager.ts:155-157(per-filecatchblock)
uploadPotreeFiles 는 S3 자격증명을 받아 awsS3Manager.uploadDirectoryToS3 를 호출한 직후 곧바로 checkUploading API 를 호출한다. 즉 디렉터리 업로드가 throw 하지 않으면 전부 성공으로 간주된다.
await awsS3Manager.uploadDirectoryToS3({
bucketName: s3Credentials.bucket_name,
bucketKeyPath: s3Credentials.basepath,
targetDirectoryPath: resultDir,
acl: s3Credentials.acl
});
await this.cupixApi.pointcloud.checkUploading(cpPointcloud.id);
그런데 uploadDirectoryToS3 의 per-file 처리는 다음과 같이 예외를 삼킨다.
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;
}
// ...
await this.s3.upload({ /* ... */ }).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);
}
}));
};
이 catch 의 로그 호출은 commit c1b797205 (TSLA-6555 refactor: remove stringify error, 2025-11-03) 에서 의도적으로 JSON.stringify(error) 를 제거하면서 다음과 같이 바뀌었다.
-logger.error('AwsS3Manager::uploadDirectoryToS3 | failed: %s - %s', filePath, JSON.stringify(error));+logger.error('AwsS3Manager::uploadDirectoryToS3 | failed: %s', filePath, error);기대 동작: winston errorSafeFormat 이 splat 의 Error 를 {name, message, stack} 으로 변환해 로그 메타에 실어준다.
실제 동작: 포맷 문자열에 %s placeholder 가 1 개뿐이라 error 는 winston.format.splat() 에 의해 메시지로 합쳐지지 않고 info[Symbol.for('splat')] 또는 numeric key 로 남는다. JSON 트랜스포트의 filterNumericKeys 가 숫자 키를 제거하고, Symbol 키는 JSON.stringify 가 직렬화하지 않으므로 결과 메시지에는 filePath 만 남는다.
winston.format.printf((info) => {
const { level, message, label, timestamp, stack, ...rest } = info;
const cleanRest = filterNumericKeys(rest);
return JSON.stringify({
timestamp,
level,
label,
message,
...(stack ? { stack } : {}),
...cleanRest
});
})
또한 uploadDirectoryToS3 는 실패 카운트나 failed: string[] 를 누적해 throw 하는 로직이 없다.
for (let i = 0; i < filesToUpload.length; i += Constants.AwsS3MaxTransferSize) {
const filesChunk = filesToUpload.slice(i, i + Constants.AwsS3MaxTransferSize);
await uploadChunk(filesChunk);
}
logger.debug('AwsS3Manager::uploadDirectoryToS3 | end');
따라서 호출자(PotreeService.uploadPotreeFiles)는 일부 파일이 실패했는지 알 수 없으며, handlingMessageErrors 도 호출되지 않는다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-potree-agent "125328"
해당 시간대 로그 시퀀스 (KST):
2026-06-24 15:04:56 info PotreeService::runByMessage | id: 125328
2026-06-24 15:06:11 error AwsS3Manager::uploadDirectoryToS3 | failed: /tmp/workspace/125328/potree/octree.bin
2026-06-24 15:06:12 info PotreeService::cleanUpAnythingRelatedModel | path: /tmp/workspace/125328
핵심 관찰:
handlingMessageErrors메시지가 같은 timestamp 에 존재하지 않는다 (다른 시간대 실패 사례에서는 항상 짝지어 등장 — 아래 비교).failed: <path>메시지에 에러 본문이 전혀 들어가 있지 않다.
비교 — downloadFile 실패 시에는 에러 본문이 정상 직렬화되어 함께 기록된다.
2026-06-24 15:43:01 error PotreeService::downloadFile | path: /tmp/workspace/1170854/1170854.cpc, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-24 15:43:01 error PotreeService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"36712d98-..."}}
downloadFile 의 catch 는 JSON.stringify(err) 를 직접 포맷 문자열에 넣고 reject 로 전파한다 — 그래서 본문도 남고 handlingMessageErrors 도 호출된다. 대조적으로 uploadDirectoryToS3 의 per-file catch 는 둘 다 깨져 있다.
추가 검색 — 동일 cluster 의 다른 발생 사례를 7일 범위에서 찾았으나 octree.bin 메시지는 이 한 건만 존재.
service:cupixworks-any-potree-agent "octree.bin" → 1 hit
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | uploadDirectoryToS3 의 per-file catch 가 예외를 swallow 하여 디렉터리 업로드 실패가 호출자에게 전파되지 않고, 로그 포맷 인자 불일치로 에러 본문이 사라졌다. |
aws-s3.manager.ts:155-157 의 catch 블록이 throw 하지 않음; uploadDirectoryToS3 끝에 누적 실패 검사가 없음 (line 161-167); 같은 timestamp 에 handlingMessageErrors 미발생; downloadFile 실패는 대조적으로 본문 포함 + handlingMessageErrors 호출됨; commit c1b797205 에서 %s - %s → %s 로 placeholder 가 줄어든 변경 확인. |
— | Confirmed |
| H2 | AWS S3 자체 장애 (eu-central-1 outage) | 동일 시간대 dep:s3-* 인시던트 없음 (status-board: active null, recent empty); 다른 pointcloud 업로드 에러가 동일 시간대에 폭증하지 않음 (7일 검색에서 octree.bin 메시지는 1건뿐). |
단일 객체(octree.bin)만 실패 — 광역 장애 패턴 아님. |
Rejected |
| H3 | S3 credential 만료 (checkToken 실패) |
checkToken 은 uploadChunk 의 try 안에서 호출되어 같은 catch 에 잡힌다. 가능한 시나리오. |
만약 credential 문제라면 동일 디렉터리의 다른 파일 업로드도 같은 catch 에서 줄줄이 실패해 다수의 failed: ... 로그가 나와야 한다. 실제로는 octree.bin 1건만 기록 — 다른 파일들은 성공한 것으로 추정. |
Rejected (단독 원인으로는 부적합) |
| H4 | 특정 파일(octree.bin) 의 일시적인 S3 PutObject 에러 (예: 5xx, throttling, AccessDenied 단발) |
octree.bin 만 단일로 실패 — 개별 객체 단위 transient 문제와 부합. |
에러 본문이 로그에서 사라져서 실제 status code 를 확인할 수 없음 — H1 의 로그 손실 때문에 검증 불가. | Inconclusive (로그가 사라져 확정 불가; H1 이 진짜 root cause) |
Fix Recommendation#
즉시 조치 (Critical)#
- 파일:
applications/agents/packages/base/src/manager/aws-s3.manager.ts:155-157 - 방향: catch 블록의
logger.error포맷 문자열 placeholder 수를 인자 수와 맞춰서 에러 본문이 Datadog 에 남도록 한다.errorSafeFormat이 Error 객체를 변환하도록 두 번째%s자리에error를 직접 넣거나,JSON.stringify(error)대신 winston 의 메타 필드로 넘긴다 (예:logger.error('AwsS3Manager::uploadDirectoryToS3 | failed: %s, error: %s', filePath, error)). 정확한 구현은cplogger.ts의errorSafeFormat동작과 일관성을 맞춰 결정. - 파일:
applications/agents/packages/base/src/manager/aws-s3.manager.ts:130-167 - 방향: per-file 실패를 누적하여 함수 종료 시 한 번이라도 실패가 있었다면 throw 한다. (예:
const failures: { filePath: string; error: unknown }[] = []누적 후if (failures.length > 0) throw new Error(...).) 이렇게 하면PotreeService.uploadPotreeFiles가handlingMessageErrors경로로 들어가고 SQS 메시지 재처리가 가능해진다. - 파일:
applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:378 - 방향: 위 변경이 적용되면
checkUploading은 자동으로 성공 경로에서만 호출된다 — 추가 수정 불필요.
단기 개선 (1주 이내)#
- pointcloud 125328 의 현재 상태를 확인하고 (
potree_state가Uploaded인지),octree.bin이 실제 S3 에 존재하는지 점검. 누락이라면 재처리 큐에 넣는다. uploadDirectoryToS3와 비슷한 패턴이 다른 매니저(AwsS3Manager.uploadFileStreamToS3외에 다른 agent 코드)에 있는지 grep 으로 점검. catch 블록에서%splaceholder 수와 인자 수가 일치하는지 일괄 검토.cplogger.ts의 winston printf 가 splat 에 남은 Error 를 자동으로 메시지 끝에 추가하도록 (예:Symbol.for('splat')의 잔여 Error 를message에 append) 보강.
장기 개선 (재발 방지)#
- 디렉터리 업로드 같은 fan-out 작업은 항상 "all-or-nothing" 의미론을 갖도록 정책화. 부분 실패 시 호출자에게 명시적으로 알리지 않으면 데이터 정합성 문제가 잠재화된다.
- ESLint 규칙 또는 wrapper 로깅 헬퍼를 도입해 포맷 문자열의
%s/%d개수와 가변 인자 수가 일치하는지 강제. 현 코드베이스에서 동일 결함이 반복될 가능성이 크다.
Monitoring#
- 디렉터리 단위 업로드 실패 감지: per-file 실패 카운트가 함수 종료 시점에 0 보다 큰 경우 별도 metric/log 발생.
uploadDirectoryToS3 | failed:로그 발생 빈도 추적.
Datadog timeseries 쿼리 예시:
sum:trace.agent.errors{service:cupixworks-any-potree-agent,resource_name:uploadDirectoryToS3}.as_count()
count:logs{service:cupixworks-any-potree-agent "AwsS3Manager::uploadDirectoryToS3 | failed:"}.rollup(sum, 300)
- pointcloud
potree_state=Uploaded인데 viewer 측에서octree.bin 404가 발생하는 경우 사후 검증 알림을 붙이면 부분 업로드 누락을 사용자 영향 전에 잡을 수 있다.
Risk Assessment#
- Risk level: medium — 단발 발생이지만 root cause 가 silent data loss(잘못된 성공 마킹) 가능성을 내포한다. 큰 pointcloud 일수록 동일 패턴 재현 시 영향이 크다.
- 예상 복잡도: standard — catch 블록 수정 + 누적 실패 throw 로직 추가 + 호출자 경로 동작 확인.