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#
- 13:00:31Z — PotreeService가 pointcloud 1046180 처리 시작, 파일 다운로드 개시
- 13:16:00.699Z — 약 15.5분 후
downloadFile에서ETIMEDOUT에러 발생 - 13:16:00.701Z —
getApiErrorToDeleteMessage가 transient system error로 분류, SQS 메시지 삭제하지 않음 - 13:16:00.702Z —
handlingMessageErrors에서 에러 로깅 (이 클러스터의 대표 에러) - 13:30:51Z — SQS visibility timeout 후 2차 시도, 다운로드 및 PotreeConverter 실행 성공
- 13:31:41Z — SQS 메시지 정상 삭제, 처리 완료
Error Log#
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:54—checkingQueue에서 SQS 메시지를 수신하고runByMessages를 호출 potree-service.ts:180—runByMessage에서 개별 메시지 처리 시작potree-service.ts:203—downloadFile호출 (cpPointcloud.downloadUrl,cpPointcloud.originalFilePath)- Failure point:
potree-service.ts:532-570—downloadFile내부에서request.get의 TLS 소켓 read timeout
파일 다운로드를 수행하는 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
}
});
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로 전파된 후의 분기 로직:
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에서:
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));
};
apiErrorObject가 undefined이고 ApproximateReceiveCount(1)가 MaxReceiveCount(10) 미만이므로, SQS 메시지가 삭제되지 않고 visibility timeout 후 재시도된다. 이 설계는 transient network error에 대한 올바른 대응이다.
Log Evidence#
Datadog에서 사용한 쿼리:
service:cupixworks-any-potree-agent status:error @environment:production
에러 직전 다운로드 시도 로그 (13:00:31Z):
PotreeService::runByMessage | id: 1046180
CupixAuth::setSession | session_id: 4c5e46c15248f009a97f1e3857475bbf8afa46f1
PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created
에러 시점의 연속 로그 (13:16:00Z):
[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):
[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 에러 (리전 전반 네트워크 이슈 증거):
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:538의request.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 nativefetch로 마이그레이션하면 timeout, retry, abort 제어가 더 용이해진다.- 다운로드 retry 로직을
downloadFile내부에 추가하여 SQS 레벨 재시도(visibility timeout 대기) 없이도 빠르게 복구할 수 있도록 개선.
Monitoring#
- transient network timeout 빈도를 추적하는 메트릭 추가:
service:cupixworks-any-potree-agent status:error ETIMEDOUT @environment:production
- 일정 기간 내 ETIMEDOUT 발생 빈도가 임계값을 초과하면 알림 설정 (예: 1시간 내 5건 이상 시 경고)
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial — 단발성 네트워크 이슈이며 SQS 재시도로 자동 복구됨. timeout 설정 추가는 간단한 변경.