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#
- 2026-06-24 23:13:04 KST —
MessageId c5b275f1-1081-43fa-99a8-51de8c74fe44처리 중 Tesla API auth 호출에서read ETIMEDOUT발생.CupixAuth::handleError | Undefined responsewarn →BaseService::handlingMessageErrorserror (ApproximateReceiveCount=1). - 2026-06-24 23:18:44 KST — 같은 서비스의 다른 메시지(
2e2194ec-...)가Cannot read properties of undefined (reading 'response')로 실패 (별개 패턴, 같은 인시던트 윈도). - 2026-06-24 23:29:46 KST — 두 번째 시스템 에러:
MessageId 7ae02df1-28dd-4c16-9b69-dc7d937dd817,errno -104 ECONNRESET, syscall: read(cluster의 두 번째 occurrence). - 2026-06-24 23:57:51 KST —
c5b275f1메시지가ApproximateReceiveCount: 2로 재처리되었고 이번엔undefined response로 다시 실패 후 삭제됨.
Error Log#
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:153—runByMessage - Auth step:
packages/base/src/base-service.ts:200—cupixAuth.startSession() - Underlying error site:
packages/api/src/authentication/cupix-auth.ts:42-57—CupixAuth.handleError가response == undefined분기로Undefined response: ...warn 출력 후 원본 system error 를 그대로 throw - Outer catch:
packages/base/src/base-service.ts:108-111—runByMessages의 try/catch 가 에러를handlingMessageErrors로 위임 - Failure logging:
packages/base/src/base-service.ts:290-313—handlingMessageErrors
try {
await this.runByMessages();
} catch (error) {
await this.handlingMessageErrors(error);
}
this.resetMessages();
await CPUtils.sleep(500);
await this.checkingQueue();
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 를 의도적으로 "삭제 사유 아님" 으로 분기하는 부분:
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 로 기록하는 분기:
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 의 임계값:
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;
};
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 쿼리:
service:cupixworks-capture-postprocessor-agent status:error "BaseService::handlingMessageErrors"
service:cupixworks-capture-postprocessor-agent "c5b275f1-1081-43fa-99a8-51de8c74fe44"
원본 로그(시간순, KST):
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 frameTLSWrap.onStreamRead까지 남아 있어 Tesla API HTTPS 연결 단계에서 발생.CupixAuth::handleError가response == 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-313의handlingMessageErrors에서error가 Node system error (errno && code && syscall) 이고receiveCount < MaxReceiveCount인 케이스는logger.warn으로 다운그레이드 검토. 현재는logger.error로 찍어 transient retry path 까지 alert 노이즈가 된다. 다운그레이드 시 error-sweeper / Datadogstatus: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.retryable은e.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 의 빈도:
service:cupixworks-capture-postprocessor-agent status:error "BaseService::handlingMessageErrors" "syscall"
ApproximateReceiveCount 가 2 이상으로 재처리된 케이스(실제 retry 발생):
service:cupixworks-capture-postprocessor-agent "BaseService::handlingMessageErrors" "ApproximateReceiveCount\":\"2"
CupixAuth TLS undefined-response warn (timeout/reset 의 선행 신호):
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 정책까지 손대는 경우)