ES /docs

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의 cupixworks-any-floorplan-agent에서 floorplan 원본 파일 다운로드가 두 건 연속으로 ETIMEDOUT (errno -110, syscall: read)으로 실패했다. 두 메시지(floorplan id 88414, 88454)는 SQS에서 받은 뒤 약 15분 4347초 동안 응답을 받지 못하고 OS 레벨 TCP read timeout이 발동하여 끊겼고, 결과적으로 두 floorplan의 처리가 실패 상태로 종료되었다.

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#

  1. 2026-06-18 18:25:35 KST — SQS 메시지 수신, BaseService::runByMessage | id: 88414
  2. 2026-06-18 18:26:49 KST — SQS 메시지 수신, BaseService::runByMessage | id: 88454
  3. 2026-06-18 18:41:18 KST — id 88414 download read socket timeout, FloorplanService::downloadFile error 발생 (메시지 수신 후 약 15분 43초 경과)
  4. 2026-06-18 18:42:36 KST — id 88454 download read socket timeout, FloorplanService::downloadFile error 발생 (약 15분 47초 경과)
  5. 2026-06-18 18:41:18 / 18:42:36 KSTBaseService::handlingMessageErrors가 두 메시지 모두 error로 종결 처리

Error Log#

Datadog Logs

text
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::runByMessageFloorplanService::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-270request.get(...)error 이벤트로 ETIMEDOUT을 emit, 로그 한 줄 후 reject.
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:237-276typescript
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 쿼리:

text
service:cupixworks-any-floorplan-agent status:error "FloorplanService::downloadFile"
text
service:cupixworks-any-floorplan-agent (88414 OR 88454)

핵심 로그 (KST):

text
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 없음. 메시지 수신 → 에러까지 두 건 모두 정확히 15분 4347초. errno -110 + syscall: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:244 request.get(url, { ... }) 옵션에 timeout (request body 응답 활동 timeout, ms)을 명시한다. 권장 값은 30~60초 수준에서 시작(정상 다운로드의 p99 시간을 측정한 뒤 그 위 여유로 결정).
  • 또는 origin/develop의 TSLA-13233 흐름(@agents/baseuploadFile 도입)을 그대로 이어, @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-307 uploadFile) 도 동일 검토.

장기 개선 (재발 방지)#

  • 사내 공통 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에 그대로 사용 가능):
text
logs("service:cupixworks-any-floorplan-agent status:error \"ETIMEDOUT\"").index("*").rollup("count").by("service").last("1d")
text
logs("service:cupixworks-any-floorplan-agent \"FloorplanService::downloadFile\" status:error").index("*").rollup("count").by("service").last("1d")
  • 다운로드 처리 시간 분포(p50/p95/p99)를 측정하기 위한 로그/메트릭 추가 (현재는 start / complete debug 로그만 존재 → 시간 차이 계산 가능하나 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.tsuploadFile@agents/base.uploadFile로 마이그레이션함. origin/developfloorplan-service.ts:237-275 downloadFileWithHeadermaster와 동일하게 request.get(url, { headers }) 호출에 timeout 옵션이 여전히 없음. 즉 본 클러스터의 실패 지점(floorplan-service.ts:268, downloadFile 경로)에는 코드 변경이 들어가지 않았으므로 develop 머지/배포만으로는 ETIMEDOUT 워커 슬롯 점유 문제가 그대로 재현될 수 있다.
(부수 발견) develop에 도입된 @agents/base.downloadFile 헬퍼 활용 가능성 수용 (Fix Recommendation에 반영) origin/developapplications/agents/packages/base/src/util/transfer.tsdownloadFile이 이미 존재 (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, branch develop @ 188fb1c93)
  • 확인 커밋: 567dd4802 (TSLA-13233, applications/agents/packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts +14/-28). develop 전체 floorplan-agent 관련 커밋 7건 중 download 경로를 건드린 것은 없음 (bc539bc66/fb1ea71d2 gm convert stderr 로깅, f80ad2ace ghostscript 버전 bump 등은 본 이슈와 무관).
  • 비교: git show origin/master:.../floorplan-service.ts vs git show origin/develop:.../floorplan-service.tsdownloadFileWithHeader 본문 동일.
  • 신규 참조: applications/agents/packages/base/src/util/transfer.ts (develop), downloadFile / uploadFile / RETRYABLE_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'EAI_AGAIN'].