ES /docs

BaseService::handlingMessageErrors | Error and message object - {"error":{"statusCode":400,"requestU

RCA: BaseService::handlingMessageErrors — pointcloud voxel_state STAT40000 재시도 루프

Overview#

What Happened#

2026-07-03 05:47~05:48 KST 사이 cupixworks-any-voxel-agent(us-west-2, Siteline 팀 tenant cupix)가 pointcloud ID 1190788 SQS 메시지를 반복 수신했고, CPRealityCapture::validateInvalid potreeUrl을 던진 뒤 catch 블록이 updateVoxelState('error')로 tesla API에 PATCH를 시도했지만 이미 voxel_state: error인 레코드였기 때문에 tesla가 STAT40000 State not changed(HTTP 400)로 응답했다. 이 400 응답이 agent에서 BaseService::handlingMessageErrors 에러로 로깅되어 클러스터에 5회 집계되었다.

Quick Facts#

Field Value
exception.class Cupix::Errors::InvalidState (tesla side) → HTTP 400 propagated as agent error
exception.message State not changed (code STAT40000)
top_frame packages/base/src/base-service.ts:311 (handlingMessageErrors logger.error)
upstream failure packages/cupix-tesla-voxel-agent/src/model/cpreality-capture.ts:147throw new Error('Invalid potreeUrl')
env production, region us-west-2, tenant cupix, team siteline

Affected Teams#

Team / Domain Error Count Impact
siteline (tenant cupix) 5 (동일 pointcloud 1190788) 사용자 가시적 실패 없음. SQS 재시도 5회 → agent가 메시지를 400 응답으로 삭제 처리(getApiErrorToDeleteMessage) → 이후 재수신 없음. Voxel 파이프라인은 이미 error 상태로 마감됨.

Timeline#

  1. 2026-07-03 05:47:40 KST — Pointcloud 1190788 voxel_stateerror로 업데이트됨 (Elasticsearch voxel_state_updated_at: 2026-07-02T20:47:40Z). 이 시점의 실행은 클러스터 기간 이전이지만 최초 CPRealityCapture::validate | Invalid potreeUrl 로그가 05:47:40 KST에도 관측됨.
  2. 2026-07-03 05:47:51 KSTBaseService::runByMessage | id: 1190788 (재수신 #1). validate 실패 로그 재발생.
  3. 2026-07-03 05:47:52 KST — 첫 handlingMessageErrors 에러 로깅 (first_seen).
  4. 2026-07-03 05:47:52 ~ 05:48:08 KST — 동일 pointcloud에 대해 5개의 서로 다른 SQS MessageId가 연속 처리되며 각각 STAT40000 발생 (last_seen 05:48:08 KST).
  5. 2026-07-03 05:48:08 KST 이후 — 추가 로그 없음. getApiErrorToDeleteMessage가 400 응답을 반환하여 SQS 메시지 삭제 → 재시도 종료.

Error Log#

Datadog Logs

text
BaseService::handlingMessageErrors | Error and message object - {"error":{"statusCode":400,"requestUriHref":"http://api-tesla.cupix.internal/api/v1/pointclouds/1190788?fields%5B0%5D=id&fields%5B1%5D=name&fields%5B2%5D=state&fields%5B3%5D=resource_state&fields%5B4%5D=potree_state&fields%5B5%5D=octree_state&fields%5B6%5D=cpc_mesh_state&fields%5B7%5D=kind&fields%5B8%5D=potree_paths&fields%5B9%5D=pointcloud_type&fields%5B10%5D=mesh_type&fields%5B11%5D=capture&fields%5B12%5D=meta&fields%5B13%5D=record&fields%5B14%5D=cluster&fields%5B15%5D=created_at&fields%5B16%5D=updated_at&fields%5B17%5D=thumbnail_urls&fields%5B18%5D=potree_upload_urls&fields%5B19%5D=resource_upload_url&fields%5B20%5D=octree_upload_url&fields%5B21%5D=cpc_mesh_upload_url","bodyResult":{"code":"STAT40000","type":"Cupix::Errors::InvalidState","reason":"State not changed","message":"State not changed"},"modelId":1190788},"sqsMessage":{"MessageId":"84d93780-38d3-4584-8acd-3f150e498eae","Attributes":{"ApproximateReceiveCount":"1"}}}

Impact#

  • Service: cupixworks-any-voxel-agent
  • Team: siteline
  • 발생 횟수: 5
  • 최초 발생: 2026-07-03 05:47 KST
  • 최근 발생: 2026-07-03 05:48 KST

사용자에게 노출되는 실패는 없다. Pointcloud voxel 파이프라인은 이미 error로 마감된 상태이고, agent는 SQS 메시지를 자동 삭제하여 재시도 폭주로 이어지지 않았다. 다만 이 400은 error 레벨로 로깅되어 알람/집계 대시보드를 오염시킨다.

Root Cause Summary#

Pointcloud 1190788은 potree_url(및 potree_state != Uploaded)이 유효하지 않아 CPRealityCapture::validate가 예외를 던진다. 예외는 voxel-service.ts:63의 catch 블록에서 잡히고, catch 로직이 realityCaptureManager.updateVoxelState(VoxelState.Error)를 호출해 tesla PATCH /api/v1/pointclouds/:id{voxel_state: 'error'}를 보낸다. Tesla 측 Parameter::VoxelModule#update_voxel_state는 대상 상태가 현재 상태와 동일하면 Cupix::Errors::InvalidState(STAT40000, "State not changed")를 raise한다. Pointcloud 1190788의 voxel_state는 이미 error(첫 실행에서 세팅됨)이므로, 이후 동일 메시지가 다시 큐잉/재수신될 때마다 catch 경로가 필연적으로 400을 받아 handlingMessageErrors가 error 레벨로 로깅된다. 즉 upstream(validate 실패)은 정상적인 방어 로직이고, error 로그는 agent가 "이미 error인 상태로 다시 error 전이"를 시도해 발생하는 자기 유발 노이즈다.

Technical Analysis#

Code Path#

Entry point: packages/base/src/base-service.ts:108-110runByMessages() catch가 handlingMessageErrors로 위임.

packages/cupixworks/applications/agents/packages/base/src/base-service.ts:107-115typescript
try {
    await this.runByMessages();
} catch (error) {
    await this.handlingMessageErrors(error);
}
this.resetMessages();
await CPUtils.sleep(500);
await this.checkingQueue();

Voxel-service run 흐름 — validate 실패 후 catch가 updateVoxelState(Error)를 호출:

packages/cupixworks/applications/agents/packages/cupix-tesla-voxel-agent/src/voxel-service.ts:45-67typescript
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);
        // ... loadEntityParameters / loadSubModels / calculateVoxels / save / upload
    } catch (error: any) {
        logger.error('VoxelService::run | error', error);
        if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Error);
    }
};

Validate가 예외를 던지는 지점:

packages/cupixworks/applications/agents/packages/cupix-tesla-voxel-agent/src/model/cpreality-capture.ts:137-157typescript
validate = (): boolean => {
    logger.debug('CPRealityCapture::validate | begin - id: %d', this.id);
    if (this.id == Constants.UnknownId) {
        logger.error('CPRealityCapture::validate | Invalid ID: %d', this.id);
        throw new Error('Invalid ID');
    }

    if (this.isPointcloud) {
        if (!this.potreeUrl) {
            logger.error('CPRealityCapture::validate | Invalid potreeUrl: %s for Pointcloud ID: %d', this.potreeUrl, this.id);
            throw new Error('Invalid potreeUrl');
        }
        // ...
    }
    return true;
};

Failure point (tesla API) — voxel_state가 이미 target과 같으면 STAT40000:

tesla/app/concerns/parameter/voxel_module.rb:6-31ruby
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 'error'
      raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.voxel_state_error?

      @model.error_voxel_state!
    # ...
    end
  end
end

기대 동작: pointcloud가 이미 error 상태라면 굳이 다시 error로 전이할 필요가 없다. 재실행 시 agent는 이 상태를 감지해 no-op 처리하거나 warn 이하로 다운그레이드해야 한다.

실제 동작: catch 블록이 조건 없이 updateVoxelState(Error)를 호출하고, tesla가 반환한 400을 agent의 handlingMessageErrors가 error 레벨로 로깅한다.

Log Evidence#

Datadog 쿼리 (재현):

text
service:cupixworks-any-voxel-agent 1190788

각 SQS 메시지 처리마다 다음 3-tuple이 반복된다:

text
2026-07-02 20:47:51Z  info   BaseService::runByMessage | id: 1190788
2026-07-02 20:47:52Z  error  CPRealityCapture::validate | Invalid potreeUrl:  for Pointcloud ID: 1190788
2026-07-02 20:47:52Z  warn   CupixAuth::handleError | Response statusCode: 400, ...
                             body.result: {"code":"STAT40000","type":"Cupix::Errors::InvalidState","reason":"State not changed","message":"State not changed"}
2026-07-02 20:47:52Z  warn   BaseService::getApiErrorToDeleteMessage | error msg - {"statusCode":400, ...}
2026-07-02 20:47:52Z  error  BaseService::handlingMessageErrors | Error and message object - {...STAT40000...,"sqsMessage":{"MessageId":"84d93780-..."}}

동일 패턴이 5개 SQS MessageId(84d93780-..., d425e3e4-..., 0769829b-..., 1565d068-..., 40551d0e-...)에 대해 각각 발생. 모두 ApproximateReceiveCount: 1이므로 SQS 재드라이브가 아니라 상위(tesla worker)가 동일 pointcloud에 대해 새 메시지 5개를 큐잉했음을 의미한다.

Elasticsearch pointclouds 인덱스 (production):

json
{
  "id": 1190788,
  "state": "error",
  "voxel_state": "error",
  "voxel_state_updated_at": "2026-07-02T20:47:40.000Z",
  "kind": "sub",
  "parent": { "id": 1190787 }
}

voxel_state_updated_at(20:47:40Z)이 최초 handlingMessageErrors(20:47:52Z)보다 12초 앞선다. 즉 첫 번째 실행에서 validate 실패 → updateVoxelState('error') 성공 → 이후 4개의 재큐잉된 메시지가 모두 STAT40000을 받은 순서다.

potree_url 필드는 Elasticsearch 문서에 존재하지 않는다 (grep 결과 없음). Tesla 측 pointcloud가 potree 업로드 파이프라인을 완료하지 못한 상태에서 voxel-agent 큐잉이 이루어진 것으로 보이며, 이는 voxel-agent 자체가 아니라 upstream(pointcloud potree state 관리)의 문제다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 voxel_state가 이미 error인 pointcloud에 catch 블록이 다시 updateVoxelState(Error)를 호출해 tesla가 STAT40000 반환. Agent가 이를 error 레벨로 로깅. Elasticsearch voxel_state: error, updated_at 20:47:40Z. 매 실행마다 Invalid potreeUrlSTAT40000 순서. Tesla parameter/voxel_module.rb:19-20의 raise 조건 일치. Confirmed
H2 Pointcloud 1190788이 삭제되었거나 존재하지 않음 (404). Elasticsearch에서 pointcloud 문서 확인됨(state: error, voxel_state: error). GET pointcloud가 성공했기 때문에 validate 단계까지 도달함. Rejected
H3 SQS DLQ 재시도로 인한 폭주 (ApproximateReceiveCount > 1). 모든 5개 메시지가 ApproximateReceiveCount: 1. 서로 다른 MessageId. 재드라이브가 아니라 상위가 반복 큐잉. Rejected
H4 Tesla API 인증/네트워크 이슈. 400 응답이 정상 도착하고 body에 STAT40000 JSON이 정확히 포함됨. 401/5xx 아님. Rejected
H5 potree_url이 비어있어 voxel 처리 자체가 실패해야 하며 근본 원인은 pointcloud potree 업로드 파이프라인의 실패다. `CPRealityCapture::validate Invalid potreeUrl: (빈 문자열). Elasticsearch 문서에 potree_url` 필드 자체가 없음. 이 클러스터의 error 로그는 STAT40000이지 Invalid potreeUrl이 아님. Upstream 문제이지만 본 클러스터 error 시그니처의 직접 원인은 아니다.

Fix Recommendation#

즉시 조치 (Critical)#

없음. 이 클러스터 자체는 사용자 영향이 없고 self-healing (SQS 메시지는 400 응답으로 삭제됨). Alarm 노이즈 억제가 주요 이슈.

단기 개선 (1주 이내)#

  1. packages/cupix-tesla-voxel-agent/src/voxel-service.ts:63-66의 catch 블록에서 updateVoxelState(Error)를 호출하기 전에 현재 srvRealityCapture.voxel_state가 이미 Error인지 확인해 중복 전이를 건너뛴다. RealityCaptureManagerloadRealityCapture 결과에 이미 voxel_state 값을 갖고 있으므로 추가 API 호출 없이 비교 가능.
  2. 또는 RealityCaptureManager::updateVoxelState에서 this._srvRealityCapture?.voxel_state === state이면 debug 로그만 남기고 return하도록 조기 종료 가드를 추가한다. 이 방식이 잘못된 위치에서 여러 catch가 호출되어도 안전.
  3. BaseService::handlingMessageErrors (packages/base/src/base-service.ts:290-313)에서 bodyResult.code === 'STAT40000'이고 요청이 자기 자신의 error state 전이라면 error 대신 warn으로 다운그레이드. 실제 실패는 이미 이전 로그(Invalid potreeUrl)에 남아 있으므로 이 400은 정보 가치가 낮다. 메모리의 "true bug vs 예상 운영 시나리오" 가이드에 부합.

권장 접근은 (2) — 관리 클래스 단일 지점에서 방어하는 편이 향후 다른 catch 경로에서도 재발을 방지한다.

장기 개선 (재발 방지)#

  • Upstream 조사: potree_url이 없는 상태로 voxel-agent 큐잉이 발생하는 이유. Pointcloud potree 파이프라인의 상태 머신과 voxel 큐잉 트리거가 어긋나는지 확인 필요 (별도 클러스터/티켓으로 관리).
  • Agent catch 패턴을 base로 일반화: 실패 시 status 업데이트를 자동으로 idempotent하게 만드는 helper(safeUpdateState) 도입.
  • Tesla Parameter::VoxelModule#update_voxel_state의 동일 상태 전이를 400으로 응답하는 정책 재검토 — 200 no-op가 더 호출자 친화적일 수 있으나 계약 변경이므로 신중히.

Monitoring#

기존 클러스터 알람으로 충분히 감지된다. Fix 이후 재발 여부 추적용 timeseries widget 쿼리 예:

text
service:cupixworks-any-voxel-agent status:error "STAT40000" "voxel_state"
text
service:cupixworks-any-voxel-agent "handlingMessageErrors" "pointclouds"

Fix 배포 이후 위 쿼리 카운트가 0으로 유지되는지 1주간 모니터.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (5-10줄의 idempotent 가드 추가 + 로그 레벨 조정)