PotreeService#downloadFile — missing retry on transient network error
RCA: PotreeService::downloadFile 503 Service Unavailable
Overview#
What Happened#
2026-06-08 15:20:54 KST에 cupixworks-any-potree-agent (gad 테넌트, me-central-1 리전) 가 pointcloud 1132267의 원본 .cpc 파일을 S3 presigned URL로 다운로드하던 중 AWS S3 가 503 Service Unavailable 응답을 반환하여 작업이 실패했다. 단 1건의 단발성 이벤트로, downstream(S3) 의 일시적 장애로 보인다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | PotreeService::downloadFile |
| exception.message | code: 503, message: Service Unavailable |
| top_frame | applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:555 |
| runtime | Node.js / TypeScript (cupix-tesla-potree-agent ECS task) |
| env | production, region me-central-1 (S3 호스트 기준), tenant gad |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad (me-central-1 pointcloud 처리) | 1 | pointcloud 1132267 Potree 변환 실패 — potree_state 가 Error 로 마킹되어 사용자 측에서 해당 point cloud 미리보기/뷰어 불가. SQS 메시지가 재처리 큐로 돌아가지 않고 삭제될 수 있음 (Hypotheses 참조). |
Timeline#
- 2026-06-08 15:20:51 KST —
PotreeService::runByMessage | id: 1132267— 메시지 처리 시작 - 2026-06-08 15:20:54 KST — S3 presigned URL (
s3.me-central-1.amazonaws.com/cupixworks-source-...mece1/...) GET 응답503 Service Unavailable(AmazonS3 server, request id309942E0451E8166) - 2026-06-08 15:20:54 KST —
PotreeService::downloadFile | response path: /tmp/workspace/1132267/1132267.cpc, code: 503(error) - 2026-06-08 15:20:54 KST —
CupixAuth::handleError | Undefined responsewarn —response객체 형태가 axios/nodejs 어느 쪽과도 매치되지 않아 statusCode 판별 누락 - 2026-06-08 15:20:54 KST —
PotreeService::getApiErrorToDeleteMessage | undefined responsewarn —response가error.response경로에 없어undefined response로 인식
Error Log#
PotreeService::downloadFile | response path: /tmp/workspace/1132267/1132267.cpc, code: 503, message: Service Unavailable
Impact#
- Service:
cupixworks-any-potree-agent - Team: gad
- 발생 횟수: 1
- 최초 발생: 2026-06-08 15:20:54 KST
- 최근 발생: 2026-06-08 15:20:54 KST
Root Cause Summary#
me-central-1 리전의 AWS S3 가 presigned GET 요청에 대해 503 Service Unavailable 을 반환했다(content-length 0, x-amz-request-id 309942E0451E8166). PotreeService.downloadFile() 는 request.get(url) 의 응답 statusCode 가 200 이 아닐 경우 즉시 reject 만 하고 재시도(retry) 로직이 없다 — 같은 코드베이스의 CupixAuth.retryable() 는 5xx 에 대해 지수 백오프 재시도를 제공하지만 다운로드 경로는 이를 사용하지 않는다. 따라서 일시적인 S3 503 한 번에도 전체 pointcloud 변환 작업이 실패한다. 단 1회 발생이라는 점에서 S3 측 transient error 로 추정되며, 클라이언트 측 재시도 부재가 에러를 사용자 영향(potree_state: Error)으로 확대시킨 root cause 다.
Technical Analysis#
Code Path#
- Entry point:
applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:180—runByMessage - Invokes download:
potree-service.ts:203—await this.downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath) - Failure point:
potree-service.ts:553-556— non-200 응답 시 즉시 reject - Error escalation:
potree-service.ts:213— catch 시updatePotreeState(targetId, PointcloudPotreeState.Error)로 상태를 Error 로 변경 - Auxiliary:
applications/agents/packages/api/src/authentication/cupix-auth.ts:42-57—handleError는 로깅만 수행 (statusCode 추출 후setErrorCode), retry 트리거 없음
private downloadFile = (url: string, path: string): Promise<void> => new Promise((resolve, reject) => {
const cupixAuth = this.cupixAuth;
cupixAuth.checkToken()
.then(() => {
logger.debug('PotreeService::downloadFile | start path: %s, url: %s', path, url);
const fileStream = fs.createWriteStream(path);
const sendReq = request.get(url, {
headers: {
'X-CUPIX-AUTH': cupixAuth.accessToken
}
});
// ...
sendReq
.on('response', res => {
if (res.statusCode !== 200) {
logger.error('PotreeService::downloadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
reject(cupixAuth.handleError(res));
} else {
sendReq.pipe(fileStream);
}
})
.on('error', err => {
logger.error('PotreeService::downloadFile | path: %s, error: %s', path, JSON.stringify(err));
reject(cupixAuth.handleError(err));
});
})
retryable = <T>(f: () => Promise<T>, retries?: number): Promise<T> => new Promise((resolve, reject) => {
const _retries = retries != undefined ? retries : 0;
f()
.then(response => resolve(response))
.catch(e => {
if (e && e.statusCode > 500 && _retries < Constants.MaxRetries) {
// exponential backoff: 2^retries * 1000 ms
const delay = Math.pow(2, _retries) * 1000;
logger.warn(`CupixAuth::retryable | pathname: ${pathName}, code: ${e.statusCode}, try: ${_retries + 1} - run after ${delay / 1000} seconds`);
setTimeout(() => { /* retry f() */ }, delay);
}
});
});
기대 동작 vs 실제 동작: 일시적 5xx 에 대해 retryable 로 감싸 백오프 재시도하면 대부분 자가 회복 가능. 실제로는 다운로드 경로가 retryable 미사용으로 1번 실패에 즉시 영구 실패 처리되어 potree_state: Error 로 전이됨.
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-any-potree-agent "1132267"
시간 범위: 2026-06-08T05:00:00Z ~ 2026-06-08T07:30:00Z
이벤트 타임라인 (KST):
15:20:51 info PotreeService::runByMessage | id: 1132267
15:20:54 warn CupixAuth::handleError | Undefined response: {"statusCode":503,"request":{"method":"GET"}}
15:20:54 warn PotreeService::getApiErrorToDeleteMessage | undefined response - {"statusCode":503,...}
15:20:54 error PotreeService::downloadFile | response path: /tmp/workspace/1132267/1132267.cpc, code: 503, message: Service Unavailable
S3 응답 헤더 발췌 (warn 로그에서):
{
"statusCode": 503,
"headers": {
"date": "Mon, 08 Jun 26 06:20:53 GMT",
"x-amz-id-2": "SYQmdtHQMufdzDlA8h4eT6zTe5HmLICrffUIw2NZLZcaSTFc0SGEE1FEWLVt2nXU1ddWioyL6tj2in68MIbO7q0zSfitujUYfPR2c8l/HOtDG7wS38U2tVWzLQYbFKDZ",
"x-amz-request-id": "309942E0451E8166",
"content-type": "application/xml",
"server": "AmazonS3",
"content-length": "0"
},
"request": {
"uri": {
"host": "s3.me-central-1.amazonaws.com",
"pathname": "/cupixworks-source-b169da1a0187-mece1/resources/9a5i56/mece1/v1"
},
"method": "GET",
"headers": {
"X-CUPIX-AUTH": "session_token:9ymj7pekxisp,session_id:10682851",
"referer": "http://api-tesla.cupix.internal/api/v1/pointclouds/1132267/download"
}
}
}
핵심 관찰:
- 응답 server 가
AmazonS3, content-length0, body 없음 — AWS 측 일시적 장애 패턴. - 호스트는
s3.me-central-1.amazonaws.com(Middle East Central). 클러스터 frontmatter 의regions: us-west-2는 agent ECS task 실행 리전이고, 실제 데이터 리전은 me-central-1 (gad 테넌트). - 14일 윈도우 내 (
now-7d) 동일 서비스에서 다른 503 발생 없음 — 단발성/transient. - cupixworks-api 로그에서도
1132267관련 error 없음 — 다운로드 URL 발급(/api/v1/pointclouds/1132267/download) 자체는 성공.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | AWS S3 (me-central-1) 의 transient 503; 클라이언트 측 재시도 부재로 단발 실패가 작업 실패로 확대됨 | response server AmazonS3, content-length 0, x-amz-request-id 존재; downloadFile 가 retryable 미사용 (potree-service.ts:532-570); 14일간 동일 패턴 재발 없음 (단발) |
— | Confirmed |
| H2 | Cupix API (/api/v1/pointclouds/1132267/download) 가 잘못된 / 만료된 presigned URL 을 반환 |
referer 가 cupix-api download 엔드포인트 | X-Amz-Date=20260608T062051Z, X-Amz-Expires=10800 (3시간) — URL 발급 2초 후 사용으로 만료 아님; 서명 형식 정상; tesla 측 1132267 error 로그 없음 |
Rejected |
| H3 | 인증 토큰(X-CUPIX-AUTH) 누락/만료로 인한 거부 |
CupixAuth::handleError warn 가 흐름에 등장 |
S3 presigned URL 은 X-Amz-Signature 로 인증; X-CUPIX-AUTH 헤더는 S3 가 무시; 503 본문 비어있음 (auth 거부면 보통 403/401 + AccessDenied XML) | Rejected |
| H4 | 디스크 풀/네트워크 단절 등 agent 측 환경 문제 | fs.createWriteStream(path) 도 실패할 수 있음 |
이벤트는 on('response') 에서 발생 (HTTP 응답 수신 단계 도달); statusCode 503 명시; 같은 task 가 직후 다른 메시지 정상 처리 (potree-service runByMessage 1132395 등 16:21~16:22 정상) |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 별도 즉시 조치 불필요. 단 1회 발생이고 외부 일시적 장애로 보임. 사용자 영향이 있는 pointcloud
1132267은 수동 또는 재처리 도구로 retry 만 해도 복구 가능 (가설: presigned URL 재발급 후 재시도 시 정상 다운로드).
단기 개선 (1주 이내)#
downloadFile에 retry 적용:applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:532-570. 5xx 응답 시CupixAuth.retryable패턴(지수 백오프,Constants.MaxRetries)을 다운로드 경로에도 적용. 동일 패턴이 forge-agent / floorplan-agent / thumbnail-agent / room-agent / postprocessor-agent / 3d-recon-agent / preprocessor-agent 의transfer.manager.ts에도 존재 — 공통 헬퍼로 한 번에 개선하는 방향 권장.- 에러 분류: 5xx (transient, retry 후 실패) →
warn로 다운그레이드 후 재시도, 최종 실패 시에만error. 401/403/404 등은 즉시error. memory 노트의 "Cross-region tokens, rate limits, and transient network issues may warrant warn-level logging" 가이드라인과 일치. handleError의 response 파싱 개선: cupix-auth.ts:42-56 — request 라이브러리의 IncomingMessage 형태(res.statusCode직접 노출,response.body미존재)를 axios/node 와 함께 처리하도록 분기 보강. 현재는 "Undefined response" 로 떨어져 statusCode 추출 실패.
장기 개선 (재발 방지)#
- 모든 agent 의 S3/HTTP 다운로드/업로드 경로를 하나의 공통
transfer.manager(또는request-with-retry) 모듈로 통합. 현재는 7개 패키지가transfer.manager.ts또는 인라인downloadFile을 각자 구현 — retry/timeout/에러 분류 정책이 일관되지 않음. - pointcloud potree 처리 작업의 멱등성 강화: SQS visibility timeout 만료 시 자동 재시도가 의도대로 동작하는지 확인. 현 로직(
getApiErrorToDeleteMessage+checkReceiveCountToDeleteMessage, potree-service.ts:456-525)은 5xx (statusCode 500) 를400 <= s <= 500조건에 포함시켜 메시지를 즉시 삭제할 수 있음 — 503 은 501-504 범위 밖 (>500) 이라 삭제 대상은 아니지만, 의도가 모호하므로 명시적으로 5xx transient 분기를 분리.
Monitoring#
-
추가 메트릭/알림:
cupixworks-any-potree-agent의PotreeService::downloadFile error code:5xx카운트가 1시간 내 임계값 초과 시 알림 (region/tenant 별).potree_state: Error로 전이된 pointcloud 비율 모니터.
-
Datadog 쿼리 예시:
service:cupixworks-any-potree-agent status:error "PotreeService::downloadFile" "code: 5"
service:cupixworks-any-potree-agent status:warn "CupixAuth::handleError" "statusCode\":5"
Risk Assessment#
- Risk level: low (1회 발생, 외부 일시 장애)
- 예상 복잡도: standard (downloadFile 에 retry wrapping 추가 + 공통화 검토)