BaseTransferManager isRetryable — HttpError 필드 누락으로 재시도 불가
RCA: TransferManager::failTask HTTP request failed (no retry)
Overview#
3d-reconstruction agent가 byuk tenant의 octree pointcloud를 s3.eu-west-2.amazonaws.com으로 업로드하다 첫 시도에서 실패했고, retry 없이 즉시 failTask로 넘어갔다. 에러 객체에 code와 HTTP status가 모두 없어 isRetryable 판정이 false로 나오면서 5회 재시도 예산이 하나도 사용되지 않았다.
What Happened#
2026-07-23 02:52 KST에 cupix-capture-3d-reconstruction-agent가 byuk tenant의 byuk.44068_131881_octree.bin을 pre-signed AWS S3 URL(eu-west-2)로 PUT하다 실패했다. axios가 던진 error에 code: undefined, statusCode: undefined, message: "HTTP request failed"만 담겨 있어 재시도 정책이 이 실패를 재시도 불가로 분류하고 failTask를 즉시 호출했다. 1건 단발성 이벤트지만, 재시도 안전망이 우회된 사례라 관측성과 재시도 조건을 검토할 필요가 있다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | TransferManager::failTask (log-only, no thrown class) |
| exception.message | HTTP request failed |
| top_frame | applications/agents/packages/base/src/manager/transfer.manager.ts:166-170 |
| deploy | 74c91b011 (TSLA-13701 TransferManager::failTask improve HttpError observability) |
| env | production, eu-central-1 (agent), eu-west-2 (S3 target) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| byuk (3d-reconstruction) | 1 | octree pointcloud 업로드 1건 실패, capture 44068 후속 파이프라인 중단 가능성 |
Timeline#
- 2026-07-23 02:24 KST —
ThreeDReconstruction::runThreeDReconstruction가 byuk / production / eu-central-1에서 시작됨. - 2026-07-23 02:29 KST — 동일 도메인에서 재개 로그 (2번째 info 이벤트).
- 2026-07-23 02:52:49 KST — 첫 upload 시도가 즉시 실패,
TransferManager::failTask발화,count: 0/5로 종료.
Error Log#
TransferManager::failTask | path: /tmp/workspace/byuk.44068_result/byuk.44068_131881_octree.bin, url: https://s3.eu-west-2.amazonaws.com/cupixworks-hosting-4c317feb9604-euwe2/562df1e3fb63e832/octree/euwe2/pointclouds/135132?x-amz-acl=bucket-owner-full-control&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQBGWD5FH4B3TIZO5%2F20260722%2Feu-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260722T175141Z&X-Amz-Expires=7200&X-Amz-SignedHeaders=host&X-Amz-Signature=1dc6b31e25565bb8c74b4dd78b5f0a31370868716b4b17581fbeb04f3c80f980, count: 0/5, code: undefined, message: HTTP request failed
Impact#
- Service:
cupixworks-capture-3dreconstruction-instance - Team: byuk
- 발생 횟수: 1
- 최초 발생: 2026-07-23 02:52 KST
- 최근 발생: 2026-07-23 02:52 KST
Root Cause Summary#
Upload 시도 중 axios가 던진 error 객체에 code와 response.status가 둘 다 비어 있어, isRetryable이 어떤 조건에도 걸리지 않고 false를 반환했다. retryTask → canRetry false 분기 → failTask 경로가 첫 시도(retryCount=0)에서 즉시 실행되어 재시도 5회 예산이 소비되지 않았다. 에러 message "HTTP request failed"는 axios 기본 message 문자열이 아니며, timeout이나 network error도 아닌 특정 low-level 실패(예: aborted request, TLS handshake 실패, keep-alive socket 종료)에서 error.code가 유실된 채 전달됐을 가능성이 크다. 즉, 진짜 문제는 두 가지가 겹쳐 있다: (1) axios error가 code/statusCode 없이 도달했고, (2) 그런 error를 non-retryable로 취급하는 정책이 안전하지 않다.
Technical Analysis#
Code Path#
Entry point: TransferManager::transferTask (base 클래스)의 catch 블록이 retryTask 호출.
private transferTask = async (task: ITransferTask): Promise<void> => {
try {
await task.init();
// ...
if (task.action === TaskAction.Upload) {
await this.doUploadFile(task.url, task.target, task.Headers);
}
await this.onAfterTransfer(task);
this.completeTask(task);
} catch (error) {
this.retryTask(task, error);
}
};
Retry gate: retryTask가 canRetry를 호출하고, 3d-reconstruction agent는 isRetryable(error) && task.retry()로 오버라이드한다.
protected canRetry(task: ITransferTask, error: any): boolean {
return isRetryable(error) && (task as BaseTask).retry();
}
Failure point: isRetryable이 code도 status도 없는 error에 대해 false 반환.
export const isRetryable = (error: any): boolean => {
if (RETRYABLE_CODES.includes(error?.code)) return true;
const status = error?.statusCode || error?.response?.status;
if (status && status >= 500) return true;
return false;
};
RETRYABLE_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'EAI_AGAIN']. error가 이 목록에 없고 HTTP status도 없으면 false. Fail 로그는 다음 지점에서 발화한다.
private failTask = (task: ITransferTask, error: any): void => {
this._running = false;
this._stop = true;
// ...
this._failed.push(task);
this.onTaskFailed(task, error);
logger.error('TransferManager::failTask | path: %s, url: %s, count: %d/%d, code: %s, statusCode: %s, message: %s, body: %s',
task.target, task.url, task.retryCount, MaxRetries,
error?.code, error?.statusCode ?? error?.response?.statusCode, error?.message,
summarizeResponseBody(error?.response?.body));
};
기대 동작: 일시적 네트워크 실패는 5회까지 재시도(MaxRetries=5). 실제 동작: retryCount=0에서 즉시 fail. code, statusCode, body 모두 undefined로 로깅됐고, 상위 계층이 onTaskFailed → postErrorProcess로 실패 상태를 전파했다.
Log Evidence#
Datadog query (재현용):
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::failTask"
동일 시간대 byuk 관련 컨텍스트:
2026-07-23 02:24:00 INFO ThreeDReconstruction::runThreeDReconstruction environments | domain: byuk, envName: production, launchMode: CUPIXWORKS, region: eu-central-1, userEmail: undefined
2026-07-23 02:29:34 INFO ThreeDReconstruction::runThreeDReconstruction environments | domain: byuk, envName: production, launchMode: CUPIXWORKS, region: eu-central-1, userEmail: undefined
2026-07-23 02:52:49 ERROR TransferManager::failTask | path: /tmp/workspace/byuk.44068_result/byuk.44068_131881_octree.bin, url: https://s3.eu-west-2.amazonaws.com/..., count: 0/5, code: undefined, message: HTTP request failed
동일 서비스의 다른 실패 사례(비교용, 대부분 me-central2 MinIO에서 5/5 소진 후 실패):
2026-07-23 03:05:18 ERROR TransferManager::failTask | ..., count: 5/5, code: ERR_BAD_RESPONSE, message: Request failed with status code 503
2026-07-23 02:35:26 ERROR TransferManager::failTask | ..., count: 5/5, code: ERR_BAD_RESPONSE, message: Request failed with status code 503
이 클러스터만 유일하게 count: 0/5, code: undefined로 다른 실패 패턴과 구분된다. eu-west-2(진짜 AWS S3)로 향한 upload가 재시도 정책 밖에서 종료된 유일한 케이스다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | axios error가 code/statusCode 없이 전달되어 isRetryable가 false 반환, 첫 시도에 즉시 fail |
count: 0/5, code: undefined, isRetryable 로직이 이 두 조건 없으면 false 반환 (util/transfer.ts:66-72) |
— | Confirmed |
| H2 | S3(eu-west-2)가 5xx로 응답해서 fail | 다른 로그들은 5/5 소진 후 Request failed with status code 503으로 실패 |
이 로그는 code: undefined, statusCode 없음, count 0/5 — HTTP 응답 자체를 받지 못한 상태 |
Rejected |
| H3 | Pre-signed URL 만료로 인한 auth 실패 (403) | URL은 X-Amz-Date=20260722T175141Z, X-Amz-Expires=7200 → 유효기한 2026-07-22T19:51Z까지, 실제 실패 17:52Z → 유효기간 내 |
statusCode 있어야 하는데 undefined | Rejected |
| H4 | axios interceptor가 retry 중이었고 최종 실패에서 code 유실 | interceptor는 RETRYABLE_CODES 또는 status>=500에서만 3회 재시도 (util/transfer.ts:12-33), 그 외 error는 그대로 throw. 재시도 없이 원본 error가 throw됐을 가능성이 있음 |
로그에서 interceptor의 transfer::retry info 로그가 없음 → interceptor가 재시도 시도하지 않음 (즉, 처음부터 retryable 아님으로 판정) |
Rejected |
| H5 | file stream 실패 (fs.createReadStream/ENOENT 등) | uploadFile이 fs.createReadStream(path) 사용 (util/transfer.ts:57-64). fs error라면 error.code는 ENOENT처럼 채워짐 |
code: undefined라 fs error 형태 아님 |
Rejected |
| H6 | TCP/TLS handshake 실패로 axios가 code 없는 error를 던짐 (예: keep-alive socket close, proxy 중단) | message: HTTP request failed는 axios 기본 message가 아니라 wrapping된 문자열. code: undefined, statusCode: undefined, response: undefined 조합은 응답 전 단계 실패와 일치. eu-west-2(멀리 있는 리전)로의 국경 간 트래픽에서 드물게 발생 가능 |
단발 이벤트라 재현 어려움, body 필드가 새로 추가됐지만 이 로그는 구 포맷(body 필드 없음)이라 서비스가 새 로거 배포 전일 가능성 있음 — 즉, 아직 커버되지 않은 실패 케이스 |
Inconclusive (H1이 confirmed root cause이고 H6은 그 원인 시나리오 후보) |
Fix Recommendation#
즉시 조치 (Critical)#
- 파일:
applications/agents/packages/base/src/util/transfer.ts:66-72 - 접근:
isRetryable를 conservative-retry 방향으로 확장한다. HTTP 응답 자체를 받지 못한 error (즉error.response가 없고error.code도 없는 axios error)는 network-level 실패로 간주해 retry 대상으로 넣어야 한다. 5xx만 retry하는 현재 정책은 "응답 없음" 케이스를 사각지대로 남긴다. - 근거: 로그 evidence에서 이 케이스만 재시도 예산 0을 그대로 소비하고 실패했다. axios가 code 없는 error를 던지는 경로 (예: socket hang up이 code 없이 전달되는 버전, custom agent, DNS 실패 후 흡수된 error)를 방어해야 한다.
- 방향 예시 (구현 코드 아님):
error.response === undefined && !error.code조건도 재시도 목록에 포함하되, 무한 루프 방지를 위해retryCount < MaxRetries게이트는 그대로 유지한다.
단기 개선 (1주 이내)#
- axios 요청 config에
validateStatus,httpAgent/httpsAgent(keep-alive 및 socket timeout 명시),transitional옵션을 검토해 error.code가 왜 유실되는지 재현 조건을 만들 것. TSLA-13701이 실패 로그에statusCode와body를 추가했지만, 이 클러스터 로그는 아직 이전 포맷을 사용 중 (body:필드가 없음). 관측성 개선 commit이 실제로 3d-reconstruction agent 이미지에 배포됐는지 태그/버전으로 확인 필요.- axios interceptor의
transfer::retryinfo 로그가 이 실패에는 없다. 즉 interceptor 재시도조차 트리거되지 않았음을 명시적으로 기록하도록axios.interceptors.response.usecatch에서 non-retryable도 debug 로그로 남기면 향후 진단이 쉬워진다.
장기 개선 (재발 방지)#
- 3d-reconstruction agent의 upload 실패 → capture 재처리 경로가 사람 개입 없이 회복되는지 리뷰. 현재
failTask는 컨테이너 전체를 stop시키고 (this._stop = true) 후속 upload도 처리하지 않는다. 부분 실패에 대한 격리 (partial fail + continue) 정책 도입을 검토. - eu-west-2 target으로의 pre-signed URL 요청을 별도 SLI로 측정해서, cross-region 업로드 실패율을 추적. 현재는 me-central-2 MinIO 5xx 노이즈에 묻혀 있음.
Monitoring#
- 알림 추가:
code: undefined && count: 0/5조합이 시간당 N건 이상이면 warn (재시도 사각지대 감시). - Datadog widget용 timeseries queries:
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::failTask"
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::failTask" "count: 0/"
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager::failTask" "code: undefined"
- 위 세 쿼리를 timeseries 위젯의 count 그래프로 시각화하고, 세 번째 쿼리(재시도 없이 실패한 케이스)가 기저선 대비 튀는 순간을 alert threshold로 잡는다.
Risk Assessment#
- Risk level: low (단건, 단일 tenant, 단일 파일)
- 예상 복잡도: standard (retry 조건 튜닝은 shared
util/transfer.ts변경이라 다른 agent에도 영향; 회귀 테스트 필요)