ES /docs

BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIME

RCA: BaseService::handlingMessageErrors ETIMEDOUT during SQS message processing

Overview#

What Happened#

2026-06-24 23:13 KST에 cupixworks-capture-postprocessor-agent가 SQS 메시지(MessageId: c5b275f1-...) 처리 중 Tesla API 호출에서 read ETIMEDOUT (errno -110)을 만나 BaseService::handlingMessageErrors 에러를 기록했다. 16분 뒤 같은 서비스의 다른 워커에서 동일한 errno/code/syscall 형태의 시스템 레벨 네트워크 오류가 한 번 더 발생했고, 두 케이스 모두 메시지가 즉시 삭제되지 않고 SQS 재시도 큐로 돌아갔다.

Quick Facts#

Field Value
exception.class BaseService::handlingMessageErrors
exception.message read ETIMEDOUT (Node.js system error, errno -110, syscall: read)
top_frame packages/base/src/base-service.ts:311
underlying_origin CupixAuth::handleError (packages/api/src/authentication/cupix-auth.ts:54) — TLS read on Tesla API auth call
env production, regions us-west-2, eu-central-1
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
gad / capture-postprocessor pipeline 2 두 개의 SQS 메시지가 즉시 삭제되지 않고 재시도 큐로 환류. MessageId c5b275f1 은 약 45 분 후 ApproximateReceiveCount: 2 로 재처리되었으나 여전히 undefined response 로 실패

Timeline#

  1. 2026-06-24 23:13:04 KSTMessageId c5b275f1-1081-43fa-99a8-51de8c74fe44 처리 중 Tesla API auth 호출에서 read ETIMEDOUT 발생. CupixAuth::handleError | Undefined response warn → BaseService::handlingMessageErrors error (ApproximateReceiveCount=1).
  2. 2026-06-24 23:18:44 KST — 같은 서비스의 다른 메시지(2e2194ec-...)가 Cannot read properties of undefined (reading 'response') 로 실패 (별개 패턴, 같은 인시던트 윈도).
  3. 2026-06-24 23:29:46 KST — 두 번째 시스템 에러: MessageId 7ae02df1-28dd-4c16-9b69-dc7d937dd817, errno -104 ECONNRESET, syscall: read (cluster의 두 번째 occurrence).
  4. 2026-06-24 23:57:51 KSTc5b275f1 메시지가 ApproximateReceiveCount: 2 로 재처리되었고 이번엔 undefined response 로 다시 실패 후 삭제됨.

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"c5b275f1-1081-43fa-99a8-51de8c74fe44","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-capture-postprocessor-agent
  • Team: gad
  • 발생 횟수: 2
  • 최초 발생: 2026-06-24 23:13:04 KST
  • 최근 발생: 2026-06-24 23:29:46 KST

Root Cause Summary#

BaseService.runByMessage 가 SQS 메시지 본문을 처리하기 전에 cupixAuth.startSession() 으로 Tesla API 에 인증 세션을 연다. 이 호출의 TLS 응답을 기다리던 중 underlying socket 이 timeout (errno -110 ETIMEDOUT) 또는 reset (errno -104 ECONNRESET) 으로 끊겼고, 에러 객체가 { errno, code, syscall } 만 가진 Node.js system error 형태였다. BaseService.getApiErrorToDeleteMessage 는 이 형태를 nodejs common system error 로 분기하여 undefined 를 반환하고(base-service.ts:245-247), receiveCount=1 < MaxReceiveCount=10 이므로 메시지를 삭제하지 않고 SQS 가시성 타임아웃 후 재시도되도록 둔다. 즉 코드 동작 자체는 설계대로 retry-by-redelivery 였고, error 로그는 single-attempt timeout 을 그대로 표면화한 것이다.

Technical Analysis#

Code Path#

  • Entry point: packages/base/src/base-service.ts:153runByMessage
  • Auth step: packages/base/src/base-service.ts:200cupixAuth.startSession()
  • Underlying error site: packages/api/src/authentication/cupix-auth.ts:42-57CupixAuth.handleErrorresponse == undefined 분기로 Undefined response: ... warn 출력 후 원본 system error 를 그대로 throw
  • Outer catch: packages/base/src/base-service.ts:108-111runByMessages 의 try/catch 가 에러를 handlingMessageErrors 로 위임
  • Failure logging: packages/base/src/base-service.ts:290-313handlingMessageErrors
packages/base/src/base-service.ts:107-115typescript
try {
    await this.runByMessages();
} catch (error) {
    await this.handlingMessageErrors(error);
}
this.resetMessages();
await CPUtils.sleep(500);
await this.checkingQueue();
packages/base/src/base-service.ts:163-185typescript
try {
    await TraceUtils.activateSpan(span, async () => {
        this.setLogMeta(msgObject);
        logger.info('BaseService::runByMessage | id: %d', targetId);

        await this.authenticateByMessage(msgObject);
        await this.run(targetId, msgObject);
        ...
        await this.deleteByMessage(message);
        ...
    });
    TraceUtils.finishSpan(span, true);
} catch (error) {
    TraceUtils.finishSpan(span, false, error);
    throw error;
}

getApiErrorToDeleteMessage 가 system error 를 의도적으로 "삭제 사유 아님" 으로 분기하는 부분:

packages/base/src/base-service.ts:240-276typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | undefined error');
        return 'undefined error';
    }
    if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | nodejs common system error', error);
        return;
    }
    const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
    if (response == undefined) {
        logger.warn('BaseService::getApiErrorToDeleteMessage | undefined response', error);
        return 'undefined response';
    }
    ...
    if (statusCode != undefined && statusCode >= 400 && statusCode <= 500) {
        if (statusCode === 401) return;
        return errorMsg;
    }
    return;
};

handlingMessageErrors 가 error 를 status 로 기록하는 분기:

packages/base/src/base-service.ts:290-312typescript
private handlingMessageErrors = async (error: any): Promise<void> => {
    const errorAndMessage = {
        error: error,
        sqsMessage: {}
    };
    if (this.messageInProcess) {
        errorAndMessage.sqsMessage = {
            MessageId: this.messageInProcess.MessageId,
            Attributes: this.messageInProcess.Attributes
        };
        const apiErrorObject = this.getApiErrorToDeleteMessage(error);
        if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
            try {
                errorAndMessage.error = apiErrorObject;
                await this.deleteByMessage(this.messageInProcess);
                if (this._modelInProcess != undefined && this._modelInProcess.id > 0) await this.updateErrorState(this._modelInProcess);
            } catch (error) {
                logger.error('BaseService::handlingMessageErrors | Errors in error handling', error);
            }
        }
    }
    logger.error('BaseService::handlingMessageErrors | Error and message object - %s', JSON.stringify(errorAndMessage));
    resetLogMeta();
};

checkReceiveCountToDeleteMessage 의 임계값:

packages/base/src/base-service.ts:278-288typescript
private checkReceiveCountToDeleteMessage = (): boolean => {
    const receiveCount = this.messageInProcess?.Attributes?.ApproximateReceiveCount != undefined
        ? Number(this.messageInProcess.Attributes.ApproximateReceiveCount) : undefined;
    logger.debug('BaseService::checkReceiveCountToDeleteMessage | receive count: %d', receiveCount);

    if (receiveCount == undefined) return true;

    if (receiveCount >= Constants.MaxReceiveCount) return true;

    return false;
};
packages/shared-config/src/constants.ts:11typescript
export const MaxReceiveCount = 10;

기대 동작 vs 실제 동작:

  • 기대: 과도기 네트워크 오류는 SQS visibility timeout 후 자연 재시도 → 메시지 본문은 보존, alert 채널은 깨끗.
  • 실제: 코드 동작 자체는 기대대로지만 logger.error('... Error and message object - %s', ...) 가 무조건 error level 로 찍히기 때문에 (base-service.ts:311), 정상 retry path 에 해당하는 system error 까지 Datadog 의 status:error 로 노출되고, error-sweeper 가 이를 cluster 로 잡음.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-capture-postprocessor-agent status:error "BaseService::handlingMessageErrors"
text
service:cupixworks-capture-postprocessor-agent "c5b275f1-1081-43fa-99a8-51de8c74fe44"

원본 로그(시간순, KST):

text
2026-06-24 23:13:04  warn   CupixAuth::handleError | Undefined response: {"stack":"Error: read ETIMEDOUT\n    at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\n    at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17)","message":"read ETIMEDOUT","errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-06-24 23:13:04  warn   read ETIMEDOUT
2026-06-24 23:13:04  error  BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-110,"code":"ETIMEDOUT","syscall":"read"},"sqsMessage":{"MessageId":"c5b275f1-1081-43fa-99a8-51de8c74fe44","Attributes":{"ApproximateReceiveCount":"1"}}}
2026-06-24 23:29:46  error  BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"7ae02df1-28dd-4c16-9b69-dc7d937dd817","Attributes":{"ApproximateReceiveCount":"1"}}}
2026-06-24 23:57:48  info   AwsQueueManager::deleteMessage | end - message id: c5b275f1-1081-43fa-99a8-51de8c74fe44
2026-06-24 23:57:51  error  BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"c5b275f1-1081-43fa-99a8-51de8c74fe44","Attributes":{"ApproximateReceiveCount":"2"}}}

핵심 관찰:

  • 첫 번째 에러(23:13:04)는 TLS read 타임아웃이 stack frame TLSWrap.onStreamRead 까지 남아 있어 Tesla API HTTPS 연결 단계에서 발생. CupixAuth::handleErrorresponse == undefined 분기로 떨어졌으므로 Tesla 서버가 응답을 보내기 전에 클라이언트 측 socket 이 끊겼다.
  • 같은 MessageId c5b275f1 가 약 45 분 뒤(23:57:51) ApproximateReceiveCount: 2 로 재처리되었으나 이번에는 error: "undefined response" 로 실패 후 삭제됨. 즉 SQS 재시도는 정상 동작했고 메시지가 영구 손실되지는 않았다.
  • 두 번째 cluster occurrence(23:29:46)는 MessageId 7ae02df1, errno -104 ECONNRESET 로 다른 메시지지만 동일한 패턴(연결 끊김 → system error → delete 안 함 → SQS 재시도)이다.

Status board#

이 cluster 는 svc:cupixworks-capture-postprocessor-agent::unknown 스코프의 active 인시던트(2026-06-24-svc-cupixworks-capture-postprocessor-agent--unknown-1) 의 일부로, 같은 윈도(23:18:44 KST)에 da9ba310-80c5-4aca-aaf3-b8809bc15022 cluster 가 함께 트리거되었다. 두 cluster 는 서로 다른 패턴이지만(이쪽은 system error, 다른 쪽은 Cannot read properties of undefined (reading 'response')) 같은 outbound HTTP 호출 경로의 partial-failure 윈도를 공유한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Tesla API 호출(cupixAuth.startSession)의 TLS read 가 timeout/reset 되어 Node system error 가 표면화됨 `CupixAuth::handleError Undefined response의 stack 이TLSWrap.onStreamRead; payload 에 errno -110 ETIMEDOUT, syscall: read(그리고 두 번째 occurrence 는errno -104 ECONNRESET); error site cupix-auth.ts:54`
H2 코드 버그가 메시지를 영구 잃게 함 같은 MessageId c5b275f1 가 45 분 후 ApproximateReceiveCount: 2 로 재처리됨 (`AwsQueueManager::deleteMessage end - message id: c5b275f1...23:57:48 KST).getApiErrorToDeleteMessage 가 system error 를 의도적으로 retry path 로 보내는 설계(base-service.ts:245-247`)
H3 외부 종속성 outage (예: Tesla API 전체 장애) 같은 윈도에 또 다른 cluster da9ba310 가 같은 서비스에서 다른 타입의 outbound 실패로 발생 error 가 단 2 회로 산발적이고 region 도 us-west-2/eu-central-1 로 분산됨. status board 의 dep:* 인시던트가 아닌 svc:* 그룹임. 14:13~14:29 사이 대량 실패 추세는 보이지 않음 Inconclusive — 단발성 transient 가능성이 더 큼, 추가 모니터링 필요
H4 handlingMessageErrors 가 실제로 잘못된 동작을 함 로그 레벨이 error 로 찍힘 코드 의도(base-service.ts:240-276)대로 system error 는 즉시 삭제 대상이 아니며 SQS 재시도가 정상 동작함. 즉 동작은 옳고 로그 레벨만 과도함 Rejected (동작 측면) / Confirmed for log-noise concern

Fix Recommendation#

즉시 조치 (Critical)#

해당 사항 없음. 메시지 손실은 없고 SQS 재시도 경로가 정상 동작한다. 단발 ETIMEDOUT 2 건만으로 코드 변경은 불필요하다.

단기 개선 (1주 이내)#

  • packages/base/src/base-service.ts:290-313handlingMessageErrors 에서 error 가 Node system error (errno && code && syscall) 이고 receiveCount < MaxReceiveCount 인 케이스는 logger.warn 으로 다운그레이드 검토. 현재는 logger.error 로 찍어 transient retry path 까지 alert 노이즈가 된다. 다운그레이드 시 error-sweeper / Datadog status:error 대시보드의 거짓 양성을 줄일 수 있다.
    • 근거: getApiErrorToDeleteMessage 가 같은 분류로 이미 logger.warn('... nodejs common system error', error) 를 사용 중(base-service.ts:246). 두 곳의 로그 레벨이 일관되도록 맞추는 방향.
  • 메모리에 명시된 팀 컨벤션과도 정합: cross-region/transient/네트워크 타임아웃은 warn 레벨이 적절.

장기 개선 (재발 방지)#

  • Tesla API 호출 클라이언트(현재 cupixAuth.startSession 경로)에 명시적 timeout 설정과 1 회 자동 retry (지수 백오프) 도입 여부 검토. 현재 CupixAuth.retryablee.statusCode > 500 일 때만 동작하여(cupix-auth.ts:64) system error 는 retry 가 적용되지 않는다.
  • agent 가 SQS 메시지마다 startSession 을 호출하는 비용을 재검토. 토큰 캐싱/세션 재사용 도입 시 transient 네트워크 실패 노출면을 줄일 수 있다.

Monitoring#

다음 쿼리는 release dashboard timeseries widget 에 그대로 들어가도 그래프가 그려지도록 monitor-only 문법(| stats, count by(...)) 을 사용하지 않았다.

system error 분류로 떨어지는 retry path 의 빈도:

text
service:cupixworks-capture-postprocessor-agent status:error "BaseService::handlingMessageErrors" "syscall"

ApproximateReceiveCount 가 2 이상으로 재처리된 케이스(실제 retry 발생):

text
service:cupixworks-capture-postprocessor-agent "BaseService::handlingMessageErrors" "ApproximateReceiveCount\":\"2"

CupixAuth TLS undefined-response warn (timeout/reset 의 선행 신호):

text
service:cupixworks-capture-postprocessor-agent status:warn "CupixAuth::handleError | Undefined response"

알림 권고:

  • 위 첫 두 쿼리에 대해 5 분 윈도에서 5 건 초과 시 Slack notify (현재 baseline 은 2 건/16 분 수준이므로 충분히 여유 있음).

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (로그 레벨 다운그레이드만 하는 경우) / standard (auth 경로 retry 정책까지 손대는 경우)