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#
- 2026-04-21 13:00:31 UTC —
PotreeService::runByMessage시작 (pointcloud ID 1046180, potree_state: created) - 2026-04-21 13:16:00 UTC —
PotreeService::downloadFileETIMEDOUT 에러 발생 (다운로드 약 16분 소요 후 실패) - 2026-04-21 13:16:00 UTC —
handlingMessageErrors에서 ETIMEDOUT을 system error로 인식, SQS 메시지 삭제하지 않음 (retry 허용) - 2026-04-21 13:30:51 UTC — SQS visibility timeout 만료 후 재처리 시작 (retry)
- 2026-04-21 13:31:40 UTC —
cleanUpAnythingRelatedModel완료 (약 49초 — 정상 처리 또는 이미 처리된 상태로 스킵)
Error Log#
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:180—runByMessage에서 SQS 메시지를 처리 potree-service.ts:203—downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath)호출- Failure point:
potree-service.ts:532-570—downloadFile메서드
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):
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 허용)
}
// ...
};
getApiErrorToDeleteMessage는 errno, code, syscall 속성이 모두 있는 에러를 "nodejs common system error"로 인식하고 undefined를 반환한다. 이 경우 handlingMessageErrors에서 apiErrorObject가 undefined이고, ApproximateReceiveCount가 1이므로 (MaxReceiveCount 10 미만) SQS 메시지를 삭제하지 않아 retry가 가능하다.
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#
첫 번째 시도 (실패):
Datadog 쿼리: service:cupixworks-any-potree-agent "1046180"
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, 성공):
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):
Datadog 쿼리: service:cupixworks-any-potree-agent status:error "ETIMEDOUT" (2026-04-14 ~ 2026-04-21)
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:562의downloadFileerror 로그를error에서warn으로 변경하고,potree-service.ts:523의handlingMessageErrors도 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#
- 다운로드 타임아웃 빈도 추적:
service:cupixworks-any-potree-agent status:error "ETIMEDOUT" "downloadFile"
- retry 후 성공/실패 비율 추적 (ApproximateReceiveCount 기반):
service:cupixworks-any-potree-agent "handlingMessageErrors" "ApproximateReceiveCount"
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial
- 근거: 일시적 네트워크 문제이며, SQS retry 메커니즘이 정상 작동하여 자동 복구된다. 7일간 약 10건으로 빈도가 낮고, 최종 사용자에게 영구적 영향은 없다. 주요 개선은 에러 레벨 조정과 timeout 설정이며, 코드 변경 범위가 작다.