ES /docs

PotreeService::downloadFile | response path: /tmp/workspace/1121500/1121500.cpc, code: 500, message:

RCA: PotreeService::downloadFile | S3 500 Internal Server Error

Overview#

What Happened#

2026-06-03 09:34 KST에 cupixworks-any-potree-agent가 pointcloud ID 1121500의 CPC 파일을 S3 me-central-1 리전에서 다운로드하려다 AWS S3로부터 HTTP 500 Internal Server Error를 수신했다. 에이전트는 재시도 없이 즉시 실패 처리하여 SQS 메시지를 삭제하고 pointcloud의 potree_state를 Error로 업데이트했다.

Quick Facts#

Field Value
exception.message PotreeService::downloadFile | response path: /tmp/workspace/1121500/1121500.cpc, code: 500, message: Internal Server Error
top_frame potree-service.ts:555
env production, us-west-2
S3 region me-central-1
S3 bucket cupixworks-source-b169da1a0187-mece1
S3 request ID E1PGT0SX2FA8CXVK

Timeline#

  1. 2026-06-03 09:34 KST — PotreeService가 SQS에서 pointcloud 1121500 메시지 수신 (runByMessage)
  2. 2026-06-03 09:34 KST — Tesla API에서 pointcloud 메타 조회 성공 (200)
  3. 2026-06-03 09:34 KST — Tesla API /pointclouds/1121500/download 호출 → 302 redirect (S3 presigned URL)
  4. 2026-06-03 09:34 KST — S3 me-central-1에서 500 Internal Server Error 응답
  5. 2026-06-03 09:34 KSThandlingMessageErrors에서 "undefined response" 판정 → SQS 메시지 삭제, potree_state → Error

Error Log#

Datadog Logs

text
PotreeService::downloadFile | response path: /tmp/workspace/1121500/1121500.cpc, code: 500, message: Internal Server Error

Impact#

  • Service: cupixworks-any-potree-agent
  • Team: gad
  • 발생 횟수: 1
  • 최초 발생: 2026-06-03 09:34 KST
  • 최근 발생: 2026-06-03 09:34 KST

Root Cause Summary#

AWS S3 me-central-1 리전에서 일시적(transient) 500 Internal Server Error가 발생했다. 이는 AWS 측의 일시적인 서버 에러이며, potree agent의 downloadFile 메서드에 재시도(retry) 로직이 전혀 구현되어 있지 않아 단 한 번의 실패로 전체 작업이 영구 실패 처리되었다. 에러 핸들링 로직(getApiErrorToDeleteMessage)에서 S3 응답을 Tesla API 응답 포맷으로 파싱하려 하여 "undefined response"로 판정한 뒤, SQS 메시지를 삭제하고 pointcloud 상태를 Error로 설정하여 복구 불가능한 상태로 만들었다.

Technical Analysis#

Code Path#

  • Entry point: potree-service.ts:180runByMessage 에서 메시지 처리 시작
  • Download initiation: potree-service.ts:203downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath) 호출
  • Download URL construction: cppointcloud.ts:70Environment.CPX_API_ENDPOINT + '/pointclouds/' + pointcloudIdStr + '/download'로 Tesla API 엔드포인트 구성
  • Tesla API response: 302 redirect → S3 presigned URL (s3.me-central-1.amazonaws.com)
  • Failure point: potree-service.ts:554-556 — S3가 statusCode 200이 아닌 500 반환
packages/cupix-tesla-potree-agent/src/potree-service.ts:532-570typescript
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));
                });
        })
  • 이 코드에는 retry 로직이 없음. statusCode !== 200이면 즉시 reject.

Error handling flow: potree-service.ts:502-525handlingMessageErrors

packages/cupix-tesla-potree-agent/src/potree-service.ts:456-490typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) {
        logger.warn('PotreeService::getApiErrorToDeleteMessage | undefined error');
        return 'undefined error';
    }
    if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
        logger.warn('PotreeService::getApiErrorToDeleteMessage | nodejs common system error - %s', JSON.stringify(error));
        return;  // 네트워크 에러는 메시지를 삭제하지 않음 (재시도 가능)
    }
    const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
    if (response == undefined) {
        logger.warn('PotreeService::getApiErrorToDeleteMessage | undefined response - %s', JSON.stringify(error));
        return 'undefined response';  // <-- S3 500 에러가 여기에 해당
    }
    // ...
    if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
        if (statusCode === 401) return;
        return errorMsg;  // 400-500 범위면 메시지 삭제
    }
    return;
};

S3 500 응답은 error.response가 없는 형태로 reject되어 "undefined response"를 반환한다. 이 반환값이 truthy이므로 handlingMessageErrors에서 SQS 메시지를 삭제하고 potree_state를 Error로 설정한다.

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-any-potree-agent "1121500"
Time: 2026-06-03T00:00:00Z to 2026-06-03T02:00:00Z

S3 응답 상세 (warn 로그에서 확인):

json
{
  "statusCode": 500,
  "headers": {
    "x-amz-request-id": "E1PGT0SX2FA8CXVK",
    "x-amz-id-2": "6Nxx4RSCvq12g8SYNJQ6mPvETRQ+fLSjRhKN5gktgbqVS/dYUjdc7o+EJOSgzBt7wLTQzbaRUm6N7wVg5pbLu/1SKemG0j5/",
    "server": "AmazonS3"
  },
  "request": {
    "uri": {
      "host": "s3.me-central-1.amazonaws.com",
      "pathname": "/cupixworks-source-b169da1a0187-mece1/resources/n2qn5u/mece1/v1"
    },
    "method": "GET",
    "headers": {
      "X-CUPIX-AUTH": "session_token:9ymj7pekxisp,session_id:10682851",
      "referer": "http://api-tesla.cupix.internal/api/v1/pointclouds/1121500/download"
    }
  }
}

Tesla API 로그 확인:

text
[302] GET /api/v1/pointclouds/1121500/download (Api::V1::PointcloudsController#download_single_resource)

동일 시간대 유사 에러 패턴 (3일간):

text
2026-06-03 09:55:34 - downloadFile | path: /tmp/workspace/118430/118430.cpc, error: ECONNRESET
2026-06-03 09:34:50 - downloadFile | response path: /tmp/workspace/1121500/1121500.cpc, code: 500
2026-06-03 08:31:56 - downloadFile | path: /tmp/workspace/1121376/1121376.cpc, error: ECONNRESET
2026-06-02 10:48:29 - downloadFile | path: /tmp/workspace/118160/118160.cpc, error: ETIMEDOUT
2026-06-02 09:50:41 - downloadFile | path: /tmp/workspace/1119712/1119712.cpc, error: ETIMEDOUT
2026-06-02 03:06:52 - downloadFile | path: /tmp/workspace/1119084/1119084.cpc, error: ETIMEDOUT

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 AWS S3 transient 500 error + 재시도 로직 부재 S3 x-amz-request-id 포함된 500 응답, AWS 공식 문서에서 S3 5xx는 retry 권장, downloadFile에 retry 미구현 Confirmed
H2 presigned URL 만료 또는 서명 불일치 URL의 X-Amz-Expires=10800 (3시간)이고 생성 직후 요청, 서명 알고리즘 정상, 403이 아닌 500 반환 Rejected
H3 S3 객체가 존재하지 않거나 삭제됨 객체 미존재 시 404 반환됨, 실제로는 500 반환, 동일 버킷의 다른 작업(upload)은 성공 Rejected
H4 에이전트-S3 간 네트워크 문제 (cross-region) 에이전트는 us-west-2, S3는 me-central-1으로 cross-region 통신, 동일 시간대 ECONNRESET/ETIMEDOUT 에러도 다수 S3 500은 네트워크 에러와 다른 서버 사이드 응답 Partially supporting

Fix Recommendation#

즉시 조치 (Critical)#

  • potree-service.ts:532downloadFile 메서드에 exponential backoff 재시도 로직 추가 (최대 3회)
  • S3 5xx 및 네트워크 에러(ECONNRESET, ETIMEDOUT) 모두 재시도 대상에 포함
  • 현재 ECONNRESET은 getApiErrorToDeleteMessage에서 return undefined하여 메시지 삭제 안 됨 (SQS visibility timeout 후 재시도 가능), 그러나 S3 500은 "undefined response"로 메시지 삭제됨 — 이 비대칭 동작을 수정해야 함

단기 개선 (1주 이내)#

  • getApiErrorToDeleteMessage 로직 개선: S3 응답(statusCode 포함, x-amz-request-id 헤더 존재)을 식별하여 5xx 에러 시에는 메시지를 삭제하지 않도록 변경. 이렇게 하면 SQS의 기본 재시도 메커니즘으로 자동 복구 가능.
  • downloadFile에서 reject 시 전달하는 에러 객체에 statusCode를 명시적으로 포함하도록 수정하여 downstream 에러 핸들러가 S3 에러를 올바르게 분류할 수 있도록 개선.

장기 개선 (재발 방지)#

  • 모든 agent의 HTTP 다운로드/업로드 로직에 공통 retry wrapper 적용 (AWS SDK의 내장 retry와 유사하게 jitter 포함 exponential backoff)
  • me-central-1 등 원격 리전 S3 접근 시 cross-region latency를 고려한 timeout 값 조정
  • deprecated request 라이브러리를 got 또는 axios 등 retry 지원 라이브러리로 교체 검토

Monitoring#

  • service:cupixworks-any-potree-agent status:error "downloadFile" — 다운로드 실패 빈도 모니터링
  • service:cupixworks-any-potree-agent "code: 500" — S3 500 에러 재발 감시
  • Datadog metric 추가 권장: potree.download.failure{reason:s3_500}, potree.download.failure{reason:network}

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — downloadFile에 retry 추가는 단순하나, getApiErrorToDeleteMessage의 에러 분류 로직 수정 시 다른 에러 케이스 영향 확인 필요