ES /docs

TransferManager::failTask | path: /tmp/workspace/rabren.739490_result/rabren.739490_1463351_ref_plan

RCA: TransferManager::failTask | ref_planes.json HTTP request failed

Overview#

What Happened#

3d-reconstruction agent가 rabren.739490 capture의 ref_planes.json pointcloud resource 를 업로드하기 위해 POST /api/v1/pointclouds/1233204/resources 를 호출했다. 최초 호출은 client-side 에서 ECONNABORTED 로 abort 되었지만 tesla 서버는 이미 plane kind resource 를 생성한 상태였다. 1초 뒤 재시도가 같은 요청을 반복했고, tesla 가 HTTP 400 Duplicate kind: plane 을 반환했으며 TransferManager 는 400 은 retryable 이 아니므로 count: 1/5 에서 failTask 를 호출하고 task 를 실패 처리했다.

Quick Facts#

Field Value
exception.class HttpError
exception.message HTTP request failed (server body: Duplicate kind: plane, code ARG10001)
top_frame node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:1040:40
runtime node (agent bundle /tmp/agent/dist)
deploy @tesla/typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
rabren (team.id 1242) 1 Capture 739490 의 3d reconstruction 결과 upload 실패 — pointcloud 1233204 의 ref_planes resource 가 client 관점에서 실패 상태로 종료

Timeline#

  1. 2026-07-22 10:47:30 KST — 최초 createResource(POST /pointclouds/1233204/resources, kind=plane) 호출이 ECONNABORTED 로 실패. TransferManager::retryTask 가 1000ms 뒤 재시도 예약 (count: 1/5, code: ECONNABORTED).
  2. 2026-07-22 10:47:31 KST — Retry 로 renew()init() 재실행. Server(tesla) 가 HTTP 400 Duplicate kind: plane (ARG10001) 반환. HttpError: HTTP request failed 로 raise (log id AwAAAZ-HgbFCuHYqjg...).
  3. 2026-07-22 10:47:31 KSTTransferManager::failTask 발화 (count: 1/5, code: undefined, message: HTTP request failed), 이어 BaseTaskContainer::checkTaskFail | Total: 1, Done: 0, Failed: 1.

Error Log#

Datadog Logs

text
TransferManager::failTask | path: /tmp/workspace/rabren.739490_result/rabren.739490_1463351_ref_planes.json, url: https://s3.amazonaws.com/cupixworks-source-dc9dcff32488-usea1/resources/epyquq/usea1/v1?x-amz-acl=bucket-owner-full-control&x-amz-storage-class=ONEZONE_IA&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQBGWD5FH5URQYYOO%2F20260722%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260722T014731Z&X-Amz-Expires=7200&X-Amz-SignedHeaders=host&X-Amz-Signature=e048e8a0aa7554def8082331da6ca27603ac8f02dd2bbdbf5c332c2913053471, count: 1/5, code: undefined, message: HTTP request failed

Impact#

  • Service: cupixworks-capture-3dreconstruction-instance
  • Team: rabren
  • 발생 횟수: 1
  • 최초 발생: 2026-07-22 10:47:31 KST
  • 최근 발생: 2026-07-22 10:47:31 KST

Capture 739490 의 pointcloud 1233204 에 대한 ref_planes 산출물이 업로드되지 못했다. 후속 3d reconstruction 결과 조합 단계가 이 resource 를 필요로 하면 finalize 도 실패하여 전체 job 이 실패로 종료된다. 발생 횟수는 1건이지만, 원인이 "abort 된 create request 뒤에 서버는 이미 리소스를 생성" 이라는 비멱등 재시도 패턴이라 네트워크가 불안정한 세션에서 재발 가능성이 있다.

Root Cause Summary#

원본 로그의 표면 메시지("HTTP request failed") 는 message 만 담고 있어 오해를 부르지만, Datadog raw payload 의 response.body.result 를 보면 실제 응답은 HTTP 400 Duplicate kind: plane (Cupix::Errors::Parameter, code ARG10001) 이다. 흐름은 다음과 같다: 최초 UploadRefPlanesPointcloudTask.init()POST /pointclouds/:id/resources 를 호출했지만 client 가 응답 수신 전에 ECONNABORTED 로 소켓을 끊었고, tesla 는 이미 요청을 처리하여 plane kind resource 를 DB 에 만들었다. TransferManager::retryTaskECONNABORTED 가 retryable code 라서 1s 뒤 재시도했고, retry 는 renew() 를 호출한다. 그런데 _resource === undefined (첫 시도가 client 쪽에선 실패이므로) 이라 renewinit() 을 다시 실행해 createResource 를 재호출했다. Tesla resource_factory.rb:13 / multiple_resourcable_controller.rb:53 은 같은 kind resource 가 이미 있으면 ARG10001 Duplicate kind: plane 을 400 으로 반환한다. 400 은 isRetryable 기준(5xx or RETRYABLE_CODES)에 해당하지 않아 failTask 로 종료된다. 즉 root cause 는 tesla 의 non-idempotent 한 create_resource 엔드포인트와 그것을 그대로 재호출하는 agent 의 renew 폴백 로직 조합이다.

Technical Analysis#

Code Path#

Entry point: UploadRefPlanesPointcloudTask.init (첫 호출) — POST /api/v1/pointclouds/:id/resources 로 kind=plane resource 를 생성 요청.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-ref-planes-pointcloud.task.ts:29-42typescript
init = async (): Promise<void> =>  {
    const resourceFileName = path.basename(this.target as string);
    const resourceData = await this._container.cupixApi.pointcloud.createResource(this.pointcloudId, {
        name: resourceFileName,
        kind: TESLA.CreatePointcloudResourceRequest.KindEnum.Plane
    });
    if (resourceData == undefined) {
        throw new Error(...);
    } else {
        this._resource = resourceData;
        ...
    }
};

Retry 판정: BaseTransferManager.retryTaskcanRetry 를 호출하고, canRetryisRetryable(error) && task.retry() 로 결정한다.

applications/agents/packages/base/src/util/transfer.ts:1-8typescript
const RETRYABLE_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'EAI_AGAIN'];
applications/agents/packages/base/src/util/transfer.ts:67-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;
};

Retry 진입 시 renew 폴백: _resource 가 아직 undefined 이므로 init 이 다시 실행되어 createResource 를 재호출한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-ref-planes-pointcloud.task.ts:44-56typescript
renew = async (): Promise<void> => {
    ...
    if (this._resource == undefined) {
        await this.init();
    } else {
        const resourceData = await this._container.cupixApi.pointcloud.createResourceUploadUrl(this.pointcloudId, this.resourceKind, {});
        ...
        this._resource = resourceData;
    }
};

Failure point (server): tesla 의 controller 가 이미 존재하는 kind 를 감지하고 400 을 던진다.

app/controllers/concerns/multiple_resourcable_controller.rb:52-53ruby
def create_resource
  raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Duplicate kind: #{params[:kind]}") and return unless @model.resources.find_by_kind(params[:kind]).blank?
app/factories/resource_factory.rb:12-14ruby
if params[:kind].present?
  raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Duplicate kind: #{params[:kind]}") unless self.parent.resources.find_by_kind(params[:kind]).blank?
end

Failure point (client): SDK 는 400 응답을 HttpError 로 감싸는데 이 에러 객체에는 code 필드가 없고 statusCode 만 있다. isRetryableerror.statusCode 를 참조하도록 되어 있어 statusCode=400 은 5xx 미만 → false. retryTaskcanRetry === false 로 판단하고 즉시 failTask 를 호출한다.

applications/agents/packages/base/src/manager/transfer.manager.ts:130-142typescript
private failTask = (task: ITransferTask, error: any): void => {
    ...
    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);
};

기대 동작: create 가 abort 된 뒤 재시도할 때 tesla 상태와 동기화하여 이미 존재하는 resource 를 재사용하고 새 upload URL 을 발급받아야 한다. 실제 동작: 무조건 create 재시도 → 400 → non-retryable 로 종료.

Log Evidence#

Datadog query:

text
service:cupixworks-capture-3dreconstruction-instance status:error
from: 2026-07-22T01:00:00Z, to: 2026-07-22T02:30:00Z

Retry (선행 로그, retryCount=1 로 증가하며 예약):

text
2026-07-22 10:47:30 KST  info
TransferManager::retryTask | retrying after 1000 ms - path: /tmp/workspace/rabren.739490_result/rabren.739490_1463351_ref_planes.json, url: https://s3.amazonaws.com/.../..., count: 1/5, code: ECONNABORTED

Retry 결과 (raw log 의 attributes 에서 실제 응답 확인):

Datadog log AwAAAZ-HgbFCuHYqjg... (level: error)json
{
  "message": "HTTP request failed",
  "name": "HttpError",
  "statusCode": 400,
  "stack": "HttpError: HTTP request failed\n    at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.../api/pointcloudApi.js:1040:40)\n    ...",
  "response": {
    "statusCode": 400,
    "request": {
      "method": "POST",
      "uri": {
        "hostname": "api-tesla.cupix.internal",
        "pathname": "/api/v1/pointclouds/1233204/resources"
      }
    },
    "body": {
      "result": {
        "reason": "Duplicate kind: plane",
        "code": "ARG10001",
        "type": "Cupix::Errors::Parameter",
        "message": "Duplicate kind: plane"
      }
    }
  },
  "capture": { "id": 739490 },
  "job": { "id": 1215958 },
  "team": { "domain": "rabren", "id": 1242 },
  "user": { "id": 52040, "email": "garrettcalhoun@rabren.com" }
}

TransferManager 표면 로그 (cluster fingerprint 의 대표 메시지):

text
2026-07-22 10:47:31 KST  error
TransferManager::failTask | path: /tmp/workspace/rabren.739490_result/rabren.739490_1463351_ref_planes.json, url: https://s3.amazonaws.com/.../..., count: 1/5, code: undefined, message: HTTP request failed

code: undefined 인 이유: SDK 의 HttpErrorerror.code 를 세팅하지 않고 error.statusCode(=400)와 error.message("HTTP request failed") 만 세팅한다. 그래서 failTask 의 log formatter (error?.code) 는 undefined 를 출력했지만, 실제로는 5xx 가 아닌 4xx 상태를 가진 에러라 isRetryable 이 false 로 판정한 것이다.

동일 시간대에 다른 job 도 실패했지만 원인이 다르다 (scope confirmation):

text
2026-07-22 10:52:29 KST  error
TransferManager::failTask | path: /tmp/workspace/crcc-sama.43835_result/crcc-sama.43835_131377.cpc, url: https://s3.me-central2.cupixstorages.com/..., count: 5/5, code: ERR_BAD_RESPONSE, message: Request failed with status code 503

crcc-sama 케이스는 me-central2 minio 가 503 을 반환하여 5회 재시도까지 소진한 별개 케이스(다른 fingerprint 후보). 본 cluster (rabren.739490) 와는 원인/스토리지/리전이 모두 다르다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 presigned PUT 이 실패해서 upload 가 안 됐다 log 표면에 url: https://s3.amazonaws.com/... 가 노출됨 Stack trace 가 pointcloudApi.js:1040 (SDK 의 tesla API 클라이언트) 이고, response.request.uri.hostnameapi-tesla.cupix.internal. S3 요청이 아니라 tesla API 호출이 실패한 것 Rejected
H2 Tesla API 500 계열 outage response.statusCode: 400 이며 서버가 정상적으로 domain error(ARG10001)를 반환. status-board 도 dep:tesla / dep:s3 active incident 없음 Rejected
H3 최초 create 요청이 abort 됐지만 서버는 이미 리소스를 생성했고, 재시도가 그대로 create 를 재호출해 Duplicate kind: plane (400) 발생. 400 은 non-retryable → 즉시 failTask 선행 log count: 1/5, code: ECONNABORTED (10:47:30), 후행 log 의 response.body.result.reason: "Duplicate kind: plane", statusCode: 400. Code (resource_factory.rb:13, multiple_resourcable_controller.rb:53) 로 tesla 의 동작 확인 Confirmed
H4 Agent 가 최대 재시도(5회) 를 소진한 뒤 실패 로그에 count: 1/5 로 명시 → 첫 재시도에서 즉시 실패 Retry 카운트가 5 가 아닌 1. isRetryable(400) === false 라 카운트 증가 없이 종료 Rejected
H5 S3 me-central2 minio 503 outage 의 영향 같은 시간대에 me-central2 에서 503 이 여러 건 발생 본 cluster 의 S3 host 는 s3.amazonaws.com (us-east-1), me-central2 와 별개. 실제 실패도 S3 가 아닌 tesla API 였음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-ref-planes-pointcloud.task.ts:44-56 (renew) 및 형제 파일들 (upload-new-pointcloud.task.ts, upload-octree-pointcloud.task.ts, upload-mesh-pointcloud.task.ts, upload-ply-pointcloud.task.ts, upload-cpc-mesh-pointcloud.task.ts, upload-semantic-taxonomy-pointcloud.task.ts 등 3d-reconstruction-agent 내 create-then-upload task 전반) 의 renew 로직에 아이덤포턴시를 부여한다. 접근 방식: _resource == undefined 인 경우에도 곧바로 createResource 를 재호출하지 말고, 먼저 list/find-by-kind 계열 API 로 같은 kind 가 이미 존재하는지 조회한 뒤 존재하면 그 resource 로 _resource 를 세팅하고 createResourceUploadUrl 경로로 넘어가도록 한다. 이 변경만으로 이번 클래스의 경합을 해소한다.
  • 대안 (또는 병행): applications/agents/packages/base/src/util/transfer.ts:67-72 isRetryable 은 그대로 두되, renew() 내부에서 400 Duplicate kind 를 명시적으로 catch 하여 existing resource 조회 후 upload URL 재발급 경로로 전환. server contract 변경 없이 client 측만으로 처리 가능.
  • 프런트엔드/외부 조율 필요 없음, 순수 agent 코드 변경.

단기 개선 (1주 이내)#

  • Tesla 측 POST /api/v1/pointclouds/:id/resources 를 idempotent 하게 만드는 옵션 검토: 요청 헤더에 Idempotency-Key (agent 가 task 별 UUID 발급) 를 받고, 동일 key 재요청 시 기존 리소스를 200 으로 반환. app/controllers/concerns/multiple_resourcable_controller.rb:52-53 의 duplicate 체크를 idempotency 캐시 hit 시 skip. 이 변경은 pointcloud 뿐 아니라 다른 resourcable (capture, pano 등) 도 동일 문제 소지가 있으므로 concern 레벨에서 통일하는 편이 낫다.
  • Agent 의 HttpError 로깅 개선: failTask 가 log format 에 error?.statusCodeerror?.response?.body 요약을 포함하도록 확장. 현재는 code: undefined, message: HTTP request failed 만 남아 debugging 이 매우 어려움. applications/agents/packages/base/src/manager/transfer.manager.ts:130-142 참조.

장기 개선 (재발 방지)#

  • Agent 의 network layer 를 axios 로 완전 통일하고 axios.interceptors 의 자동 재시도(transfer.ts:12-33)와 TransferManager 의 상위 재시도(transfer.manager.ts:143-165)를 통합. 현재 두 재시도 계층이 서로 다른 정책(각각 3회 / 5회, 서로 다른 backoff)을 가진다. 하나의 정책으로 통일하고, 재시도 시 서버 side effect 가능성을 명시적으로 다루는 상태머신(created→uploading→done)에 매핑한다.
  • Chaos test: POST /resources 응답을 client 측에서 강제로 abort 시키는 시나리오를 3d-reconstruction agent E2E 에 추가하여 이 경합을 CI 에서 회귀 검증.

Monitoring#

  • Duplicate kind 로 종료되는 재시도 케이스 추적.
text
service:cupixworks-capture-3dreconstruction-instance "Duplicate kind"
  • 근본 실패 지표: create resource 재시도가 첫 시도에서 즉시 실패한 case.
text
service:cupixworks-capture-3dreconstruction-instance "TransferManager::failTask" "count: 1/5"
  • 상관 지표: agent → tesla-api 간 ECONNABORTED 발생 빈도.
text
service:cupixworks-capture-3dreconstruction-instance "code: ECONNABORTED"

Risk Assessment#

  • Risk level: medium — 단일 이벤트지만 원인이 재시도 로직 자체의 결함이라 네트워크 flakiness 가 있을 때 반복 재발 가능. 실패 시 capture 전체가 진행되지 못해 사용자 가시적 영향이 있음.
  • 예상 복잡도: standard — renew 경로에 lookup 한 단계 추가 또는 400 catch 분기 추가. 형제 task 파일들에도 동일 패턴 반영이 필요해 파일 수는 다수지만 각 파일 변경은 소규모.