ES /docs

PotreeService::downloadFile | path: /tmp/workspace/1046180/1046180.cpc, error: {"errno":-110,"code":

RCA: PotreeService::downloadFile ETIMEDOUT

Overview#

What Happened#

2026-04-21 13:16:00 UTC에 cupixworks-any-potree-agent 서비스에서 pointcloud ID 1046180의 .cpc 파일을 다운로드하는 중 TCP read ETIMEDOUT (errno -110) 에러가 발생했다. 파일 다운로드가 약 16분간 진행된 후 TCP 연결이 타임아웃되었으며, SQS 메시지의 재처리(retry)를 통해 약 14분 뒤 자동 복구되었다.

Quick Facts#

Field Value
exception.class ETIMEDOUT (Node.js system error)
exception.message read ETIMEDOUT
top_frame TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)
env production, us-west-2

Timeline#

  1. 2026-04-21 13:00:31 UTCPotreeService::runByMessage 시작 (pointcloud ID 1046180, potree_state: created)
  2. 2026-04-21 13:16:00 UTCPotreeService::downloadFile ETIMEDOUT 에러 발생 (다운로드 약 16분 소요 후 실패)
  3. 2026-04-21 13:16:00 UTChandlingMessageErrors에서 ETIMEDOUT을 system error로 인식, SQS 메시지 삭제하지 않음 (retry 허용)
  4. 2026-04-21 13:30:51 UTC — SQS visibility timeout 만료 후 재처리 시작 (retry)
  5. 2026-04-21 13:31:40 UTCcleanUpAnythingRelatedModel 완료 (약 49초 — 정상 처리 또는 이미 처리된 상태로 스킵)

Error Log#

Datadog Logs

text
PotreeService::downloadFile | path: /tmp/workspace/1046180/1046180.cpc, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}

Impact#

  • Service: cupixworks-any-potree-agent
  • 발생 횟수: 1 (이 클러스터), 최근 7일간 동일 에러 약 10건 (다양한 pointcloud ID)
  • 최초 발생: 2026-04-21T13:16:00.699Z
  • 최근 발생: 2026-04-21T13:16:00.699Z
  • 사용자 영향: 해당 pointcloud의 potree 변환이 일시적으로 지연됨. SQS retry 메커니즘으로 자동 복구되므로 최종 사용자에게 영구적 영향은 없음.

Root Cause Summary#

Potree agent가 Cupix API (/pointclouds/{id}/download)에서 .cpc 파일을 HTTP 스트림으로 다운로드하는 중 TCP 연결의 read syscall에서 타임아웃이 발생했다. downloadFile 메서드는 request 라이브러리의 request.get()를 사용하되 timeout 옵션을 설정하지 않아 OS 기본 TCP 타임아웃(일반적으로 수 분)에 의존한다. 대용량 pointcloud 파일 다운로드 중 네트워크 불안정 또는 API 서버 측 연결 중단으로 인해 TCP 소켓의 read 작업이 타임아웃되었다. 이 에러는 일시적 네트워크 문제로, handlingMessageErrors의 system error 감지 로직이 정상적으로 SQS 메시지를 보존하여 retry가 이루어졌다.

Technical Analysis#

Code Path#

  • Entry point: potree-service.ts:180runByMessage 에서 SQS 메시지를 처리
  • potree-service.ts:203downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath) 호출
  • Failure point: potree-service.ts:532-570downloadFile 메서드
potree-service.ts:532-565typescript
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('error', err => {
                    logger.error('PotreeService::downloadFile | path: %s, error: %s', path, JSON.stringify(err));
                    reject(cupixAuth.handleError(err));
                });
        });
});

request.get() 호출 시 timeout 옵션이 설정되어 있지 않다. 이로 인해 TCP 소켓의 타임아웃이 OS 기본값에 의존하며, 대용량 파일 다운로드 중 연결이 중단될 경우 장시간(이 경우 약 16분) 대기 후 ETIMEDOUT이 발생한다.

에러 처리 흐름 (potree-service.ts:502-525):

potree-service.ts:456-464typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) { /* ... */ }
    if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
        logger.warn('PotreeService::getApiErrorToDeleteMessage | nodejs common system error - %s', JSON.stringify(error));
        return;  // undefined 반환 → 메시지 삭제하지 않음 (retry 허용)
    }
    // ...
};

getApiErrorToDeleteMessageerrno, code, syscall 속성이 모두 있는 에러를 "nodejs common system error"로 인식하고 undefined를 반환한다. 이 경우 handlingMessageErrors에서 apiErrorObjectundefined이고, ApproximateReceiveCount가 1이므로 (MaxReceiveCount 10 미만) SQS 메시지를 삭제하지 않아 retry가 가능하다.

potree-service.ts:502-521typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    // ...
    const apiErrorObject = this.getApiErrorToDeleteMessage(error);
    if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
        // apiErrorObject가 undefined이고 receiveCount < MaxReceiveCount이면
        // 이 블록에 진입하지 않음 → 메시지 보존 → SQS retry
    }
    logger.error('PotreeService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
};

Log Evidence#

첫 번째 시도 (실패):

text
Datadog 쿼리: service:cupixworks-any-potree-agent "1046180"
text
2026-04-21 22:00:31 KST — PotreeService::runByMessage | id: 1046180
2026-04-21 22:00:31 KST — PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created
2026-04-21 22:16:00 KST — PotreeService::downloadFile | path: /tmp/workspace/1046180/1046180.cpc, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-04-21 22:16:00 KST — [warn] PotreeService::getApiErrorToDeleteMessage | nodejs common system error - {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-04-21 22:16:00 KST — [warn] CupixAuth::handleError | Undefined response: {"stack":"Error: read ETIMEDOUT\n    at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)","message":"read ETIMEDOUT","errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-04-21 22:16:00 KST — PotreeService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"2e76ff99-6d50-4256-9eca-27a5c6c6b808","Attributes":{"ApproximateReceiveCount":"1"}}}

다운로드 시작(22:00:31)부터 에러(22:16:00)까지 약 16분 소요. TCP read syscall이 타임아웃됨.

두 번째 시도 (retry, 성공):

text
2026-04-21 22:30:51 KST — PotreeService::runByMessage | id: 1046180
2026-04-21 22:31:40 KST — PotreeService::cleanUpAnythingRelatedModel | path: /tmp/workspace/1046180

retry는 약 49초 만에 완료. SQS 메시지 삭제(deleteMessage) 및 정리가 수행되었으므로 정상 처리된 것으로 판단된다.

최근 7일간 동일 패턴 (ETIMEDOUT):

text
Datadog 쿼리: service:cupixworks-any-potree-agent status:error "ETIMEDOUT" (2026-04-14 ~ 2026-04-21)
text
2026-04-16 06:38 — pointcloud 1036120 (us-west-2)
2026-04-16 09:53 — pointcloud 1036516 (1차 시도)
2026-04-16 10:29 — pointcloud 1036516 (2차 시도, ReceiveCount: 2)
2026-04-17 19:05 — pointcloud 1039711
2026-04-17 23:57 — pointcloud 105912
2026-04-18 00:21 — pointcloud 1040086 (1차 시도)
2026-04-18 00:57 — pointcloud 1040086 (2차 시도, ReceiveCount: 2)
2026-04-19 13:23 — pointcloud 106110
2026-04-21 01:17 — pointcloud 1044704
2026-04-21 13:16 — pointcloud 1046180 (이 클러스터)

7일간 약 10건 발생. 특정 시간대 집중 없이 산발적으로 발생하며, 다양한 pointcloud ID에 영향. 일부 건은 2차 시도에서도 실패 후 3차에서 성공하는 패턴을 보임.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 대용량 파일 다운로드 중 TCP 연결 타임아웃 (일시적 네트워크 문제) ETIMEDOUT errno -110은 TCP read 타임아웃을 의미. 다운로드 시작 후 약 16분 뒤 발생. stack trace에 TLSWrap.onStreamRead 확인. 7일간 산발적 발생 패턴. retry 시 대부분 성공. -- Confirmed
H2 API 서버 측 연결 끊김 또는 응답 지연 ETIMEDOUT이 서버 측 문제로도 발생 가능. 동일 시간대에 다른 job(106328, 196344)은 정상 처리됨 → 특정 다운로드 연결의 문제 서비스 전체 장애는 아님 (다른 job은 정상). API 서버 로그를 직접 확인하지 못함 Inconclusive
H3 request 라이브러리의 timeout 미설정으로 인한 지연 확대 request.get() 호출 시 timeout 옵션 없음 (potree-service.ts:538). OS 기본 TCP 타임아웃에 의존하여 16분간 대기 timeout을 설정했더라도 ETIMEDOUT 자체는 발생했을 것. 다만 실패 감지 시간은 단축 가능 Confirmed (contributing factor)

Fix Recommendation#

즉시 조치 (Critical)#

  • 에러 레벨 조정: 이 에러는 일시적 네트워크 문제이며 SQS retry로 자동 복구된다. potree-service.ts:562downloadFile error 로그를 error에서 warn으로 변경하고, potree-service.ts:523handlingMessageErrors도 system error인 경우 warn으로 변경하는 것을 권장한다. 이를 통해 불필요한 error alert을 줄일 수 있다.

단기 개선 (1주 이내)#

  • request.get()에 timeout 옵션 추가 (potree-service.ts:538): 다운로드 타임아웃을 명시적으로 설정(예: 5분)하여, TCP 기본 타임아웃(~16분) 대기 대신 빠르게 실패 감지 후 retry를 시작할 수 있도록 한다.
  • request 라이브러리 대체 검토: request 라이브러리는 deprecated 상태. node-fetch, axios, 또는 Node.js 내장 fetch/undici로 마이그레이션을 고려한다.

장기 개선 (재발 방지)#

  • 파일 다운로드 retry 로직 내장: 현재 retry는 SQS message visibility timeout에 의존한다. downloadFile 내부에 exponential backoff retry(2-3회)를 구현하면 SQS 재처리 없이 같은 agent 내에서 빠르게 복구할 수 있다.
  • 다운로드 progress 모니터링: 대용량 파일 다운로드 시 진행률 또는 수신 바이트를 주기적으로 로깅하여, 타임아웃 발생 시 얼마나 다운로드되었는지 파악할 수 있게 한다.

Monitoring#

  • 다운로드 타임아웃 빈도 추적:
text
service:cupixworks-any-potree-agent status:error "ETIMEDOUT" "downloadFile"
  • retry 후 성공/실패 비율 추적 (ApproximateReceiveCount 기반):
text
service:cupixworks-any-potree-agent "handlingMessageErrors" "ApproximateReceiveCount"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 근거: 일시적 네트워크 문제이며, SQS retry 메커니즘이 정상 작동하여 자동 복구된다. 7일간 약 10건으로 빈도가 낮고, 최종 사용자에게 영구적 영향은 없다. 주요 개선은 에러 레벨 조정과 timeout 설정이며, 코드 변경 범위가 작다.