BaseTransferManager HTTP 400을 non-retryable로 분류 — 연쇄 실패 유발
RCA: TransferManager::failTask | HTTP request failed on capture 692809 uploads
Overview#
What Happened#
2026-07-14 03:57 KST 무렵 cupixworks-capture-postprocessor-agent 에서 capture 692809 (workspace /tmp/workspace/733798/videos/692809/original/)의 pano 프레임 업로드 태스크가 23초 동안 222회 연속 실패했다. 실패한 태스크는 모두 url: undefined, count: 0/5, code: undefined, message: HTTP request failed 시그니처를 가졌으며, retry 없이 즉시 failTask로 종결되었다. 이는 tesla-v1 API 클라이언트(@tesla/typescript-node-sdk) 가 pano 생성/업로드 URL 발급 API 호출 시 소켓 레벨 실패를 던져 UploadNewPanosTask.init()이 URL을 설정하기 전에 실패한 결과다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError (from @tesla/typescript-node-sdk) |
| exception.message | HTTP request failed |
| top_frame | packages/base/src/manager/transfer.manager.ts:148 (failTask) |
| runtime | Node.js, axios@1.16.1, @tesla/typescript-node-sdk@1.13.3 |
| env | production, us-west-2 (agent) / me-central-1 (S3 target) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gpinet (capture 692809, workspace 733798) | 222 | 단일 capture의 신규 pano 업로드 전체 실패 — postprocessor job 이 파일 전송 단계에서 중단 |
Timeline#
- 2026-07-14 03:53:31 KST — 첫 wave: 동일 workspace의 원본 프레임 5개가 me-central-1 S3에 업로드되던 중
ERR_BAD_REQUEST/Request failed with status code 400로 실패 (sibling clusterebd3578b-b22a-45ac-a6d3-fcddb1183a75). - 2026-07-14 03:57:03 KST — 두 번째 wave 시작:
HTTP request failed시그니처의failTask로그가 발생하기 시작. 최초 first_seen. - 2026-07-14 03:57:23–26 KST — 3초 동안 222건이 폭발적으로 기록.
url: undefined,retryCount: 0로 즉시 종결. - 2026-07-14 03:57:26 KST — 마지막 실패 (last_seen). 이후 다른 workspace(734126, 734053 등) 처리가 정상 진행되어 서비스 자체는 살아있음.
Error Log#
TransferManager::failTask | path: /tmp/workspace/733798/videos/692809/original/692809_00289.jpg, url: undefined, count: 0/5, code: undefined, message: HTTP request failed
Impact#
- Service:
cupixworks-capture-postprocessor-agent - Team: gpinet
- 발생 횟수: 222
- 최초 발생: 2026-07-14 03:57 KST
- 최근 발생: 2026-07-14 03:57 KST
Root Cause Summary#
TransferManager::failTask 로그의 url: undefined 는 file transfer 중 실패가 아니라 transferTask 진입 직후 task.init() 단계에서 예외가 발생했음을 의미한다. UploadNewPanosTask.init()은 cupixApi를 통해 pano record 를 생성(cupixApi.pano.create) 하거나 업로드 URL을 발급(cupixApi.pano.createUploadUrl) 하는데, 이 호출들은 자동 생성된 @tesla/typescript-node-sdk (구식 request 라이브러리 기반) 를 사용하고 있으며, 요청 콜백이 에러를 받으면 HttpError('HTTP request failed') (response 미포함) 를 던진다. 이 예외는 BaseTransferManager.transferTask의 catch(error) 로 잡혀 retryTask 로 넘어가지만, isRetryable(error) 이 error.code (undefined) 와 error.response?.status (undefined) 로 판정하기 때문에 어떤 재시도 대상 코드에도 매치되지 않아 retryCount=0 인 채 곧바로 failTask 로 종결된다. 요약하면 (1) tesla API SDK 호출이 소켓/네트워크 레벨 오류를 냈고, (2) 새 axios 기반 isRetryable 이 SDK의 HttpError 를 인식하지 못해 재시도를 건너뛰었으며, (3) 결과적으로 capture 692809 의 모든 pano init 실패가 그대로 태스크 실패로 확대됐다.
Technical Analysis#
Code Path#
Entry point: postprocessor 워커가 capture 692809의 신규 pano 업로드를 큐잉하고 BaseTransferManager.run() → transferTask(task) 를 호출한다.
transferTask 는 먼저 task.init() 을 부른 뒤 url/target 이 세팅되었는지 검사한다:
private transferTask = async (task: ITransferTask): Promise<void> => {
try {
await task.init();
if (task.url == undefined || task.target == undefined) {
this.failTask(task, {});
return;
}
if (task.action === TaskAction.Download) {
await this.doDownloadFile(task.url, task.target, task.Headers);
} else if (task.action === TaskAction.Upload) {
await this.doUploadFile(task.url, task.target, task.Headers);
} else {
this.failTask(task, {});
return;
}
await this.onAfterTransfer(task);
this.completeTask(task);
} catch (error) {
this.retryTask(task, error);
}
};
UploadNewPanosTask.init() 은 tesla API를 호출해 pano 를 만들거나 업로드 URL 을 발급받는다. 이 시점에는 _inputItem.uploadUrl 이 아직 없으므로 task.url 이 undefined 다:
get url(): string | undefined { return this.cpPano.uploadUrl; }
init = async (): Promise<void> => {
logger.debug('UploadNewPanosTask::init | begin - cpPano target: %s', this.target);
if (this.panoId !== Constants.UnknownId) {
logger.debug('UploadNewPanosTask::init | end - exist pano id: %d', this.panoId);
return;
}
...
const responsePano = await this._container.cupixApi.pano.create(createPanoRequest);
this.cpPano.setServerPanoData(responsePano);
...
this.setInitDone();
};
cupixApi.pano.create 가 예외를 던지면 transferTask 의 catch(error) 로 잡혀 retryTask 가 호출된다. retryTask 는 canRetry(task, error) (기본 구현 = isRetryable(error)) 로 재시도 여부를 판단한다:
retryTask = (task: ITransferTask, error: any): void => {
if (this.canRetry(task, error)) {
...
setTimeout(() => {
this.onRetryRenew(task)
.then(() => { this.addTask(task); })
.catch(retryError => { this.retryTask(task, retryError); });
}, delay);
} else {
this.failTask(task, error);
}
};
isRetryable 은 axios error 를 기준으로 만들어졌다:
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;
};
Failure point: tesla-v1 SDK의 HttpError 는 code 도 response.status 도 세팅하지 않는다 — 콜백이 에러를 받은 경우 response 자체가 undefined 상태로 넘어올 수 있다:
export class HttpError extends Error {
constructor (public response: http.IncomingMessage, public body: any, public statusCode?: number) {
super('HTTP request failed');
this.name = 'HttpError';
}
}
isRetryable → false → failTask 즉시 호출 → 로그 남김:
private failTask = (task: ITransferTask, error: any): void => {
this._running = false;
this._stop = true;
const idx = this._transferring.indexOf(task);
this._transferring.splice(idx, 1);
this._failed.push(task);
this.onTaskFailed(task, error);
logger.error('TransferManager::failTask | path: %s, url: %s, count: %d/%d, code: %s, message: %s',
task.target, task.url, task.retryCount, MaxRetries, error?.code, error?.message);
};
기대 동작: SDK 소켓/네트워크 오류는 재시도 대상이므로 retryTask 에서 최대 5회 지수백오프 재시도를 해야 한다. 실제 동작: HttpError 의 형태가 axios 기준의 isRetryable 과 불일치하여 재시도 0회로 곧장 실패로 처리된다.
Log Evidence#
Datadog 쿼리 (cluster 원본):
service:cupixworks-capture-postprocessor-agent status:error @environment:production "TransferManager::failTask"
주 실패 로그 (첫 로그):
2026-07-14 03:57:03 error
TransferManager::failTask | path: /tmp/workspace/733798/videos/692809/original/692809_00289.jpg, url: undefined, count: 0/5, code: undefined, message: HTTP request failed
동일 workspace 에서 4분 앞선 첫 wave — 이 로그는 url 이 있고 code: ERR_BAD_REQUEST, message: Request failed with status code 400 로, S3 me-central-1 프리사인 PUT 응답이 400 이었음을 보여준다 (sibling cluster ebd3578b-b22a-45ac-a6d3-fcddb1183a75):
2026-07-14 03:53:56 error
TransferManager::failTask | path: /tmp/workspace/733798/videos/692809/original/692809_00286.jpg, url: https://s3.me-central-1.amazonaws.com/cupixworks-source-b169da1a0187-mece1/resources/rg7jzs/mece1/v1?x-amz-acl=bucket-owner-full-control&x-amz-storage-class=ONEZONE_IA&X-Amz-Algorithm=AWS4-HMAC-SHA256&...&X-Amz-Expires=7200&X-Amz-Signature=..., count: 0/5, code: ERR_BAD_REQUEST, message: Request failed with status code 400
BaseService 가 상위 axios 에러를 함께 로깅한 항목:
2026-07-14 03:53:31 error
BaseService::handlingMessageErrors | Error and message object - {"error":{"message":"Request failed with status code 400","name":"AxiosError","stack":"AxiosError: Request failed with status code 400
at settle (.../axios.cjs:2069:12)
...
at async uploadFile (/tmp/agent/dist/app.cjs:5730:7)
at async BaseTransferManager2.doUploadFile (/tmp/agent/dist/app.cjs:5810:11)
at async BaseTransferManager2.transferTask (/tmp/agent/dist/app.cjs:5896:15)"...
HTTP request failed 문자열 이 실제로 tesla SDK 의 HttpError 에서 나온다는 것을 보여주는 동일 서비스 내 warn 로그 (다른 시각의 다른 프로세스):
2026-07-14 04:09:39 warn
JobManager::updateErrorActionJob | end - {
name: 'HttpError',
message: 'HTTP request failed',
stack: 'HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.../node_modules/@tesla/typescript-node-sdk/api/jobApi.js:556:40)
at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
...
tesla-api 측 오류 부재 확인:
query: service:cupixworks-api status:error
range: 2026-07-13T18:56:00Z ~ 2026-07-13T18:58:00Z
result: Found 0 logs
즉 tesla-api 서버는 5xx 를 남긴 적이 없으므로, HTTP request failed 는 서버가 오류 응답을 준 것이 아니라 클라이언트(agent) 측 소켓 오류 (연결 리셋, 조기 종료, 타임아웃 등) 이거나 tesla-api 가 응답을 500 이하로 반환하되 body 파싱 실패한 케이스일 가능성이 높다 — uncertain -- needs verification (agent 인스턴스의 네트워크/DNS 관련 debug 로그 확보 필요).
Status board 는 같은 서비스에서 동일 root_cause_type("unknown") 인시던트가 2026-07-10 18:10–18:35 UTC 에 이미 발생하여 해소된 이력을 보여준다 — 재발 패턴이므로 감시가 필요하다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | UploadNewPanosTask.init() 내 cupixApi.pano.create/createUploadUrl 호출이 tesla-v1 SDK HttpError('HTTP request failed') 를 던지고, 이 예외가 isRetryable 매치에 실패해 재시도 0회로 즉시 실패 처리됨 |
모든 222건 url: undefined (init 이후 url 세팅 실패), code: undefined + message: 'HTTP request failed' 는 tesla-v1 SDK apis.ts:199 HttpError super() 문자열과 정확히 일치, retryCount: 0, isRetryable 이 code/response.status 로만 판정 (transfer.ts:66-72) |
— | Confirmed |
| H2 | S3 me-central-1 지속 장애로 업로드가 계속 400 을 받는 것이 root cause | 03:53 wave 에서 ERR_BAD_REQUEST / status 400 발생 (5건, sibling cluster ebd3578b) |
222건 wave 의 url 이 undefined 이므로 S3 에 도달하기 전에 실패. code: ERR_BAD_REQUEST 대신 undefined. tesla-api 로그에도 5xx 없음 |
Rejected |
| H3 | `task.url == undefined | task.target == undefined조건으로failTask(task, {})` 가 호출된 케이스 (init 은 정상 반환) |
url: undefined 시그니처 |
|
| H4 | tesla-api 서버 5xx 로 인한 SDK 예외 | 소켓 오류 심볼 상관성 | cupixworks-api 서비스에 동시간대 status:error 0건 (Datadog 확인). 서버가 5xx 를 냈다면 로그가 남거나 SDK 가 statusCode: 5xx 로 세팅했어야 함 |
Rejected |
| H5 | 첫 wave 의 S3 400 → capture 재처리 큐잉 → 재처리 시점(약 3.5분 후) 짧은 시간 동안 agent → tesla-api 소켓 오류가 발생 | 두 wave 가 동일 workspace/capture 에 몰림, 시간 간격 규칙적, tesla-api 서버 로그 부재로 클라이언트/네트워크 원인 가능성 | 네트워크 debug 로그 확인 필요 | Inconclusive |
Fix Recommendation#
즉시 조치 (Critical)#
applications/agents/packages/base/src/util/transfer.ts의isRetryable이 tesla-v1 SDKHttpError(nameHttpError,response미정의) 케이스를 재시도 대상으로 인식하도록 확장한다. 방향:error?.name === 'HttpError'이고error?.statusCode == null || error.statusCode >= 500이면 retryable 로 취급. 또한 axiosECONNREFUSED,ERR_NETWORK등 자주 발생하는 클라이언트 실패 코드도 검토 후 추가.applications/agents/packages/cupix-capture-postprocessor-agent/src/manager/transfer/upload-new-panos.task.ts:23-45init()/renew()의 API 호출 실패는 파일 업로드 자체 실패와 다른 실패 유형이므로, 실패 시failTask로그에stage힌트(예:phase: init|renew|transfer) 를 함께 남겨 후속 진단을 단축한다. UploadNewPanosTask 외의 다른 컨테이너/에이전트에도 동일하게 적용 검토.- 실패 시
error.stack(또는error.name) 을failTask로그에 최소 한 줄이라도 포함해 SDK 예외인지 axios 예외인지 로그만 보고 구분할 수 있게 한다. (code: undefined, message: HTTP request failed만으로는 SDK/axios 구분 불가 — 이번 RCA 에서도 SDK stack trace 확보에 별도 쿼리가 필요했음.)
단기 개선 (1주 이내)#
retryTask가retryCount === 0인 채로failTask로 넘어가는 경로를 별도 카운터/메트릭으로 노출한다. 현재는 “재시도 안 하고 즉시 실패” 와 “5회 재시도 후 실패” 가 로그상 구분되지 않아 알람 튜닝이 어렵다.- tesla-v1 SDK 예외를 표준 형태로 정규화하는 helper 를
packages/api에 두고, agent 내 모든cupixApi.*호출 지점에서 재활용해 “SDK 원본 예외 형태 불일치” 로 인한 유사 이슈를 예방한다.
장기 개선 (재발 방지)#
- 자동 생성 tesla-v1 client (
@tesla/typescript-node-sdk) 가 여전히request@2.88.2(deprecated) 기반이다. TSLA-12622 에서 파일 전송은 axios 로 이관됐지만 API 콜은 그대로 남아 있음. SDK 자체를 axios/fetch 기반으로 재생성하거나, 최소한 agent 측에서 tesla SDK 호출을 얇게 감싼 axios 어댑터로 대체하면 이번과 같은 이중 에러 형식 문제를 근본적으로 없앨 수 있다. - capture 별 pano 업로드 실패가 222건 폭발한 후에도 job 이 즉시 종료되었을 뿐 사용자/티어에 대한 회복 경로 (예: capture 를 postprocess retry 큐로 되돌림) 가 명시되지 않았다 — postprocessor job level 재시도/DLQ 정책 재점검 필요.
Monitoring#
Datadog 대시보드에 아래 timeseries 를 추가한다. 모든 쿼리는 timeseries widget 에 그대로 넣을 수 있도록 sum: prefix 또는 count(...) 표현을 유지한다.
failTask 즉시-실패 (retryCount=0) 발생 카운트:
sum:datadog.log.events{service:cupixworks-capture-postprocessor-agent,status:error} by {host}.as_count()
HTTP request failed 시그니처 특정 감시 (log-based metric 생성 후):
sum:logs.transfer_manager.http_request_failed{service:cupixworks-capture-postprocessor-agent}.as_count()
capture-postprocessor 서비스의 전체 error rate (5분 rolling):
sum:datadog.log.events{service:cupixworks-capture-postprocessor-agent,status:error}.as_count().rollup(sum, 300)
임계값 알람은 failTask 카운트가 5분 내 20건 이상이면 warn, 100건 이상이면 critical 로 설정 (이번 인시던트 규모 222건 / 23초 기준).
Risk Assessment#
- Risk level: medium — 서비스 전반이 죽지 않고 단일 capture 만 실패했으나, 재시도 로직 자체가 무력화되는 결함이므로 유사 이벤트에서 언제든 재발한다. 2026-07-10 에도 동일 scope (
svc:cupixworks-capture-postprocessor-agent::unknown) 인시던트가 있었고 status board 는 재발 패턴을 감지 중. - 예상 복잡도: standard —
isRetryable확장과 로그 필드 추가는 base 패키지 한 곳 수정 + 유닛 테스트 갱신으로 커버 가능. tesla SDK 교체는 장기 트랙으로 분리.