ES /docs

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#

  1. 2026-07-14 03:53:31 KST — 첫 wave: 동일 workspace의 원본 프레임 5개가 me-central-1 S3에 업로드되던 중 ERR_BAD_REQUEST / Request failed with status code 400 로 실패 (sibling cluster ebd3578b-b22a-45ac-a6d3-fcddb1183a75).
  2. 2026-07-14 03:57:03 KST — 두 번째 wave 시작: HTTP request failed 시그니처의 failTask 로그가 발생하기 시작. 최초 first_seen.
  3. 2026-07-14 03:57:23–26 KST — 3초 동안 222건이 폭발적으로 기록. url: undefined, retryCount: 0로 즉시 종결.
  4. 2026-07-14 03:57:26 KST — 마지막 실패 (last_seen). 이후 다른 workspace(734126, 734053 등) 처리가 정상 진행되어 서비스 자체는 살아있음.

Error Log#

Datadog Logs

text
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.transferTaskcatch(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 이 세팅되었는지 검사한다:

applications/agents/packages/base/src/manager/transfer.manager.ts:180-201typescript
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.urlundefined 다:

applications/agents/packages/cupix-capture-postprocessor-agent/src/manager/transfer/upload-new-panos.task.ts:21-45typescript
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 가 예외를 던지면 transferTaskcatch(error) 로 잡혀 retryTask 가 호출된다. retryTaskcanRetry(task, error) (기본 구현 = isRetryable(error)) 로 재시도 여부를 판단한다:

applications/agents/packages/base/src/manager/transfer.manager.ts:159-178typescript
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 를 기준으로 만들어졌다:

applications/agents/packages/base/src/util/transfer.ts:66-72typescript
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의 HttpErrorcoderesponse.status 도 세팅하지 않는다 — 콜백이 에러를 받은 경우 response 자체가 undefined 상태로 넘어올 수 있다:

repos/cupix-api/client/openapi/typescript-node/tesla-v1/api/apis.ts:197-202typescript
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 즉시 호출 → 로그 남김:

applications/agents/packages/base/src/manager/transfer.manager.ts:146-158typescript
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 원본):

text
service:cupixworks-capture-postprocessor-agent status:error @environment:production "TransferManager::failTask"

주 실패 로그 (첫 로그):

text
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):

text
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 에러를 함께 로깅한 항목:

text
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 로그 (다른 시각의 다른 프로세스):

text
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 측 오류 부재 확인:

text
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, isRetryablecode/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 의 urlundefined 이므로 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.tsisRetryable 이 tesla-v1 SDK HttpError (name HttpError, response 미정의) 케이스를 재시도 대상으로 인식하도록 확장한다. 방향: error?.name === 'HttpError' 이고 error?.statusCode == null || error.statusCode >= 500 이면 retryable 로 취급. 또한 axios ECONNREFUSED, ERR_NETWORK 등 자주 발생하는 클라이언트 실패 코드도 검토 후 추가.
  • applications/agents/packages/cupix-capture-postprocessor-agent/src/manager/transfer/upload-new-panos.task.ts:23-45 init() / 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주 이내)#

  • retryTaskretryCount === 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) 발생 카운트:

text
sum:datadog.log.events{service:cupixworks-capture-postprocessor-agent,status:error} by {host}.as_count()

HTTP request failed 시그니처 특정 감시 (log-based metric 생성 후):

text
sum:logs.transfer_manager.http_request_failed{service:cupixworks-capture-postprocessor-agent}.as_count()

capture-postprocessor 서비스의 전체 error rate (5분 rolling):

text
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 교체는 장기 트랙으로 분리.