ES /docs

VoxelService::run | end - error: "Request failed with status code 400"

RCA: VoxelService::run | end - error: "Request failed with status code 400"

Error Log#

Datadog Logs

text
VoxelService::run | end - error: "Request failed with status code 400"

Impact#

  • Service: cupixworks-any-voxel-agent
  • 발생 횟수: 1
  • 최초 발생: 2026-04-06T08:03:50.066Z
  • 최근 발생: 2026-04-06T08:03:50.066Z

Root Cause Summary#

Voxel agent가 pointcloud ID 1018363에 대해 voxel_stateaggregating으로 변경한 후 voxel 계산 처리 중 실패가 발생했고, catch 블록에서 voxel_stateerror로 변경하려 할 때 Tesla API가 STAT40000 ("State not changed") 에러를 반환했다. 이는 voxel_module.rb의 중복 상태 전환 방지 로직(raise if @model.voxel_state_error?)에 의해 발생한 것으로, pointcloud가 동시에 실행된 다른 프로세스(또는 중복 SQS 메시지)에 의해 이미 error 상태로 전환되었기 때문이다. Production 큐(cupix-tesla-voxel-agent-production)에서 수신된 메시지를 처리하는 과정에서 발생했으며, 동일 agent 인스턴스가 QA 큐와 production 큐를 동시에 처리하는 구조가 근본 원인이다.

Technical Analysis#

Code Path#

  • Entry point: voxel-service.ts:46VoxelService.run() 메서드
  • voxel-service.ts:50loadRealityCapture(targetId, 'pointcloud') → Tesla API GET 호출, pointcloud 1018363 로드 성공 (200)
  • voxel-service.ts:53updateVoxelState(TESLA.VoxelState.Aggregating) → Tesla API PUT 호출, aggregating 상태로 변경 성공 (200)
  • voxel-service.ts:54-62 — voxel 계산 및 업로드 처리 중 실패 발생 (구체적 실패 지점은 info 레벨 이상 로그에 기록되지 않음)
  • Failure point: voxel-service.ts:66 — catch 블록에서 updateVoxelState(TESLA.VoxelState.Error) 호출 시 Tesla API가 400 반환
typescript
// voxel-service.ts:46-68
run = async (targetId: number, msgObject?: any): Promise<void> => {
    const targetType = msgObject.type ?? 'capture';
    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);
        await this.realityCaptureManager.loadSubModels(cpRealityCapture);

        const result = await this.voxelManager.calculateVoxels(cpRealityCapture);
        // ... save and upload ...
    } catch (error: any) {
        logger.error('VoxelService::run | end - error: %s', JSON.stringify(error.message));
        if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Error);
    }
};

updateVoxelStatereality_capture.manager.ts:81-97에서 Tesla API의 pointcloud update 엔드포인트를 호출한다:

typescript
// reality_capture.manager.ts:81-97
updateVoxelState = async (state: TESLA.VoxelState): Promise<void> => {
    // ...
    if (this.realityCaptureType === 'pointcloud') {
        this._srvRealityCapture = await this.cupixApi.pointcloud.update(this.realityCaptureId, { voxel_state: state });
    }
    // ...
};

Tesla API 서버 측에서는 voxel_module.rb:6-31update_voxel_state 메서드가 중복 상태 전환을 차단한다:

ruby
# app/concerns/parameter/voxel_module.rb:6-30
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 '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!
    # ...
    end
  end
end

error 상태로의 전환이 거부된 것은 pointcloud가 이미 error 상태였음을 의미한다. 이는 동일 pointcloud에 대한 다른 처리 프로세스가 먼저 error 상태를 설정했거나, 이전 실패 처리에서 이미 error 상태로 전환된 후 SQS 메시지가 재처리된 경우를 나타낸다.

Log Evidence#

Datadog 쿼리 (voxel agent 로그):

text
service:cupixworks-any-voxel-agent
Time range: 2026-04-06T08:03:45Z to 2026-04-06T08:03:55Z

핵심 로그 타임라인 (에러 발생 전후):

text
2026-04-06 17:03:49 KST [info]  CupixAuth::setSession | session_id: 5f1d2e5233cdf8904505a2bebe08333790938fa1
2026-04-06 17:03:49 KST [info]  AwsS3Manager::constructor | region: us-west-2
2026-04-06 17:03:50 KST [warn]  CupixAuth::handleError | Response statusCode: 400, requestUriHref: http://api-tesla.cupix.internal/api/v1/pointclouds/1018363?fields=id%2Cname%2Ckind%2Cpointcloud_type%2Cstate%2Cpotree_state%2Cparent%2Crecord%2Clevel%2Cpotree_url%2Centry_filename%2Cvoxel_state%2Cvoxels_result_urls%2Cmeta%2Ccreated_at, body.result: {"code":"STAT40000","type":"Cupix::Errors::InvalidState","reason":"State not changed","message":"State not changed"}
2026-04-06 17:03:50 KST [error] VoxelService::run | end - error: "Request failed with status code 400"
2026-04-06 17:03:50 KST [info]  BaseService::cleanUpAnythingRelatedModel | path: undefined
2026-04-06 17:03:50 KST [warn]  BaseService::cleanUpAnythingRelatedModel | end - undefined modelDirPath
2026-04-06 17:03:50 KST [info]  AwsQueueManager::deleteMessage | begin - queue url: https://sqs.us-west-2.amazonaws.com/002596530511/cupix-tesla-voxel-agent-production
2026-04-06 17:03:50 KST [info]  AwsQueueManager::deleteMessage | end - message id: 882ee430-6833-4e65-855d-8439793d18b7

Tesla API 서버 측 로그:

text
service:cupixworks-api "1018363"
Time range: 2026-04-06T07:00:00Z to 2026-04-06T09:00:00Z
json
{"timestamp": "2026-04-06 17:03:50 KST", "status": "info", "message": "[200] GET /api/v1/pointclouds/1018363 (Api::V1::PointcloudsController#show)"}
{"timestamp": "2026-04-06 17:03:50 KST", "status": "info", "message": "[200] PUT /api/v1/pointclouds/1018363 (Api::V1::PointcloudsController#update)"}
{"timestamp": "2026-04-06 17:03:51 KST", "status": "info", "message": "[400] PUT /api/v1/pointclouds/1018363 (Api::V1::PointcloudsController#update)", "error": {"reason": "State not changed", "code": "STAT40000", "message": "State not changed", "class": "Cupix::Errors::InvalidState"}}

Tesla API에서 확인된 3건의 요청:

  1. GET (200) — loadRealityCapture로 pointcloud 데이터 조회 성공
  2. PUT (200) — updateVoxelState(Aggregating) 성공
  3. PUT (400) — updateVoxelState(Error) 실패 — STAT40000, 이미 error 상태

SQS 큐 관련 주의사항: 에러 발생 메시지는 cupix-tesla-voxel-agent-production 큐에서 수신되었으나, 동일 시간대에 cupix-tesla-voxel-agent-qa 큐에서도 다른 capture (40289, 40314, 40320)를 성공적으로 처리 중이었다. 에이전트가 두 환경의 큐를 동시에 처리하는 구조임을 확인.

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: voxel-service.ts:64-67 (catch 블록)
  • catch 블록에서 updateVoxelState(Error) 호출 시 실패를 무시하도록 에러 핸들링 추가 필요. 현재는 catch 블록 내부의 상태 업데이트 실패가 원래 에러를 덮어쓰거나 추가 에러 로그를 발생시킨다.
  • updateVoxelState(Error) 호출을 try-catch로 감싸서, 상태 전환 실패 시에도 원래 에러를 정상적으로 처리하고 SQS 메시지를 삭제하도록 해야 한다.

단기 개선 (1주 이내)#

  • 파일: voxel_module.rb:6-31
  • STAT40000 "State not changed" 에러를 동일 상태로의 전환 시 에러 대신 no-op으로 처리하는 것을 검토. 멱등성(idempotency)을 보장하면 중복 메시지나 재시도 시 불필요한 에러를 방지할 수 있다.
  • 파일: base-service.ts:145-151
  • 동일 pointcloud/capture ID에 대한 중복 SQS 메시지를 필터링하는 로직 추가를 검토.

장기 개선 (재발 방지)#

  • 하나의 agent 인스턴스가 여러 환경(production, QA)의 큐를 동시에 처리하는 구조를 재검토. 환경별 격리를 강화하면 예기치 않은 리소스 경합을 줄일 수 있다.
  • SQS 메시지의 deduplication 전략 검토 (FIFO 큐 또는 application-level deduplication).
  • voxel agent 처리 파이프라인에 상태 확인 단계 추가 — 상태 전환 전에 현재 상태를 확인하여 불필요한 전환을 사전에 방지.

Monitoring#

  • voxel_state 전환 실패 모니터링:
text
service:cupixworks-any-voxel-agent "STAT40000" OR "State not changed"
  • voxel agent 에러율 추이:
text
service:cupixworks-any-voxel-agent status:error
  • Tesla API에서 pointcloud voxel state 400 응답 비율:
text
service:cupixworks-api "pointclouds" "STAT40000"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 1회 발생이며 voxel 처리 자체의 핵심 기능(계산 및 업로드)에는 영향을 미치지 않는다. catch 블록의 상태 업데이트 실패일 뿐이며, BaseService::handlingMessageErrors에 의해 SQS 메시지는 정상 삭제되었다. 다만, catch 블록의 에러 핸들링이 불완전하여 원래 에러의 근본 원인이 마스킹될 수 있는 구조적 문제가 존재한다.