ES /docs

BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONN

RCA: FloorplanService::downloadFile ECONNRESET while fetching original floorplan

Overview#

What Happened#

cupixworks-any-floorplan-agent(production, us-west-2)가 SQS 메시지 86b02f14-377e-4fa7-9710-24f2de51103e(floorplan id 89294) 를 처리하는 중 원본 PNG 파일을 다운로드하다가 TLS 소켓이 상대방에 의해 끊겨 ECONNRESET (errno -104, syscall: read) 으로 실패했다. 단발성 네트워크 끊김이며, 동일 메시지/같은 floorplan은 후속 redrive 와 다른 메시지에서 정상 처리되었다.

Quick Facts#

Field Value
exception.class Error (Node.js system error)
exception.code ECONNRESET
exception.errno -104
exception.syscall read
top_frame node:internal/stream_base_commons:218:20 (TLSWrap.onStreamRead)
failure point packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:267-270
sqs.message_id 86b02f14-377e-4fa7-9710-24f2de51103e
sqs.receive_count 4 (재시도 중)
floorplan.id 89294
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
chrischoi / floorplan-agent 1 메시지 (errno=-104 발생 2건이 14일 창에 산발) 단일 floorplan(89294) 처리 한 번 실패 — 같은 메시지가 SQS visibility timeout 후 재전달되어 후속 처리에서 성공. 사용자 가시 영향 없음.

Timeline#

  1. 2026-06-25 00:01:13 KSTFloorplanService::downloadFile/tmp/workspace/89294/89294.png 다운로드 중 ECONNRESET 발생 (request.get(...).on('error', ...) 콜백에서 cupixAuth.handleError 호출 → reject)
  2. 2026-06-25 00:01:13 KSTBaseService::handlingMessageErrors 가 errno/code/syscall 을 가진 시스템 에러로 인식, 메시지를 SQS 에서 삭제하지 않고 종료
  3. 2026-06-25 02:55 KST 무렵 (이후) — 같은 floorplan id 89294 가 다른 메시지로 재처리되어 정상 완료 (tileFloorplan | tiling done 로그 확인)
  4. 2026-06-25 동일 일자 17:55, 19:56, 21:57 KSTid: 89294 추가 처리 모두 성공 (재발 없음)

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"86b02f14-377e-4fa7-9710-24f2de51103e","Attributes":{"ApproximateReceiveCount":"4"}}}

Impact#

  • Service: cupixworks-any-floorplan-agent
  • Team: chrischoi
  • 발생 횟수: 1
  • 최초 발생: 2026-06-25 00:01:13 KST
  • 최근 발생: 2026-06-25 00:01:13 KST

Root Cause Summary#

FloorplanService.downloadFileWithHeader 가 원본 floorplan PNG 를 HTTPS GET 으로 받는 도중 상대 측(스토리지/CDN) 이 TLS 연결을 끊어 Node.js 가 ECONNRESET (errno=-104, syscall=read) 시스템 에러를 throw 했다. 이는 코드 결함이 아니라 외부 의존성(원격 호스트 또는 그 사이의 네트워크 장비)의 단발성 연결 종료다. 코드 측 안전망은 이미 의도대로 동작했다 — BaseService.getApiErrorToDeleteMessageerrno/code/syscall 시그니처로 이를 "nodejs common system error" 로 분류해 undefined 를 반환했고, 그 결과 SQS 메시지를 삭제하지 않아 visibility timeout 후 재전달되었으며 후속 처리에서 성공했다. 다만 이 경로에는 애플리케이션 레벨 재시도가 없어, ECONNRESET 한 번에 한 개 메시지가 한 번 실패 처리되고 SQS 재전달에 의존한다.

Technical Analysis#

Code Path#

  • Entry point: packages/base/src/base-service.ts:107-110runByMessages 가 던진 에러를 handlingMessageErrors 로 위임
  • Download issue: packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:76-80downloadFileWithHeader(downloadUrl, originalFilePath, headers) 호출
  • Failure point: packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:267-270request.get(...).on('error', ...) 콜백이 JSON.stringify(err) 로 로그 후 cupixAuth.handleError(err) 결과를 reject
  • System-error short-circuit: packages/base/src/base-service.ts:245-248errno/code/syscall 트리오면 즉시 return → 메시지 삭제 안 함 → SQS 재전달 의존
packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:237-270typescript
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
                }
            });

            fileStream
                .on('finish', async () => { /* ...resolve */ });

            sendReq
                .on('response', res => {
                    if (res.statusCode !== 200) { reject(cupixAuth.handleError(res)); }
                    else { sendReq.pipe(fileStream); }
                })
                .on('error', err => {
                    logger.error('FloorplanService::downloadFile | path: %s, error: %s', path, JSON.stringify(err));
                    reject(cupixAuth.handleError(err));
                });
        })
packages/base/src/base-service.ts:240-276typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) { /* ... */ }
    if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | nodejs common system error', error);
        return;  // ECONNRESET → return undefined → message NOT deleted
    }
    // ...statusCode 4xx/5xx 일 때만 errorMsg 반환
};
packages/base/src/base-service.ts:107-115typescript
try {
    await this.runByMessages();
} catch (error) {
    await this.handlingMessageErrors(error);  // 에러 1회 로그 후 메시지는 삭제하지 않음
}
this.resetMessages();

기대 동작: 일시적 네트워크 끊김 → SQS visibility timeout 후 재전달 → 다른 처리에서 성공. 실제 동작: 동일하게 진행됨. 사용자 가시 영향 없음 (id: 89294 가 같은 날 4회 이상 정상 처리됨이 info 로그로 확인됨).

Log Evidence#

Datadog query (재현용):

text
service:cupixworks-any-floorplan-agent ECONNRESET

핵심 로그 (시간순, KST):

text
2026-06-25 00:01:13  warn  read ECONNRESET
2026-06-25 00:01:13  warn  CupixAuth::handleError | Undefined response: {"stack":"Error: read ECONNRESET\n    at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\n    at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17)","message":"read ECONNRESET","errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-25 00:01:13  error FloorplanService::downloadFile | path: /tmp/workspace/89294/89294.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-25 00:01:13  error BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"86b02f14-377e-4fa7-9710-24f2de51103e","Attributes":{"ApproximateReceiveCount":"4"}}}

같은 floorplan id 89294 가 같은 날 후속 처리에서 정상 완료된 증거 (Datadog query: service:cupixworks-any-floorplan-agent "89294"):

text
2026-06-25 02:00:34  info  FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-25 01:59:28  info  BaseService::runByMessage | id: 89294
2026-06-24 21:58:29  info  FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-24 19:57:40  info  FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`
2026-06-24 17:56:35  info  FloorplanService::tileFloorplan | tiling done and now save to `/tmp/workspace/89294/tile`

빈도(14일 retention 창, service:cupixworks-any-floorplan-agent ECONNRESET status:error):

text
2026-06-25 00:01:13  (89294)
2026-06-23 11:19:09  (89075)

→ 14일 동안 2회의 단발성 발생. 시스템적인 패턴 아님.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 원격 스토리지/CDN 또는 중간 네트워크 장비가 idle/유휴 TLS 소켓을 RST 로 종료 (transient) errno=-104, code=ECONNRESET, syscall=read; stack TLSWrap.onStreamRead; CupixAuth::handleError 의 Undefined response (HTTP 응답 없는 read 시점 에러); 14일 창에 2회만 산발; 같은 id:89294 가 같은 날 4회 이상 성공 Confirmed
H2 Floorplan 89294 자체의 영구적 데이터 문제 메시지 ApproximateReceiveCount=4 같은 id 가 동일 일자에 17:56, 19:57, 21:58 KST, 다음날 02:00 KST 까지 tileFloorplan tiling done 로그로 정상 처리 확인 Rejected
H3 인증 토큰 만료/회전이 read 중 끊김을 유발 CupixAuth::handleError 가 로그에 등장 handleError 는 단순 패스스루 로깅(file cupix-auth.ts:42-57)이며 토큰 갱신 호출 없음. 또한 checkToken 은 이미 downloadFileWithHeader 시작 시점에 통과한 상태에서 실패. errno/syscall 시그니처는 인증 실패가 아닌 소켓 read 단계의 OS 레벨 에러. Rejected
H4 디스크 가득 또는 fs.createWriteStream 실패 에러는 sendReq.on('error') 의 read ECONNRESET 으로, fileStream 'error' 가 아님. fileStream finish 도 호출되지 않은 상태. Rejected
H5 외부 의존성/리전 단위 광범위 장애 status-board: svc:cupixworks-any-floorplan-agent::unknown 의 active=null, recent=[]. 다른 서비스에서 동시각 ECONNRESET 폭증 증거 없음. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 단발성 외부 네트워크 장애이며 SQS redrive 로 자체 복구된다. 코드 변경 불필요.
  • 다만 모니터링 임계치(아래 Monitoring 섹션) 만 점검할 것.

단기 개선 (1주 이내)#

  • 로그 레벨 조정 검토: packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:268logger.errorpackages/base/src/base-service.ts:311 의 최종 logger.error 는 시스템 errno 기반 transient 케이스에서 함께 트리거되어 RCA 노이즈를 발생시킨다. getApiErrorToDeleteMessage 가 "nodejs common system error" 로 분류한 경우(errno/code/syscall 트리오) 는 handlingMessageErrors 의 최종 라인을 logger.warn 으로 다운그레이드 검토. 메모리 노트의 "Cross-region tokens, rate limits, and transient network issues may warrant warn-level logging" 패턴과 일치.
  • 다운로드 단계 in-process 재시도: downloadFileWithHeadercupixAuth.retryable 와 동일한 패턴(지수 백오프, 최대 N회) 의 ECONNRESET/ETIMEDOUT 한정 재시도를 추가 검토. 현재는 SQS visibility timeout(10분, QueueVisibilityTimeout = 600) 을 기다린 뒤에야 재처리되므로 사용자 체감 latency 가 길어진다.

장기 개선 (재발 방지)#

  • HTTPS keep-alive idle timeout 튜닝: 원본 다운로드에 사용하는 request 인스턴스의 keep-alive idle 시간을 원격 호스트(스토리지/CDN) 의 idle close 보다 짧게 설정해 RST 가 발생하는 idle window 자체를 회피.
  • request (deprecated) → axios/undici/got 등으로의 단계적 마이그레이션. 현재 코드 베이스에는 이미 axios 관습이 있으므로(cupix-auth.ts 응답 호환 분기) 통일 검토.

Monitoring#

  • 추가/유지할 메트릭/알림:
    • 다운로드 ECONNRESET 시계열(이상 급증 감지)
    • 서비스 status:error 트렌드
  • Datadog timeseries 쿼리 (release dashboard widget 호환 — writing-datadog-monitoring-queries 가이드 적용):
text
sum:datadog.estimated_usage.logs.ingested_events{service:cupixworks-any-floorplan-agent,status:error}.as_count()
text
logs("service:cupixworks-any-floorplan-agent ECONNRESET").index("*").rollup("count").by("status")
text
logs("service:cupixworks-any-floorplan-agent FloorplanService::downloadFile error").index("*").rollup("count")

알림 임계: 시간당 ECONNRESET 5회 초과 또는 24시간 누적 20회 초과 시 page (현재 14일 2회 대비 충분한 헤드룸).

Risk Assessment#

  • Risk level: low — 단발성 외부 네트워크, 자체 복구 메커니즘(SQS redrive) 정상 동작, 사용자 가시 영향 없음 확인.
  • 예상 복잡도: trivial (코드 변경 시) — 로그 레벨 다운그레이드 1라인 또는 다운로드 재시도 래퍼.