FloorplanService::downloadFile | path: /tmp/workspace/88414/88414.jpg, error: {"errno":-110,"code":"
RCA: FloorplanService::downloadFile ETIMEDOUT (errno -110, syscall read)
Overview#
What Happened#
2026-06-18 18:4118:42 KST 사이 us-west-2의 47초 동안 응답을 받지 못하고 OS 레벨 TCP read timeout이 발동하여 끊겼고, 결과적으로 두 floorplan의 처리가 실패 상태로 종료되었다.cupixworks-any-floorplan-agent에서 floorplan 원본 파일 다운로드가 두 건 연속으로 ETIMEDOUT (errno -110, syscall: read)으로 실패했다. 두 메시지(floorplan id 88414, 88454)는 SQS에서 받은 뒤 약 15분 43
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error (request 라이브러리 socket error) |
| exception.message | {"errno":-110,"code":"ETIMEDOUT","syscall":"read"} |
| top_frame | packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:268 |
| runtime | Node.js (request@^2.88.2) |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| chrischoi / floorplan-agent | 2 | floorplan id 88414, 88454 처리 실패 (state → Error) |
Timeline#
- 2026-06-18 18:25:35 KST — SQS 메시지 수신,
BaseService::runByMessage | id: 88414 - 2026-06-18 18:26:49 KST — SQS 메시지 수신,
BaseService::runByMessage | id: 88454 - 2026-06-18 18:41:18 KST — id 88414 download read socket timeout,
FloorplanService::downloadFileerror 발생 (메시지 수신 후 약 15분 43초 경과) - 2026-06-18 18:42:36 KST — id 88454 download read socket timeout,
FloorplanService::downloadFileerror 발생 (약 15분 47초 경과) - 2026-06-18 18:41:18 / 18:42:36 KST —
BaseService::handlingMessageErrors가 두 메시지 모두 error로 종결 처리
Error Log#
FloorplanService::downloadFile | path: /tmp/workspace/88414/88414.jpg, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
Impact#
- Service:
cupixworks-any-floorplan-agent - Team: chrischoi
- 발생 횟수: 2
- 최초 발생: 2026-06-18 18:41:18 KST
- 최근 발생: 2026-06-18 18:42:36 KST
Root Cause Summary#
FloorplanService::downloadFileWithHeader가 사용하는 request@^2.88.2 라이브러리 호출에 read/socket timeout 옵션이 설정되어 있지 않다. 원본 파일을 받을 원격 서버(presigned URL)가 응답을 보내다 멈추거나 idle 상태가 되면, Node 프로세스는 OS 커널의 기본 TCP read timeout(Linux 기본값 약 15분, tcp_retries2/keepalive 동작에 의존)이 발동할 때까지 소켓을 점유한 채 대기한다. 두 사례 모두 SQS 수신 시각으로부터 정확히 15분 43~47초 후에 errno -110 (ETIMEDOUT) syscall read로 끊긴 점이 이 시나리오와 일치한다. 즉 직접적 원인은 원본 다운로드 소스(또는 그 사이 네트워크 경로)의 응답 지연/스트림 정체이며, 근본 원인은 다운로드 코드에 명시적 timeout이 없어 일시적 네트워크 정체가 워커 슬롯을 15분 이상 묶고 결국 실패로 귀결된다는 점이다.
Technical Analysis#
Code Path#
- Entry: SQS 메시지 수신 →
BaseService::runByMessage→FloorplanService::run(packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:51) - 다운로드 호출:
floorplan-service.ts:76-80(downloadFileWithHeader(downloadUrl, originalFilePath, headers)) - Failure point:
floorplan-service.ts:267-270—request.get(...)가error이벤트로 ETIMEDOUT을 emit, 로그 한 줄 후 reject.
private downloadFileWithHeader = (url: string, path: string, headers?: any): Promise<void> => new Promise((resolve, reject) => {
this.setErrorCode(ErrorCode.Agent.FailedToDownloadOriginalFiles);
const cupixAuth = this.cupixAuth;
cupixAuth.checkToken()
.then(() => {
logger.debug('FloorplanService::downloadFile | start path: %s, url: %s', path, url);
const fileStream = fs.createWriteStream(path);
const sendReq = request.get(url, {
headers: headers != undefined ? headers : {
'X-CUPIX-AUTH': cupixAuth.accessToken
}
});
// ... .on('response') pipes to fileStream
sendReq
.on('error', err => {
logger.error('FloorplanService::downloadFile | path: %s, error: %s', path, JSON.stringify(err));
reject(cupixAuth.handleError(err));
});
})
request.get 옵션 객체에는 timeout 키가 없다. 결과적으로 read/socket idle timeout은 라이브러리 차원에서 무제한이며, 소켓이 끊기는 시점은 OS 커널 TCP retransmit/keepalive 메커니즘에 의존한다. Linux 기본 tcp_retries2 = 15이면 약 13~30분 사이에 ETIMEDOUT이 발생하며, 본 사례의 ~15분 43초는 이 범위에 정확히 들어간다.
기대 동작 vs 실제 동작:
- 기대: 원격 서버가 응답을 일정 시간(예: 30~60초) 안에 보내지 못하면 빠르게 실패하고 SQS 재시도 또는 에러 처리.
- 실제: 응답 데이터 스트림이 멈추면 ~15분 동안 워커가 점유되었다가 OS 레벨에서 강제로 끊겨 실패.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-floorplan-agent status:error "FloorplanService::downloadFile"
service:cupixworks-any-floorplan-agent (88414 OR 88454)
핵심 로그 (KST):
2026-06-18 18:25:35 info BaseService::runByMessage | id: 88414
2026-06-18 18:26:49 info BaseService::runByMessage | id: 88454
2026-06-18 18:41:18 error FloorplanService::downloadFile | path: /tmp/workspace/88414/88414.jpg, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-06-18 18:41:18 error BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"0c3a5a16-df27-4615-af4d-1c91b4eb36c0","Attributes":{"ApproximateReceiveCount":"1"}}}
2026-06-18 18:42:36 error FloorplanService::downloadFile | path: /tmp/workspace/88454/88454.png, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-06-18 18:42:36 error BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"0c270c0e-854a-4237-b2aa-a375a4fbabc6","Attributes":{"ApproximateReceiveCount":"1"}}}
2026-06-18 18:42:36 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/88454
지난 7일간 동일 service에서 다른 에러 패턴은 "HTTP request failed", "Input file contains unsupported image format", 403/Floorplan not found 등이며 ETIMEDOUT은 본 두 건이 처음 (uncertain — 14일 retention 한계 안에서만 확인됨).
추가로 확인하지 않은 항목:
- 원격 다운로드 소스 측 (presigned S3/외부 호스트) 의 트래픽/응답 지연 메트릭은 본 RCA에서 직접 조회하지 않음. 두 건이 87초 간격으로 거의 동일 패턴이므로 일시적 네트워크/원격 호스트 지연 가능성이 높지만, 단일 endpoint 정체 vs ECS 태스크의 nat/egress 정체 가능성 모두 가설 단계 — needs verification via flow logs / VPC NAT metrics.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | request.get에 timeout 미지정으로 인해 idle TCP socket이 OS 레벨 read timeout(~15분)까지 대기 후 ETIMEDOUT으로 종료 |
floorplan-service.ts:244 request.get 옵션에 timeout 없음. 메시지 수신 → 에러까지 두 건 모두 정확히 read는 커널 TCP read timeout 시그니처 |
— | Confirmed (코드 + 로그 타임라인 일치) |
| H2 | 원본 파일 download URL host가 응답 자체를 못 했다 (DNS/연결 실패) | — | errno는 ECONNREFUSED/ENOTFOUND가 아니라 ETIMEDOUT syscall:read. 즉 TCP는 연결되었고 응답 도중 정체 |
Rejected |
| H3 | SQS visibility timeout 만료로 메시지가 재배달되어 워커가 동시에 실행되며 충돌 | infra sqs_visibility_timeout_seconds = 7200 (2h). ApproximateReceiveCount: 1 두 건 모두 첫 수신 |
재배달 흔적 없음 | Rejected |
| H4 | 같은 인시던트와 연관된 외부 의존성 장애 (s3, etc.) | 87초 간격 두 건이 동일 패턴 | status-board for-cluster 결과 active/recent incident 없음. 다른 서비스에서 동시간대 ETIMEDOUT 클러스터 미관측 |
Inconclusive (외부 호스트 단독 일시 지연 가능성 여전히 존재) |
Fix Recommendation#
즉시 조치 (Critical)#
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:244request.get(url, { ... })옵션에timeout(request body 응답 활동 timeout, ms)을 명시한다. 권장 값은 30~60초 수준에서 시작(정상 다운로드의 p99 시간을 측정한 뒤 그 위 여유로 결정).- 또는
origin/develop의 TSLA-13233 흐름(@agents/base의uploadFile도입)을 그대로 이어,@agents/base/util/transfer.ts에 이미 존재하는downloadFile(axios +REQUEST_TIMEOUT_MS = 60000+ ETIMEDOUT/5xx 자동 재시도)을downloadFileWithHeader자리에 채택한다. 동일 패턴으로@agents/base헬퍼만 import해 호출하면 timeout/재시도 정책이 한 번에 적용된다 — 단,downloadFileWithHeader가 사용하는cupixAuth.checkToken()/cupixAuth.handleError()흐름은 wrapper에서 유지해야 한다. - 근거: 본 클러스터의 직접 원인은 다운로드 호출이 OS TCP timeout까지 대기하며 워커를 점유하는 점이며, 라이브러리 timeout 한 줄 추가로 즉시 차단 가능. develop의 TSLA-13233 변경은 동일 파일의
uploadFile만@agents/base.uploadFile로 마이그레이션했고downloadFileWithHeader는 손대지 않았기 때문에 develop 머지만으로는 본 이슈가 해소되지 않는다 (Revision 1 참고).
단기 개선 (1주 이내)#
request라이브러리의 idle/socket timeout이 활동 기반(activity-based)임에 유의해 unit test 또는 통합 테스트로 stub server를 둔 timeout 동작을 검증.BaseService::handlingMessageErrors에서 ETIMEDOUT/네트워크성 에러는 별도 분류해 SQS DLQ 정책 또는 재시도 횟수 정책을 분리 적용 (현재는 단일 처리). 무조건 재시도하면 동일 host 정체 시 다시 15분 이상 점유 위험이 있음.cupix-tesla-floorplan-agent의 다른 다운로드/업로드 호출 (floorplan-service.ts:278-307uploadFile) 도 동일 검토.
장기 개선 (재발 방지)#
- 사내 공통 HTTP client wrapper를 도입해 모든 agent 패키지가 동일한 connection/socket timeout, retry, 회로 차단 정책을 공유하도록 표준화.
request는 deprecated 상태이므로undici또는axios(timeout/abort 신호 지원) 로의 마이그레이션을 검토. - 다운로드/업로드 stream에 progress watchdog (예: N초간 byte 미수신 시 abort) 도입.
Monitoring#
- 추가할 알림: floorplan-agent에서 ETIMEDOUT/
syscall":"read"/errno":-110발생 횟수. 5분 윈도우에서 ≥3건이면 경고. - Datadog timeseries 쿼리 (release dashboard에 그대로 사용 가능):
logs("service:cupixworks-any-floorplan-agent status:error \"ETIMEDOUT\"").index("*").rollup("count").by("service").last("1d")
logs("service:cupixworks-any-floorplan-agent \"FloorplanService::downloadFile\" status:error").index("*").rollup("count").by("service").last("1d")
- 다운로드 처리 시간 분포(p50/p95/p99)를 측정하기 위한 로그/메트릭 추가 (현재는
start/completedebug 로그만 존재 → 시간 차이 계산 가능하나 metric화 권장).
Risk Assessment#
- Risk level: medium — 발생 빈도는 낮으나 한 번 발생 시 워커 슬롯을 15분 이상 점유하여 처리 지연 영향. 비즈니스 영향은 누락된 floorplan id 두 건의 재처리 필요.
- 예상 복잡도: trivial (timeout 옵션 한 줄 추가). 단기 개선까지 포함하면 standard.
Revision History#
Revision 1#
Feedback: origin/develop의 변경사항으로 해소되는 이슈인지 확인 요청.
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| origin/develop의 변경사항으로 본 이슈가 해소되는가 | 거부 (해소되지 않음) | git log origin/master..origin/develop -- applications/agents/packages/cupix-tesla-floorplan-agent/ 로 확인한 결과, 유일하게 다운/업로드 코드를 건드린 커밋은 TSLA-13233 (567dd4802)이며, 해당 커밋은 floorplan-service.ts의 uploadFile만 @agents/base.uploadFile로 마이그레이션함. origin/develop의 floorplan-service.ts:237-275 downloadFileWithHeader는 master와 동일하게 request.get(url, { headers }) 호출에 timeout 옵션이 여전히 없음. 즉 본 클러스터의 실패 지점(floorplan-service.ts:268, downloadFile 경로)에는 코드 변경이 들어가지 않았으므로 develop 머지/배포만으로는 ETIMEDOUT 워커 슬롯 점유 문제가 그대로 재현될 수 있다. |
(부수 발견) develop에 도입된 @agents/base.downloadFile 헬퍼 활용 가능성 |
수용 (Fix Recommendation에 반영) | origin/develop의 applications/agents/packages/base/src/util/transfer.ts에 downloadFile이 이미 존재 (REQUEST_TIMEOUT_MS = 60000, ETIMEDOUT/ECONNRESET/5xx 자동 재시도 MAX_RETRIES = 3, exponential backoff). 따라서 TSLA-13233 패턴과 동일하게 downloadFileWithHeader를 @agents/base.downloadFile 호출로 교체하는 것이 한 줄 timeout 추가보다 일관된 즉시 조치 후보로 등장. |
변경 사항:
## Fix Recommendation→### 즉시 조치 (Critical)에 develop 머지가 본 이슈를 해소하지 못한다는 점과, develop에 이미 존재하는@agents/base.downloadFile헬퍼를 활용한 대안 조치를 추가.
추가 조사 내용:
- 탐색 레포:
cupixworks($REPOS_DIR/cupixworks, branchdevelop@188fb1c93) - 확인 커밋:
567dd4802(TSLA-13233,applications/agents/packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts+14/-28). develop 전체 floorplan-agent 관련 커밋 7건 중 download 경로를 건드린 것은 없음 (bc539bc66/fb1ea71d2gm convert stderr 로깅,f80ad2aceghostscript 버전 bump 등은 본 이슈와 무관). - 비교:
git show origin/master:.../floorplan-service.tsvsgit show origin/develop:.../floorplan-service.ts—downloadFileWithHeader본문 동일. - 신규 참조:
applications/agents/packages/base/src/util/transfer.ts(develop),downloadFile/uploadFile/RETRYABLE_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'EAI_AGAIN'].