HTTP request failed
RCA: HTTP request failed (voxel-agent updateVoxelState)
Overview#
What Happened#
2026-06-25 17:59 KST cupixworks-any-voxel-agent(ap-southeast-2)가 pointcloud 230896에 대해 PUT /api/v1/pointclouds/230896 호출로 voxel_state 전이를 시도하다 Rails API로부터 HTTP 400 (STAT40000 — State not changed)을 받아 HttpError: HTTP request failed를 throw했다. 같은 pointcloud 가 약 6시간 전(11:57 KST)에 이미 정상 처리된 흔적이 있어, SQS 재배달된 동일 메시지를 다시 처리하다 발생한 멱등성 (idempotency) 결함으로 보인다. 발생 횟수는 1건, 단일 tenant(cupix / team naylorlove)에 국한된다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError |
| exception.message | HTTP request failed |
| top_frame | node_modules/@tesla/typescript-node-sdk/.../pointcloudApi.js:3362 |
| upstream_response | 400 Cupix::Errors::InvalidState (STAT40000, "State not changed") |
| api_endpoint | PUT http://api-tesla.cupix.internal/api/v1/pointclouds/230896 |
| runtime | Node.js (filebeat 7.17.15) |
| env | production / ap-southeast-2 |
| tenant | cupix / team naylorlove |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
naylorlove (tenant cupix) |
1 | pointcloud 230896 voxel 처리가 한 차례 실패로 처리되어 voxel_state가 error로 떨어졌을 가능성. 사용자 가시적 데이터 손실은 없음 — 직전 실행이 완료 상태였다면 결과물은 유지됨. |
Timeline#
- 2026-06-25 11:57 KST —
BaseService::runByMessage | id: 230896— 첫 SQS 메시지로 pointcloud 230896 처리 시작.RealityCaptureManager::loadEntityParameters까지 정상 진입 (entity parameters length: 4). 이때updateVoxelState(Aggregating)이 먼저 실행되었음. - 2026-06-25 17:59:00 KST —
BaseService::runByMessage | id: 230896(session3ed3dd6...) — 동일 pointcloud에 대한 두 번째 처리 시작. - 2026-06-25 17:59:00 KST —
PUT /api/v1/pointclouds/230896요청, Rails API가400 STAT40000 "State not changed"응답.HttpError: HTTP request failedthrow. - 2026-06-25 17:59:01 KST —
BaseService::cleanUpAnythingRelatedModel | path: undefined(catch path 진입 후 cleanup), 이어서AwsQueueManager::deleteMessage로 메시지 정상 삭제.
Error Log#
HTTP request failed
Stack trace from Datadog raw log:
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/pointcloudApi.js:3362:40)
at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
at Request.emit (node:events:524:28)
at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1076:12)
Upstream Rails API 응답 본문 (Datadog @response.body):
{
"result": {
"reason": "State not changed",
"code": "STAT40000",
"message": "State not changed",
"type": "Cupix::Errors::InvalidState"
}
}
Impact#
- Service:
cupixworks-any-voxel-agent - Team:
naylorlove - 발생 횟수: 1
- 최초 발생: 2026-06-25 17:59 KST
- 최근 발생: 2026-06-25 17:59 KST
영향은 pointcloud 230896 한 건의 voxel 처리 실패에 국한된다. Agent의 catch 블록이 후속 updateVoxelState(Error)를 호출해 voxel_state가 error로 떨어졌을 가능성이 높다 — 즉 직전 11:57 실행이 done까지 도달했다면 그 결과가 error로 덮였을 수 있어 다운스트림(potree, mesh 등) 트리거에 부정적 영향이 있을 수 있음 (uncertain — needs verification via Kibana on pointcloud:230896.voxel_state).
Root Cause Summary#
cupixworks-any-voxel-agent(packages/cupix-tesla-voxel-agent)의 VoxelService.run()은 시작 시 realityCaptureManager.updateVoxelState(TESLA.VoxelState.Aggregating)을 무조건 호출한다. Rails API의 Parameter::VoxelModule#update_voxel_state(app/concerns/parameter/voxel_module.rb:15-18)는 요청된 state가 현재 state와 동일하면 Cupix::Errors::InvalidState(STAT40000)를 raise하여 HTTP 400을 돌려준다. SQS 메시지가 재배달되거나 (visibility timeout 초과 / 부모 워커 retry) 사용자가 동일 메시지를 다시 enqueue한 상태에서 같은 pointcloud가 다시 처리될 때, 이미 aggregating(또는 이전 실행이 멈춘 상태)인 레코드에 동일 state를 set하려 시도하면 멱등하지 않은 API 의미 때문에 400이 발생하는 구조다. Agent 측에 "이미 같은 state면 no-op"으로 처리하는 로직이 없어 SDK가 HttpError를 throw하고, VoxelService.run의 catch 가 이를 단순히 error 로그로 남긴다.
Technical Analysis#
Code Path#
- Entry point:
packages/base/src/base-service.ts:153—BaseService::runByMessage가 SQS 메시지를 받아this.run(targetId, msgObject)를 호출. - Hot path:
packages/cupix-tesla-voxel-agent/src/voxel-service.ts:52— 가장 먼저updateVoxelState(Aggregating)실행. - Failure point:
packages/cupix-tesla-voxel-agent/src/manager/reality_capture.manager.ts:91—cupixApi.pointcloud.update(id, { voxel_state: state })호출에서 SDK가 HTTP 400을 받아HttpErrorthrow. - Upstream rejection:
tesla/app/concerns/parameter/voxel_module.rb:15-18— 현재 state가 같으면STAT40000raise.
Agent 측 호출 코드:
run = async (targetId: number, msgObject?: any): Promise<void> => {
const targetType = msgObject.type ?? 'capture';
logger.debug('VoxelService::run | begin - targetId: %d, targetType: %s', targetId, targetType);
try {
const serverRealityCapture = await this.realityCaptureManager.loadRealityCapture(targetId, targetType);
const cpRealityCapture = this.realityCaptureManager.createCPRealityCapture(serverRealityCapture, targetType);
if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Aggregating);
await this.realityCaptureManager.loadEntityParameters(cpRealityCapture);
...
} catch (error: any) {
logger.error('VoxelService::run | error', error);
if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Error);
}
};
updateVoxelState = async (state: TESLA.VoxelState): Promise<void> => {
logger.debug('RealityCaptureManager::updateVoxelState | begin - %s ID: %d, state: %s', this.realityCaptureType, this.realityCaptureId, state);
if (!this.realityCaptureId || !this.realityCaptureType) {
logger.error('RealityCaptureManager::updateVoxelState | end - undefined reality capture id or type');
throw new Error('Invalid realityCaptureId or realityCaptureType');
}
if (this.realityCaptureType === 'capture') {
this._srvRealityCapture = await this.cupixApi.capture.update(this.realityCaptureId, { voxel_state: state });
} else if (this.realityCaptureType === 'pointcloud') {
this._srvRealityCapture = await this.cupixApi.pointcloud.update(this.realityCaptureId, { voxel_state: state });
} else {
...
}
};
Rails API의 거부 지점:
def update_voxel_state(params = {})
if params[:pano_voxel_state].present? || params[:voxel_state].present?
_voxel_state = params[:voxel_state] || params[:pano_voxel_state]
case _voxel_state
when 'stale'
raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.voxel_state_stale?
@model.stale_voxel_state!
when 'aggregating'
raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.voxel_state_aggregating?
@model.aggregating_voxel_state!
when 'error'
raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.voxel_state_error?
@model.error_voxel_state!
when 'done'
raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.voxel_state_done?
@model.done_voxel_state!
else
raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: "Invalid state: #{params[:voxel_state]}")
end
end
end
State machine 정의 (tesla/app/models/concerns/voxel_module/reality_capture.rb:12-46)는 any => :aggregating을 허용하므로 DB 레벨에서는 전이 자체가 가능하다. 거부는 parameter 계층에서 "동일 state 재설정"을 명시적으로 막는다.
기대 동작: Agent가 두 번째 SQS 메시지를 받으면 이미 처리된 작업으로 인식하고 skip 또는 멱등하게 재실행해야 한다.
실제 동작: Agent는 두 번째 메시지에서도 updateVoxelState(Aggregating)을 호출하고, 이미 aggregating이면 API가 400을 돌려준다. SDK 가 HttpError를 throw → catch에서 updateVoxelState(Error)로 전이시켜 voxel_state를 error로 강제 변경.
Log Evidence#
Datadog 쿼리 (재현):
service:cupixworks-any-voxel-agent status:error "HTTP request failed"
service:cupixworks-any-voxel-agent @pointcloud.id:230896
위 쿼리(@pointcloud.id:230896, 7일 윈도우)는 같은 pointcloud에 대한 두 차례 SQS 처리 흔적을 보여준다:
2026-06-25 11:57:22 info BaseService::runByMessage | id: 230896
2026-06-25 11:57:22 info CupixAuth::setSession | session_id: 3ed3dd6642a93a288c7ed23ba76a8b49f1ad8247
2026-06-25 11:57:23 info RealityCaptureManager::loadEntityParameters | entity parameters length: 4
2026-06-25 11:57:23 info CPRealityCapture::setCaptureVoxelsParams | pointcloud ID: 230896 - voxelSize: 1, ...
...
2026-06-25 17:59:00 info BaseService::runByMessage | id: 230896
2026-06-25 17:59:00 info CupixAuth::setSession | session_id: 3ed3dd6642a93a288c7ed23ba76a8b49f1ad8247
2026-06-25 17:59:00 error HTTP request failed
2026-06-25 17:59:01 info BaseService::cleanUpAnythingRelatedModel | path: undefined
2026-06-25 17:59:01 warn BaseService::cleanUpAnythingRelatedModel | end - undefined modelDirPath
2026-06-25 17:59:01 info AwsQueueManager::deleteMessage | begin - queue url: https://sqs.ap-southeast-2.amazonaws.com/.../cupix-tesla-voxel-agent-production
2026-06-25 17:59:01 info AwsQueueManager::deleteMessage | end - message id: d8c02659-299b-483d-ab1c-f6967f313d4a
핵심 단서: 두 처리 모두 session_id 가 동일(3ed3dd66...) — 같은 origin 워커가 enqueue 한 동일 메시지(또는 그 재시도)일 가능성이 높다. 두 번째 처리에서는 loadEntityParameters 라인이 없다 — 즉 loadEntityParameters 이전 라인인 updateVoxelState(Aggregating)에서 throw 되어 catch로 빠진 흐름과 일치.
PUT 요청 raw (Datadog @response.request):
PUT http://api-tesla.cupix.internal/api/v1/pointclouds/230896?fields[0]=id&fields[1]=name&fields[2]=state&fields[3]=resource_state&fields[4]=potree_state&fields[5]=octree_state&fields[6]=cpc_mesh_state&...
content-length: 29
X-CUPIX-AUTH: session_token:6b60ah09kyky,session_id:396663
요청 본문 길이가 29 바이트 — {"voxel_state":"aggregating"} (29자) 와 정확히 일치한다. 즉 두 번째 호출이 voxel_state=aggregating 으로 동일 state를 다시 시도하다 거부된 케이스.
{
"result": {
"reason": "State not changed",
"code": "STAT40000",
"message": "State not changed",
"type": "Cupix::Errors::InvalidState"
}
}
400
서비스 전체 24시간 검색 결과 동일 메시지(HTTP request failed)는 1건뿐 — 시스템적 다발 이슈가 아닌 단일 재처리 케이스로 확인된다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 동일 SQS 메시지의 재배달/재처리로 인해 이미 aggregating인 pointcloud에 또 aggregating PUT을 보내 API가 STAT40000을 raise. Agent에 멱등성 가드가 없음. |
(1) Datadog raw @response.body = Cupix::Errors::InvalidState STAT40000. (2) 동일 pointcloud.id:230896/동일 session id로 11:57·17:59 두 차례 runByMessage 발생. (3) PUT body length 29 = {"voxel_state":"aggregating"}. (4) 두 번째 실행은 loadEntityParameters 라인 없이 catch path로 직행 — voxel-service.ts:52에서 throw 된 것과 일치. (5) parameter/voxel_module.rb:16 정확히 동일 reason 문자열. |
— | Confirmed |
| H2 | api-tesla 의 일시적 5xx (네트워크/Rails 오류)로 인한 generic HTTP 실패. | 메시지가 generic HttpError: HTTP request failed. |
@response.statusCode = 400 (5xx 아님), body 가 application-level error code STAT40000 명시. 동일 시간대 다른 서비스에 HTTP request failed 발생 없음. |
Rejected |
| H3 | Datadog 로깅 누락/체이닝 문제로 같은 cluster에 다른 원인 에러가 섞임. | — | 단일 fingerprint, 단일 로그 ID(AwAAAZ7-AQeq7rhB3gAAA...), 24h 윈도우에 동일 메시지 1건. |
Rejected |
| H4 | 다른 워커(여러 pod)가 동시에 pointcloud 230896 처리 시도(race) — visibility timeout 내 동시 dispatch. | 동일 session id 가 두 번 출현하긴 하지만 timestamp 가 6시간 차이라 race 보단 redelivery에 가까움. | 11:57 → 17:59 간격(~6h)은 일반 SQS visibility window(분 단위)와 맞지 않음 — 부모 워커의 명시적 재enqueue 가능성이 더 큼. | Inconclusive — needs verification via SQS message attributes (ApproximateReceiveCount) |
Fix Recommendation#
즉시 조치 (Critical)#
- 운영적 대응 불필요 — 단일 1회 발생이며 사용자 영향이 없거나 미미. pointcloud
230896의 현재voxel_state를 Kibana로 확인 (pointcloud.id:230896on tesla index). 만약error로 강제 전이되었다면 운영자가 수동으로 reset 후 재enqueue.
단기 개선 (1주 이내)#
- Agent 측 멱등성 가드:
applications/agents/packages/cupix-tesla-voxel-agent/src/voxel-service.ts:52의updateVoxelState(Aggregating)직전에serverRealityCapture.voxel_state === 'aggregating'(또는done) 이면 skip하는 가드를 둔다. 이미cpRealityCapture = ...직전에 server 상태를 가져왔으므로 추가 fetch 없이 가능.done의 경우는 메시지 처리를 곧장 skip + SQS delete. - Server 측 API 의미 재검토:
tesla/app/concerns/parameter/voxel_module.rb의 "동일 state면 400" 정책이 정말 필요한지 재평가. agent · worker 가 멱등 호출하는 패턴이 표준이라면 동일 state 요청을 200 no-op 으로 처리하는 편이 분산 환경에서 안전. 변경하면 caller 중 명시적 conflict 처리가 필요한 곳(예: 사용자 액션 UI)이 영향받을 수 있어 호출자 그루핑 분석 선행. - 로그 레벨/메시지 보강: Agent catch 블록(
voxel-service.ts:64)의logger.error('VoxelService::run | error', error)—error.response?.body?.result?.code === 'STAT40000'인 경우는warn으로 다운그레이드하고pointcloud.id,requested_state,current_state,messageId,approximateReceiveCount등을 구조화 필드로 추가. error sweeper noise 감소 + 디버깅 가속.
장기 개선 (재발 방지)#
- SQS 메시지 dedupe / processing token: 동일 (id, type) 메시지의 6시간 내 재처리를 차단하기 위해 Redis 기반 short-window dedupe 또는 Rails 측 idempotency token(요청 헤더
Idempotency-Key) 도입. - State transition policy 문서화: 각 voxel_state 의 허용 전이와 멱등 약속을
docs/에 명시하고, agent / worker 간 계약을 단일 source of truth 로 통일. - Agent 멱등성 표준 패턴: voxel-agent 외 다른 reality-capture agent 들(
cupix-tesla-thumbnail-agent,cupix-tesla-mesh-agent등)도 동일 패턴(updateState(Working)무조건 호출)을 가지면 같은 문제 발생 가능 — 공통 base class(BaseService)에서 멱등 헬퍼를 제공.
Monitoring#
추가할 메트릭/알림:
cupixworks-any-voxel-agent의 STAT40000 발생률- voxel-agent SQS 큐의
ApproximateNumberOfMessagesNotVisible/ 메시지 재배달 비율 - pointcloud
voxel_state가error로 전이되는 카운트
Datadog 쿼리 (release dashboard timeseries widget 용):
sum:trace.error.hits{service:cupixworks-any-voxel-agent}.as_count()
logs("service:cupixworks-any-voxel-agent status:error \"HTTP request failed\"").index("*").rollup("count").by("region").last("1d")
logs("service:cupixworks-any-voxel-agent \"STAT40000\"").index("*").rollup("count").last("7d")
logs("service:cupixworks-any-voxel-agent \"VoxelService::run | error\"").index("*").rollup("count").by("@pointcloud.id").last("1d")
Risk Assessment#
- Risk level: low — 단일 발생, 단일 tenant, 사용자 영향 미미. 코드 fix(멱등성 가드) 자체는 작고 명확.
- 예상 복잡도: trivial — agent 측 1
3줄 조건문 추가 + 단위 테스트 12건. API 측 변경을 함께 한다면 standard 수준(호출자 분석 필요).