ES /docs

FloorplanService::downloadFile | path: /tmp/workspace/89075/89075.png, error: {"errno":-104,"code":"

RCA: FloorplanService::downloadFile ECONNRESET

Overview#

What Happened#

2026-06-23 11:19 KST 시점에 cupixworks-any-floorplan-agent(production us-west-2)에서 floorplan id 89075의 원본 PNG 파일을 다운로드하는 도중 TLS socket이 원격에서 강제 종료되어 ECONNRESET (errno -104, read syscall)로 실패했다. SQS 메시지 229cc426-7e7f-4252-bfa4-b51d476f5ddbApproximateReceiveCount: 1이라 자동 재시도 대상이며, 단일 occurrence(점진 발생 패턴이 아님)이다.

Quick Facts#

Field Value
exception.class Error (Node.js ECONNRESET)
exception.message {"errno":-104,"code":"ECONNRESET","syscall":"read"}
top_frame packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:268
runtime Node.js (request library, TLS read in node:internal/stream_base_commons)
env production us-west-2, tenant cupix

Affected Teams#

Team / Domain Error Count Impact
floorplan-agent (chrischoi) 1 floorplan 89075 1건 처리 실패. SQS 자동 재시도 예정

같은 시간대(2026-06-23 02:15~02:55 UTC) 다른 agent 들에서도 ECONNRESET/ETIMEDOUT 다발: TransferManager::downloadFile, TransferManager::uploadFile, ImageCPObject::uploadFile, 그리고 ioredis [ioredis] Unhandled error event: Error: connect ETIMEDOUT (/var/task/refresh_token.js, /var/task/token_save.js) — 토큰 캐시용 Redis와 외부 다운로드 양쪽에 일시적 네트워크 문제가 있었음을 시사.

Timeline#

  1. 2026-06-23 11:15:42 KSTBaseService::runByMessage | id: 89075 — SQS 메시지 수신, 처리 시작
  2. 2026-06-23 11:18:46 KST — 같은 agent 노드에서 ioredis connect ETIMEDOUT (refresh_token.js) 발생 — 동일 시간대 인프라 네트워크 이상 신호
  3. 2026-06-23 11:19:09 KSTFloorplanService::downloadFile | path: /tmp/workspace/89075/89075.png, error: {"errno":-104,"code":"ECONNRESET",...} — TLS read 실패
  4. 2026-06-23 11:19:09 KSTCupixAuth::handleError | Undefined response: ... read ECONNRESETBaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/89075 — workspace 정리
  5. 2026-06-23 11:19:09 KSTBaseService::handlingMessageErrors | ... ApproximateReceiveCount: "1" — SQS receive count 1 < MaxReceiveCount이므로 메시지 삭제 안 됨, 재시도 대기

Error Log#

Datadog Logs

text
FloorplanService::downloadFile | path: /tmp/workspace/89075/89075.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}

Impact#

  • Service: cupixworks-any-floorplan-agent
  • Team: chrischoi
  • 발생 횟수: 1
  • 최초 발생: 2026-06-23 11:19 KST
  • 최근 발생: 2026-06-23 11:19 KST

영향 범위는 floorplan 89075 1건. BaseService::handlingMessageErrorsapiErrorObject가 undefined이고 receiveCount(1) < MaxReceiveCount이므로 SQS 메시지를 삭제하지 않는다. 따라서 SQS visibility timeout 만료 후 자동 재시도가 일어나며, 다음 시도에서 네트워크가 정상이면 정상 처리될 가능성이 높다.

Root Cause Summary#

원격 HTTPS endpoint(cpFloorplanSource.downloadUrl)에서 floorplan 원본 PNG를 읽는 도중 TLS 소켓이 peer/네트워크에 의해 reset되어 read ECONNRESET이 발생했다. 이는 application 로직의 결함이 아니라 transient network/upstream connection 문제다. 같은 시간대 동일 production 환경의 다른 agent 들에서도 ECONNRESET/ETIMEDOUT과 ioredis connect ETIMEDOUT이 동반 발생한 것으로 보아, 다운로드 대상(또는 그 앞단의 LB/CDN) 혹은 NAT/네트워크 경로의 일시 장애가 원인일 가능성이 크다. downloadFileWithHeader는 단발 시도이며 자체 재시도 로직이 없으므로, transient한 네트워크 단절이 그대로 에러로 노출된다. SQS 메시지는 receive count 정책에 의해 자동으로 재시도된다.

Technical Analysis#

Code Path#

Entry: SQS 메시지 수신 → BaseService::runByMessageFloorplanService::run(targetId=89075)downloadFileWithHeader(downloadUrl, originalFilePath, headers).

run에서 cpFloorplanSource의 download URL을 받아 originalFilePath로 저장한다:

packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:74-83typescript
				if (cpFloorplan.uploadUrl != undefined
					&& cpFloorplan.localFilePath != undefined
					&& cpFloorplan.cpFloorplanSource.downloadUrl != undefined
					&& cpFloorplan.cpFloorplanSource.originalFilePath != undefined
				) {
					await this.updateFloorplanState(targetId, TESLA.FloorplanState.Processing);
					await this.downloadFileWithHeader(
						cpFloorplan.cpFloorplanSource.downloadUrl,
						cpFloorplan.cpFloorplanSource.originalFilePath,
						cpFloorplan.cpFloorplanSource.headers
					);

Failure point: downloadFileWithHeader 내부에서 request.get(url)의 응답 스트림 error 이벤트로 reject. ECONNRESET은 socket read 중 peer가 RST를 보낸 상황으로, response code 200 분기(line 260)도 거치지 못한 채 line 267-270의 'error' handler로 들어왔다:

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
					}
				});

				fileStream
					.on('finish', async () => {
						await CPUtils.sleep(100);
						fileStream.close();
						logger.debug('FloorplanService::downloadFile | complete path: %s, size: %d', path, CPUtils.getFileSize(path));
						resolve();
					});

				sendReq
					.on('response', res => {
						if (res.statusCode !== 200) {
							logger.error('FloorplanService::downloadFile | response path: %s, code: %d, message: %s', path, res.statusCode, res.statusMessage);
							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));
					});

기대 동작: 200 응답을 받고 stream pipe 완료 시 finish → resolve. 실제 동작: TLS read 중 socket이 reset되어 error 이벤트로 진입, reject. 재시도 로직이 함수 내부에 없다(request 라이브러리 default retry 옵션 사용 안 함).

에러는 runByMessages의 try/catch를 거쳐 handlingMessageErrors로 전달된다:

packages/agents/packages/base/src/base-service.ts:300-311typescript
			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));

ECONNRESET은 HTTP API 4xx/5xx response가 아니므로 getApiErrorToDeleteMessageundefined를 반환하고, ApproximateReceiveCount=1 < MaxReceiveCount이므로 메시지를 삭제하지 않는다 → SQS가 visibility timeout 후 자동 재시도한다.

Log Evidence#

Datadog query used:

text
service:cupixworks-any-floorplan-agent "89075"

floorplan 89075의 처리 흐름(시간순):

text
2026-06-23 11:15:42  info   BaseService::runByMessage | id: 89075
2026-06-23 11:19:09  warn   read ECONNRESET
2026-06-23 11:19:09  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-23 11:19:09  error  FloorplanService::downloadFile | path: /tmp/workspace/89075/89075.png, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-23 11:19:09  info   BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/89075
2026-06-23 11:19:09  error  BaseService::handlingMessageErrors | Error and message object - {"error":{"errno":-104,"code":"ECONNRESET","syscall":"read"},"sqsMessage":{"MessageId":"229cc426-7e7f-4252-bfa4-b51d476f5ddb","Attributes":{"ApproximateReceiveCount":"1"}}}

stack trace는 TLSWrap.onStreamRead에서 발생 — TLS 소켓 read 도중 peer가 connection을 RST한 패턴이다.

같은 시간대(±30분) 동일 환경에서 관련 네트워크 에러:

text
2026-06-23 11:18:46  error  [ioredis] Unhandled error event: Error: connect ETIMEDOUT
                            at Socket.<anonymous> (/var/task/refresh_token.js:159:27553)
2026-06-23 11:15:51  error  [ioredis] Unhandled error event: Error: connect ETIMEDOUT
                            at Socket.<anonymous> (/var/task/refresh_token.js:159:27553)
2026-06-23 10:54:21  error  FloorplanService::downloadFile | path: /tmp/workspace/89073/89073.png, error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"}
2026-06-23 11:42:18  error  TransferManager::downloadFile | path: /tmp/workspace/720131/videos/678689/original/678689.insv, error: {"errno":-104,"code":"ECONNRESET","syscall":"read"}
2026-06-23 11:35:17  error  TransferManager::uploadFile  ... ETIMEDOUT
2026-06-23 11:28:49  error  TransferManager::uploadFile  ... ETIMEDOUT

floorplan-agent 외 transfer 계열 agent 와 토큰 캐시(redis)까지 동시에 ECONNRESET/ETIMEDOUT — 단일 endpoint 결함이 아닌, 호스트 또는 네트워크 경로 차원의 일시 단절로 해석된다.

Datadog query used (전체 환경):

text
service:cupixworks-any-floorplan-agent "ECONNRESET" OR "ETIMEDOUT" OR "downloadFile"

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Transient network/upstream connection 단절로 다운로드 중 TLS socket이 RST됨 stack frame이 TLSWrap.onStreamRead에서 시작; 동일 시간대 다른 agent(TransferManager::downloadFile/uploadFile)와 ioredis connect ETIMEDOUT까지 함께 ECONNRESET/ETIMEDOUT 발생; 시도 1회로 단일 occurrence; HTTP status code 분기는 발생 안 함 Confirmed
H2 downloadUrl 만료/잘못된 URL로 인한 인증 실패 HTTP 4xx/5xx response가 아니라 socket-level error; res.statusCode !== 200 분기 로그 없음; getApiErrorToDeleteMessage가 undefined 반환(메시지 미삭제) Rejected
H3 floorplan source 자체가 손상되어 sharp/exiftool에서 실패 에러는 다운로드 단계(downloadFileWithHeader)에서 발생, sharp/tileFloorplan까지 진행되지 않음; workspace cleanup 직후 종료 Rejected
H4 floorplan-agent process의 file descriptor/메모리 누수로 자체 socket 종료 단일 occurrence; 같은 시간대 다른 별개 agent/Redis까지 동시 영향; agent 내부 자원 고갈이면 그 agent 단독으로 다발해야 함 Rejected
H5 영구적 service outage로 분류해야 함 occurrence_count=1, 단일 floorplan(89075)만 해당; SQS receive count=1이라 자동 재시도 예정; status-board 스코프 svc:cupixworks-any-floorplan-agent::unknown에 active 인시던트 없음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 별도 조치 불필요. SQS message가 ApproximateReceiveCount=1로 남아 있어 visibility timeout 만료 후 자동 재시도된다. 재시도 시 네트워크가 정상이면 floorplan 89075는 정상 처리된다.
  • 재시도 후에도 동일 floorplan이 MaxReceiveCount에 도달해 영구 실패로 떨어지면 그때 cpFloorplanSource.downloadUrl이 유효한지 수동 확인(packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:67cpFloorplanSource_downloadUrl info 로그).

단기 개선 (1주 이내)#

  • downloadFileWithHeader(packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:237)에 transient network error(ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN)에 대한 자체 retry-with-backoff을 추가하는 방향 검토. SQS retry는 visibility timeout(분 단위) 단위라서 사용자 체감 latency가 크다. 비교 대상: TransferManager에서도 같은 패턴이 반복되므로 base layer(예: @agents/base)에 공통 HTTP retry util을 두는 것이 좋다.
  • 현재 코드는 socket-level error를 logger.error 로 기록한다. 이런 transient network error는 application bug가 아니므로 retry가 적용된 뒤 최종 실패 시에만 error 레벨로 남기고, 첫 단절은 warn으로 강등하면 RCA/알람 노이즈가 감소한다(같은 시간대 동반 발생한 ETIMEDOUT 다발도 알람을 키운다).
  • cupixAuth.handleError가 socket-level Error를 받으면 "Undefined response" warn으로 처리하는 현 경로(packages/cupix-tesla-floorplan-agent/src/floorplan-service.ts:269)에서 retry hook을 함께 호출하도록 한다.

장기 개선 (재발 방지)#

  • agent 들이 호출하는 외부 endpoint(특히 cpFloorplanSource.downloadUrl이 가리키는 storage host)에 대한 SLI/SLO 모니터링 추가. ECONNRESET/ETIMEDOUT rate가 임계치 이상이면 인프라 측 조사 트리거.
  • ioredis connect ETIMEDOUT(/var/task/refresh_token.js, /var/task/token_save.js)은 같은 시간대 반복 발생 중이므로, 토큰 캐시 Redis 클라이언트 connection 안정성(VPC peering, security group, NAT)을 점검한다. 이것이 외부 다운로드 ECONNRESET과 같은 호스트/AZ 네트워크 이슈일 가능성 — Quick Facts에 표시한 us-west-2 의 일시 네트워크 이슈 가능성으로 연결되므로 함께 조사할 가치가 있다.
  • floorplan source download의 일시 실패가 자동 재시도되도록 base-service 차원의 transient error classification(예: getApiErrorToDeleteMessage와 별도로 isTransientNetworkError)을 도입.

Monitoring#

추가/활용할 Datadog 쿼리:

text
service:cupixworks-any-floorplan-agent status:error "downloadFile"
text
service:cupixworks-any-floorplan-agent ("ECONNRESET" OR "ETIMEDOUT")
text
service:cupixworks-any-floorplan-agent "BaseService::handlingMessageErrors"

알림 권장:

  • 임의 5분 윈도우에서 cupixworks-any-floorplan-agentECONNRESET 또는 ETIMEDOUT 카운트가 임계 이상이면 warn — 단일 floorplan이 아닌 인프라 광역 이슈로 간주.
  • 같은 시간대 ioredis connect ETIMEDOUT 카운트와 cross-correlate.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (즉시 코드 변경 불필요), short-term retry 도입 시 standard
  • transient network 원인 + 단일 occurrence + SQS auto-retry 보장 → 사용자 영향 일시적이며 자동 복구 가능