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_state는 uploaded이지만 Tesla API가 potree_url을 null로 반환한 것이 원인이다.
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#
- 2026-06-05 11:42 KST — Voxel agent가 SQS 메시지에서 Pointcloud 217038 작업을 수신
- 2026-06-05 11:42 KST — Tesla API에서 pointcloud 조회 후
potreeUrl이 빈 문자열임을 감지, validation 실패 - 2026-06-05 11:42 KST — SQS 메시지 삭제 (재시도 없이 유실)
- 2026-06-05 11:42 KST — Error Sweeper가 에러 수집
Error Log#
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_state가 uploaded임에도 불구하고, 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:45—VoxelService.run()이 SQS 메시지에서 targetId를 받아 처리 시작 - Tesla API 호출:
reality_capture.manager.ts:49-50—loadRealityCapture()로 Pointcloud 조회 후createCPRealityCapture()호출 - Validation:
cpreality-capture.ts:144-148—potreeUrl이 falsy일 때 에러 throw - potreeUrl 계산: Tesla API 측
potree.rb:14-15—revision.zero?이면nil반환
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이 반환되면 ?? ''에 의해 빈 문자열이 된다.
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_state는 uploaded이었다.
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 이상이 된다.
before_transition any => :uploaded do |pointcloud, transition|
pointcloud.increase_revision
end
def increase_revision
if self.revision.blank?
self.revision = 1
else
self.revision += 1
end
end
기대 동작: potree_state가 uploaded이면 revision >= 1이어야 하며, potree_url은 유효한 S3 URL이어야 한다.
실제 동작: potree_state는 uploaded이지만 revision이 0이어서 potree_url이 nil로 반환됨.
Log Evidence#
Datadog 쿼리:
service:cupixworks-any-voxel-agent status:error @environment:production "Invalid potreeUrl"
전체 실행 타임라인 (Pointcloud 217038):
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:
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_state는uploaded- SQS 메시지가 에러 후에도 삭제됨 — 작업 유실, 자동 재시도 없음
Attributes:
{
"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_state가 uploaded로 설정되었으나 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_state가uploaded인데 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_state가uploaded이면 자동으로 revision을 1로 보정하거나 명시적 에러를 반환하도록 변경
Monitoring#
potree_state: uploaded이면서potree_url: null인 API 응답 감지:
service:cupixworks-any-voxel-agent status:error "Invalid potreeUrl"
- Tesla API 측에서 데이터 불일치 감지:
service:cupixworks-api "potree_url" "revision" "zero"
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard