ES /docs

PotreeService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETI

RCA: PotreeService::handlingMessageErrors ETIMEDOUT

Overview#

What Happened#

2026-04-21 13:16:00 UTC에 cupixworks-any-potree-agent 서비스의 us-west-2 리전에서 pointcloud 1046180 파일 다운로드 중 TLS 소켓 read timeout(ETIMEDOUT)이 발생했다. 약 15분간 다운로드를 시도한 후 소켓이 타임아웃되었으며, SQS 메시지 재시도를 통해 약 15분 후 자동 복구되었다.

Quick Facts#

Field Value
exception.class ETIMEDOUT (errno: -110)
exception.message read ETIMEDOUT
top_frame TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)
runtime Node.js 20+ (filebeat 7.17.15)
env production, us-west-2

Timeline#

  1. 13:00:31Z — PotreeService가 pointcloud 1046180 처리 시작, 파일 다운로드 개시
  2. 13:16:00.699Z — 약 15.5분 후 downloadFile에서 ETIMEDOUT 에러 발생
  3. 13:16:00.701ZgetApiErrorToDeleteMessage가 transient system error로 분류, SQS 메시지 삭제하지 않음
  4. 13:16:00.702ZhandlingMessageErrors에서 에러 로깅 (이 클러스터의 대표 에러)
  5. 13:30:51Z — SQS visibility timeout 후 2차 시도, 다운로드 및 PotreeConverter 실행 성공
  6. 13:31:41Z — SQS 메시지 정상 삭제, 처리 완료

Error Log#

Datadog Logs

text
PotreeService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"2e76ff99-6d50-4256-9eca-27a5c6c6b808","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-any-potree-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-21T13:16:00.702Z
  • 최근 발생: 2026-04-21T13:16:00.702Z
  • 영향 범위: pointcloud 1046180 (user: arifm@sixco-trojan.com, team: gad/590). 처리가 약 30분 지연되었으나 데이터 손실 없이 자동 복구됨.

Root Cause Summary#

Potree agent가 pointcloud 1046180의 .cpc 파일을 API 서버에서 다운로드하는 도중 TLS 소켓의 read syscall에서 ETIMEDOUT (errno -110)이 발생했다. downloadFile 메서드가 request 라이브러리를 사용하여 HTTP GET 요청을 보내는데, 명시적인 timeout 설정이 없어 OS 레벨 TCP timeout(약 15분)에 의존한다. 동일 시간대에 us-west-2 리전의 다른 서비스들(TransferManager, ioredis 등)에서도 24건의 ETIMEDOUT 에러가 관측되어 일시적 네트워크 이슈가 있었음을 보여준다. SQS의 재시도 메커니즘 덕분에 메시지가 재처리되어 자동 복구되었다.

Technical Analysis#

Code Path#

  • Entry point: potree-service.ts:54checkingQueue에서 SQS 메시지를 수신하고 runByMessages를 호출
  • potree-service.ts:180runByMessage에서 개별 메시지 처리 시작
  • potree-service.ts:203downloadFile 호출 (cpPointcloud.downloadUrl, cpPointcloud.originalFilePath)
  • Failure point: potree-service.ts:532-570downloadFile 내부에서 request.get의 TLS 소켓 read timeout

파일 다운로드를 수행하는 downloadFile 메서드:

applications/agents/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
                }
            });

            fileStream
                .on('finish', async () => {
                    await CPUtils.sleep(100);
                    fileStream.close();
                    resolve();
                });

            sendReq
                .on('response', res => {
                    if (res.statusCode !== 200) {
                        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));
                });
        })
        .catch(error => {
            reject(error);
        });
});

request.get 호출에 timeout 옵션이 설정되지 않아, TCP/TLS 레벨의 OS 기본 timeout(약 15분)에 의존한다. 대용량 pointcloud 파일 다운로드 중 네트워크 불안정 시 장시간 hang 후 timeout이 발생한다.

에러가 handlingMessageErrors로 전파된 후의 분기 로직:

applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:456-464typescript
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;  // undefined 반환 → transient error로 분류
    }
    // ...
};

errno, code, syscall 속성이 모두 있으면 Node.js system error로 판단하고 undefined를 반환한다. 이후 handlingMessageErrors에서:

applications/agents/packages/cupix-tesla-potree-agent/src/potree-service.ts:502-524typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    // ...
    if (this.messageInProcess) {
        const apiErrorObject = this.getApiErrorToDeleteMessage(error);
        if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
            // apiErrorObject가 undefined이고 receiveCount(1) < MaxReceiveCount(10)이므로
            // 이 블록에 진입하지 않음 → SQS 메시지 삭제 안 함 → 재시도 가능
            try {
                await this.deleteByMessage(this.messageInProcess);
                if (this._modelInProcess != undefined) await this.updatePotreeState(..., Error);
            } catch (error) { /* ... */ }
        }
    }
    logger.error('PotreeService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
};

apiErrorObjectundefined이고 ApproximateReceiveCount(1)가 MaxReceiveCount(10) 미만이므로, SQS 메시지가 삭제되지 않고 visibility timeout 후 재시도된다. 이 설계는 transient network error에 대한 올바른 대응이다.

Log Evidence#

Datadog에서 사용한 쿼리:

text
service:cupixworks-any-potree-agent status:error @environment:production

에러 직전 다운로드 시도 로그 (13:00:31Z):

text
PotreeService::runByMessage | id: 1046180
CupixAuth::setSession | session_id: 4c5e46c15248f009a97f1e3857475bbf8afa46f1
PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created

에러 시점의 연속 로그 (13:16:00Z):

text
[13:16:00.699Z] PotreeService::downloadFile | path: /tmp/workspace/1046180/1046180.cpc, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
[13:16:00.701Z] PotreeService::getApiErrorToDeleteMessage | nodejs common system error - {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
[13:16:00.701Z] 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"}
[13:16:00.702Z] PotreeService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"2e76ff99-6d50-4256-9eca-27a5c6c6b808","Attributes":{"ApproximateReceiveCount":"1"}}}

재시도 성공 로그 (13:30:51Z):

text
[13:30:51.650Z] PotreeService::runByMessage | id: 1046180
[13:30:51.738Z] PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: uploading
[13:30:58.411Z] PotreeService::runPotreeConvertor | exec - PotreeConverter ./potree_converter_input.json
[13:31:40.991Z] PotreeService::cleanUpAnythingRelatedModel | path: /tmp/workspace/1046180
[13:31:41.046Z] AwsQueueManager::deleteMessage | end - message id: 2e76ff99-6d50-4256-9eca-27a5c6c6b808

동일 시간대 다른 서비스의 ETIMEDOUT 에러 (리전 전반 네트워크 이슈 증거):

text
service:* status:error ETIMEDOUT @environment:production

2시간 윈도우에서 24건의 ETIMEDOUT 로그 관측. TransferManager의 uploadFile, ioredis의 connect 등 다양한 서비스에서 발생. 모두 동일한 시그니처: errno: -110, code: ETIMEDOUT, syscall: read (TLS socket read timeout).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 us-west-2 리전의 일시적 네트워크 불안정으로 TLS 소켓 read timeout 발생 동일 시간대 24건의 ETIMEDOUT이 다양한 서비스(TransferManager, ioredis, PotreeService)에서 관측됨. 모두 동일한 errno: -110, syscall: read 시그니처. 재시도 시 성공. Confirmed
H2 Potree agent의 downloadFile에 timeout 미설정으로 인한 코드 결함 request.get 호출 시 timeout 옵션 미설정 확인 (potree-service.ts:538). OS 기본 TCP timeout(~15분)에 의존. timeout 미설정은 hang 시간을 늘리지만 ETIMEDOUT 자체의 원인은 아님. 네트워크가 정상이면 timeout 없이도 동작함. Contributing factor
H3 대상 파일(1046180.cpc)이 대용량이어서 다운로드 시간이 길어짐 다운로드 시작(13:00:31)부터 에러(13:16:00)까지 약 15.5분 소요 — 대용량 파일일 가능성. 재시도 시 다운로드+변환+업로드가 약 1분(13:30:51~13:31:41)에 완료됨. 파일 크기 자체가 문제는 아님. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 단발성 네트워크 이슈이며 SQS 재시도로 자동 복구됨. 에러 핸들링이 올바르게 동작하여 메시지가 삭제되지 않고 재시도됨.

단기 개선 (1주 이내)#

  • potree-service.ts:538request.get 호출에 명시적 timeout 옵션 추가. 현재 OS 기본 TCP timeout(~15분)에 의존하고 있어, 적절한 timeout(예: 5분)을 설정하면 장시간 hang을 방지하고 더 빠르게 재시도할 수 있다.
  • 이 에러의 로그 레벨을 error에서 warn으로 하향 검토. transient network error는 SQS 재시도로 자동 복구되므로 error 레벨이 과도할 수 있다. getApiErrorToDeleteMessage에서 이미 system error로 분류하고 있으므로, handlingMessageErrors에서 재시도 가능한 경우 warn 레벨로 로깅하는 것이 적절하다.

장기 개선 (재발 방지)#

  • request 라이브러리는 deprecated 상태. axios, undici, 또는 Node.js native fetch로 마이그레이션하면 timeout, retry, abort 제어가 더 용이해진다.
  • 다운로드 retry 로직을 downloadFile 내부에 추가하여 SQS 레벨 재시도(visibility timeout 대기) 없이도 빠르게 복구할 수 있도록 개선.

Monitoring#

  • transient network timeout 빈도를 추적하는 메트릭 추가:
text
service:cupixworks-any-potree-agent status:error ETIMEDOUT @environment:production
  • 일정 기간 내 ETIMEDOUT 발생 빈도가 임계값을 초과하면 알림 설정 (예: 1시간 내 5건 이상 시 경고)

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 단발성 네트워크 이슈이며 SQS 재시도로 자동 복구됨. timeout 설정 추가는 간단한 변경.