ES /docs

Invalid potreeUrl

RCA: Invalid potreeUrl

Overview#

What Happened#

2026-06-05 11:42 KST, cupixworks-any-voxel-agent 서비스에서 Pointcloud ID 217038에 대한 voxel 계산 중 potreeUrl이 빈 문자열로 확인되어 validation에 실패했다. potree_stateuploaded이지만 Tesla API가 potree_urlnull로 반환한 것이 원인이다.

Quick Facts#

Field Value
exception.class Error
exception.message Invalid potreeUrl
top_frame CPRealityCapture.validate (app.cjs:7384)
runtime Node.js
env production, ap-southeast-2

Affected Teams#

Team / Domain Error Count Impact
forida-demo 1 Pointcloud 217038의 voxel 계산 실패, floor plan footprint 미생성

Timeline#

  1. 2026-06-05 11:42 KST — Voxel agent가 SQS 메시지에서 Pointcloud 217038 작업을 수신
  2. 2026-06-05 11:42 KST — Tesla API에서 pointcloud 조회 후 potreeUrl이 빈 문자열임을 감지, validation 실패
  3. 2026-06-05 11:42 KST — SQS 메시지 삭제 (재시도 없이 유실)
  4. 2026-06-05 11:42 KST — Error Sweeper가 에러 수집

Error Log#

Datadog Logs

text
Invalid potreeUrl

Impact#

  • Service: cupixworks-any-voxel-agent
  • Team: forida-demo
  • 발생 횟수: 1
  • 최초 발생: 2026-06-05 11:42 KST
  • 최근 발생: 2026-06-05 11:42 KST

Root Cause Summary#

Tesla API(Rails)의 Pointcloud#potree_url 메서드는 revision이 0일 때 nil을 반환한다. Pointcloud 217038의 potree_stateuploaded임에도 불구하고, sys[:revision]이 0 또는 nil 상태로 남아있어 API 응답의 potree_url 필드가 null이 되었다. Voxel agent는 이 값을 빈 문자열(?? '')로 fallback 처리한 후 validate() 에서 falsy 체크에 걸려 에러를 throw했다. 이는 potree_state 전환 시 before_transition에서 increase_revision이 실행되어야 하지만, 직접적인 DB 조작이나 migration에 의해 state만 변경되고 revision은 증가하지 않은 데이터 불일치 상태로 추정된다.

Technical Analysis#

Code Path#

  • Entry point: voxel-service.ts:45VoxelService.run()이 SQS 메시지에서 targetId를 받아 처리 시작
  • Tesla API 호출: reality_capture.manager.ts:49-50loadRealityCapture()로 Pointcloud 조회 후 createCPRealityCapture() 호출
  • Validation: cpreality-capture.ts:144-148potreeUrl이 falsy일 때 에러 throw
  • potreeUrl 계산: Tesla API 측 potree.rb:14-15revision.zero?이면 nil 반환
cupix-tesla-voxel-agent/src/model/cpreality-capture.ts:44-48typescript
get isPointcloud(): boolean { return this._modelType === 'pointcloud'; }
get levelId(): number { return this._srvModel?.level?.id ?? Constants.UnknownId; }
get potreeUrl(): string { return (this._srvModel as TESLA.Pointcloud)?.potree_url ?? ''; }
get entryFileName(): string { return (this._srvModel as TESLA.Pointcloud)?.entry_filename ?? ''; }
get voxelState(): string | undefined { return this._srvModel?.voxel_state; }

API에서 potree_url: null이 반환되면 ?? ''에 의해 빈 문자열이 된다.

cupix-tesla-voxel-agent/src/model/cpreality-capture.ts:144-148typescript
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');
	}

빈 문자열은 falsy이므로 validation이 실패한다. 주목할 점은 line 150의 potree_state !== Uploaded 체크를 통과했으므로, API 응답에서 potree_stateuploaded이었다.

tesla/app/models/concerns/potree.rb:14-18ruby
def potree_url
  return nil if revision.zero?
  return self.sys[:origin_potree_url] if self.sys[:origin_potree_url].present? # debug

  Cupix::StorageService.object_url(storage_option: storage_option, key: potree_basepath(revision))
end

revision.zero?가 true이면 URL을 생성하지 않고 nil을 반환한다. 정상 흐름에서는 potree_state:uploaded 전환 시 before_transition에서 increase_revision이 호출되어 revision이 1 이상이 된다.

tesla/app/models/concerns/statable/pointcloud.rb:169-171ruby
before_transition any => :uploaded do |pointcloud, transition|
  pointcloud.increase_revision
end
tesla/app/models/concerns/revisionable.rb:39-44ruby
def increase_revision
  if self.revision.blank?
    self.revision = 1
  else
    self.revision += 1
  end
end

기대 동작: potree_stateuploaded이면 revision >= 1이어야 하며, potree_url은 유효한 S3 URL이어야 한다. 실제 동작: potree_stateuploaded이지만 revision이 0이어서 potree_urlnil로 반환됨.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-any-voxel-agent status:error @environment:production "Invalid potreeUrl"

전체 실행 타임라인 (Pointcloud 217038):

text
02:42:37.310Z [info]  BaseService::runByMessage | id: 217038
02:42:37.347Z [info]  CupixAuth::setSession | session_id: 84da27baca38ec9154471e6bc36ea2329eb8bba4
02:42:37.394Z [error] CPRealityCapture::validate | Invalid potreeUrl:  for Pointcloud ID: 217038
02:42:37.395Z [error] Invalid potreeUrl
02:42:38.237Z [info]  BaseService::cleanUpAnythingRelatedModel | path: undefined
02:42:38.237Z [warn]  BaseService::cleanUpAnythingRelatedModel | end - undefined modelDirPath
02:42:38.238Z [info]  AwsQueueManager::deleteMessage | begin
02:42:38.269Z [info]  AwsQueueManager::deleteMessage | end - message id: 501efc03-52a9-4b8a-8da2-e7d810f10fac

에러의 stack trace:

text
Error: Invalid potreeUrl
    at CPRealityCapture.validate (/tmp/agent/dist/app.cjs:7384:17)
    at RealityCaptureManager.createCPRealityCapture (/tmp/agent/dist/app.cjs:7743:24)
    at VoxelService.run (/tmp/agent/dist/app.cjs:7908:61)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /tmp/agent/dist/app.cjs:6128:17
    at async BaseService2.runByMessage (/tmp/agent/dist/app.cjs:6124:15)
    at async BaseService2.runByMessages (/tmp/agent/dist/app.cjs:6112:13)
    at async BaseService2.checkingQueue (/tmp/agent/dist/app.cjs:6076:15)

핵심 증거:

  • 로그 메시지 Invalid potreeUrl: — colon 뒤에 빈 문자열이 확인됨 (API에서 null 반환)
  • potree_state 체크(line 150-153)를 통과함 — potree_stateuploaded
  • SQS 메시지가 에러 후에도 삭제됨 — 작업 유실, 자동 재시도 없음

Attributes:

json
{
  "service": "cupixworks-any-voxel-agent",
  "region": "ap-southeast-2",
  "host": "ip-10-1-107-227.ap-southeast-2.compute.internal",
  "session.id": "84da27baca38ec9154471e6bc36ea2329eb8bba4",
  "team.domain": "forida-demo",
  "team.id": 104,
  "pointcloud.id": 217038,
  "user.id": 7085,
  "user.email": "venus.kwok@forida.com.hk"
}

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Pointcloud 217038의 revision이 0으로 남아있어 potree_url 메서드가 nil 반환 로그에서 potreeUrl: (빈값) 확인; potree.rb:15에서 revision.zero?이면 nil 반환; potree_state 체크는 통과 Confirmed
H2 API fields 목록에 potree_url이 누락되어 응답에 포함되지 않음 PointcloudFields(line 37)에는 누락됨 pointcloud.get()PointCloudFields(line 46)를 사용하며 여기에는 potree_url 포함 (line 51) Rejected
H3 네트워크 문제로 Tesla API 호출이 불완전한 응답을 반환 에러 전 session 인증 성공, API로부터 pointcloud ID 확인됨 (createCPRealityCapture에서 id 검증 통과) Rejected
H4 state machine을 우회한 직접 DB 조작으로 potree_stateuploaded로 설정되었으나 increase_revision이 호출되지 않음 refresh_voxels!(voxel_module.rb:82)는 check_calculate_voxels! 없이 직접 호출 가능; migration 스크립트 존재 (lib/cupix/migrate/pointcloud.rb) 정상 state machine 흐름에서는 before_transition이 반드시 실행됨 Confirmed

Fix Recommendation#

즉시 조치 (Critical)#

  • Pointcloud 217038의 데이터 복구: Tesla DB에서 해당 pointcloud의 sys[:revision] 값을 확인하고, potree_stateuploaded인데 revision이 0이면 1로 수정
  • Voxel agent의 에러 메시지 개선: cpreality-capture.ts:146에서 potreeUrl 뿐만 아니라 API 응답의 revision 관련 정보도 로깅하여 향후 디버깅을 용이하게 함

단기 개선 (1주 이내)#

  • Tesla API serializer에 guard 추가: potree.rb:14에서 revision.zero?이면서 potree_state == :uploaded인 경우를 감지하고 로그/알림을 발생시키는 방어 로직 추가
  • Voxel agent의 validation 메시지 개선: validate() 메서드에서 potreeUrl이 비어있을 때, pointcloud의 potree_state 정보와 함께 로깅하여 데이터 불일치 상태를 즉시 식별할 수 있도록 함
  • 에러 시 SQS 메시지 유지 여부 검토: 현재 validation 에러 후에도 메시지가 삭제되어 재시도가 불가능함. Transient한 데이터 불일치는 재시도로 해소될 수 있으므로, validation 에러 유형에 따라 DLQ(Dead Letter Queue)로 이동하는 방안 검토

장기 개선 (재발 방지)#

  • 데이터 정합성 검증 job 추가: potree_state: uploaded인 pointcloud 중 revision == 0인 레코드를 주기적으로 탐지하는 scheduled job 도입
  • Tesla API의 potree_url 계산 로직 방어 강화: revision.zero?에서 nil을 반환하는 대신, potree_stateuploaded이면 자동으로 revision을 1로 보정하거나 명시적 에러를 반환하도록 변경

Monitoring#

  • potree_state: uploaded이면서 potree_url: null인 API 응답 감지:
text
service:cupixworks-any-voxel-agent status:error "Invalid potreeUrl"
  • Tesla API 측에서 데이터 불일치 감지:
text
service:cupixworks-api "potree_url" "revision" "zero"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard