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#
- 2026-07-22 10:47:30 KST — 최초
createResource(POST /pointclouds/1233204/resources, kind=plane)호출이ECONNABORTED로 실패.TransferManager::retryTask가 1000ms 뒤 재시도 예약 (count: 1/5, code: ECONNABORTED). - 2026-07-22 10:47:31 KST — Retry 로
renew()→init()재실행. Server(tesla) 가HTTP 400 Duplicate kind: plane(ARG10001) 반환.HttpError: HTTP request failed로 raise (log idAwAAAZ-HgbFCuHYqjg...). - 2026-07-22 10:47:31 KST —
TransferManager::failTask발화 (count: 1/5, code: undefined, message: HTTP request failed), 이어BaseTaskContainer::checkTaskFail | Total: 1, Done: 0, Failed: 1.
Error Log#
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::retryTask 는 ECONNABORTED 가 retryable code 라서 1s 뒤 재시도했고, retry 는 renew() 를 호출한다. 그런데 _resource === undefined (첫 시도가 client 쪽에선 실패이므로) 이라 renew 가 init() 을 다시 실행해 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 를 생성 요청.
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.retryTask 가 canRetry 를 호출하고, canRetry 는 isRetryable(error) && task.retry() 로 결정한다.
const RETRYABLE_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'EAI_AGAIN'];
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 를 재호출한다.
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 을 던진다.
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?
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 만 있다. isRetryable 은 error.statusCode 를 참조하도록 되어 있어 statusCode=400 은 5xx 미만 → false. retryTask 는 canRetry === false 로 판단하고 즉시 failTask 를 호출한다.
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:
service:cupixworks-capture-3dreconstruction-instance status:error
from: 2026-07-22T01:00:00Z, to: 2026-07-22T02:30:00Z
Retry (선행 로그, retryCount=1 로 증가하며 예약):
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 에서 실제 응답 확인):
{
"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 의 대표 메시지):
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 의 HttpError 는 error.code 를 세팅하지 않고 error.statusCode(=400)와 error.message("HTTP request failed") 만 세팅한다. 그래서 failTask 의 log formatter (error?.code) 는 undefined 를 출력했지만, 실제로는 5xx 가 아닌 4xx 상태를 가진 에러라 isRetryable 이 false 로 판정한 것이다.
동일 시간대에 다른 job 도 실패했지만 원인이 다르다 (scope confirmation):
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.hostname 이 api-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-72isRetryable은 그대로 두되,renew()내부에서 400Duplicate 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?.statusCode와error?.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 로 종료되는 재시도 케이스 추적.
service:cupixworks-capture-3dreconstruction-instance "Duplicate kind"
- 근본 실패 지표: create resource 재시도가 첫 시도에서 즉시 실패한 case.
service:cupixworks-capture-3dreconstruction-instance "TransferManager::failTask" "count: 1/5"
- 상관 지표: agent → tesla-api 간 ECONNABORTED 발생 빈도.
service:cupixworks-capture-3dreconstruction-instance "code: ECONNABORTED"
Risk Assessment#
- Risk level: medium — 단일 이벤트지만 원인이 재시도 로직 자체의 결함이라 네트워크 flakiness 가 있을 때 반복 재발 가능. 실패 시 capture 전체가 진행되지 못해 사용자 가시적 영향이 있음.
- 예상 복잡도: standard — renew 경로에 lookup 한 단계 추가 또는 400 catch 분기 추가. 형제 task 파일들에도 동일 패턴 반영이 필요해 파일 수는 다수지만 각 파일 변경은 소규모.