CPRealityCapture::validate | Invalid potreeUrl: for Pointcloud ID: 204434
RCA: CPRealityCapture::validate | Invalid potreeUrl for Pointcloud ID: 204434
Overview#
What Happened#
2026-05-12 06:35:44 UTC에 cupixworks-any-voxel-agent 서비스(ap-southeast-2 리전)에서 Pointcloud ID 204434에 대한 voxel 계산 작업이 시작되었으나, pointcloud의 potree_url이 비어 있어 validation 단계에서 즉시 실패했다. Potree 변환 완료 후 main state가 done으로 전환되면서 voxel 계산이 트리거되었지만, potree 파일의 실제 업로드 확인(check_uploading → potree_state: uploaded → revision 증가)이 아직 완료되지 않은 시점에 voxel agent가 API를 통해 데이터를 조회한 race condition이다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error |
| exception.message | Invalid potreeUrl |
| top_frame | cpreality-capture.ts:146 |
| env | production, ap-southeast-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| endeavourgroup (team ID 180) | 1 | Pointcloud 204434의 voxel 계산 실패, voxel_state → Error 설정 |
Timeline#
- 06:32:07Z — Pointcloud 204434 resource 업로드 확인, potree 변환 agent(PotreeR1XlargeAgent) 트리거, main state:
initializing → queued - 06:35:37Z — Potree upload credentials 요청 (potree 변환 결과 업로드 시작)
- 06:35:43Z — Main state:
queued → done,start_finalization실행 →calculate_voxels→ SQS 전송 - 06:35:44.135Z — Voxel agent:
CupixAuth::setSession완료 - 06:35:44.170Z — Voxel agent: API로 pointcloud 조회 →
potree_url비어있음 → validation 실패 - 06:35:45Z — Client가
check_uploading호출 →potree_state: uploaded,revision증가,potree_url설정 완료
Error Log#
CPRealityCapture::validate | Invalid potreeUrl: for Pointcloud ID: 204434
Impact#
- Service:
cupixworks-any-voxel-agent - 발생 횟수: 1
- 최초 발생: 2026-05-12T06:35:44.170Z
- 최근 발생: 2026-05-12T06:35:44.170Z
영향이 제한적인 단일 발생 이벤트. Pointcloud 204434의 voxel 계산이 실패했으며, voxel_state가 Error로 설정되어 자동 재시도 없이 수동 재실행이 필요한 상태.
Root Cause Summary#
Pointcloud의 potree 변환이 완료되어 main state가 done으로 전환되면서 calculate_voxels가 트리거되었으나, potree 파일의 실제 업로드 확인(check_uploading)이 아직 완료되지 않은 시점에 voxel agent가 API를 통해 pointcloud를 조회했다. Rails의 Pointcloud#potree_url 메서드는 revision이 0이면 nil을 반환하고, revision은 potree_state → uploaded 전환의 before_transition에서 증가한다. main state done 전환(06:35:43Z)과 potree check_uploading(06:35:45Z) 사이 약 2초의 gap 동안 voxel agent(06:35:44Z)가 조회하여 potree_url = nil(→ 빈 문자열)을 받았다.
Technical Analysis#
Code Path#
- Entry point:
voxel-service.ts:45— SQS 메시지 수신 후run()실행 - API 조회:
reality_capture.manager.ts:69—loadRealityCapture()로 Pointcloud 204434 조회 - 인스턴스 생성:
reality_capture.manager.ts:76—createCPRealityCapture()→validate()호출 - Failure point:
cpreality-capture.ts:145-148—potreeUrl이 빈 문자열이면 에러 throw
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);
// ... voxel processing (updateVoxelState, loadEntityParameters, calculateVoxels, upload)
} catch (error: any) {
logger.error('VoxelService::run | error', error);
if (!DEBUG_MODE) await this.realityCaptureManager.updateVoxelState(TESLA.VoxelState.Error);
}
};
createCPRealityCapture = (srvRealityCapture: TESLA.Capture | TESLA.Pointcloud, targetType?: string): CPRealityCapture => {
logger.debug('RealityCaptureManager::createCPRealityCapture | begin');
if (!srvRealityCapture || !srvRealityCapture.id) {
logger.error('RealityCaptureManager::createCPRealityCapture | end - invalid reality capture');
throw new Error('Invalid srvRealityCapture');
}
const cpRealityCapture = new CPRealityCapture(srvRealityCapture, targetType);
cpRealityCapture.validate(); // ← validate 호출
logger.debug('RealityCaptureManager::createCPRealityCapture | end');
return cpRealityCapture;
};
potreeUrl getter는 API 응답의 potree_url 필드를 반환하며, null/undefined일 경우 빈 문자열로 fallback:
get potreeUrl(): string { return (this._srvModel as TESLA.Pointcloud)?.potree_url ?? ''; }
Validation에서 falsy check로 빈 문자열을 잡아 throw:
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');
}
if ((this._srvModel as TESLA.Pointcloud)?.potree_state !== TESLA.PointcloudPotreeState.Uploaded) {
logger.error('CPRealityCapture::validate | Invalid potree_state: %s for Pointcloud ID: %d', (this._srvModel as TESLA.Pointcloud)?.potree_state, this.id);
throw new Error('Invalid potree_state');
}
}
return true;
};
Rails 측 — potree_url이 nil을 반환하는 조건:
def potree_url
return nil if revision.zero?
return self.sys[:origin_potree_url] if self.sys[:origin_potree_url].present?
Cupix::StorageService.object_url(storage_option: storage_option, key: potree_basepath(revision))
end
revision은 potree_state → uploaded 전환의 before_transition에서 증가:
before_transition any => :uploaded do |pointcloud, transition|
pointcloud.increase_revision
end
Rails 측 enqueue — potree_url 존재 여부를 검증하지 않음:
def calculate_voxels!(model: nil, session: nil, **kwargs)
return unless Cupix::Tesla.launch_mode == 'CUPIXWORKS'
raise Cupix::Errors::Argument.new(code: 'ARG10000', reason: 'model is required') if model.blank?
raise Cupix::Errors::Argument.new(code: 'ARG10000', reason: 'model must be a capture or pointcloud') unless model.is_a?(::Capture) || model.is_a?(::Pointcloud)
raise Cupix::Errors::Argument.new(code: 'ARG40000', reason: 'error state model cannot calculate voxel') if model.error_code.present?
model.queued_voxel_state! if model.has_attribute?(:voxel_state)
invoke(model: model, session: session, action: 'calculate_voxels', **kwargs)
end
check_calculate_voxels!에서 potree_state_uploaded?를 확인하지만, 이 시점에서는 main state done 콜백 내에서 호출되므로 potree_state가 아직 uploaded로 전환되기 전일 수 있음:
raise Cupix::Errors::Parameter.new(code: 'ARG10060', reason: 'Potree is not uploaded') if self.respond_to?(:potree_state_uploaded?) && !self.potree_state_uploaded?
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-voxel-agent status:error @environment:production "CPRealityCapture::validate"
service:cupixworks-any-voxel-agent @environment:production 204434
Voxel agent 실행 흐름 전체:
06:35:33.413 [info] AwsQueueManager::deleteMessage begin (이전 작업 완료)
06:35:33.485 [info] AwsQueueManager::deleteMessage end - message id: 10c19736-...
06:35:44.135 [info] CupixAuth::setSession | session_id: 2d4eabcab12971f2d0221768e5811038b3174a28
06:35:44.170 [error] CPRealityCapture::validate | Invalid potreeUrl: for Pointcloud ID: 204434
06:35:44.170 [error] VoxelService::run | end - error: "Invalid potreeUrl"
06:35:44.493 [info] BaseService::cleanUpAnythingRelatedModel | path: undefined
06:35:44.493 [warn] BaseService::cleanUpAnythingRelatedModel | end - undefined modelDirPath
06:35:44.494 [info] AwsQueueManager::deleteMessage begin
06:35:44.528 [info] AwsQueueManager::deleteMessage end - message id: 000dd89a-...
에러 로그 상세:
{
"timestamp": "2026-05-12T06:35:44.170Z",
"status": "error",
"message": "CPRealityCapture::validate | Invalid potreeUrl: for Pointcloud ID: 204434",
"meta": {
"session.id": "2d4eabcab12971f2d0221768e5811038b3174a28",
"team.domain": "endeavourgroup",
"team.id": 180,
"pointcloud.id": 204434,
"user.id": 8624,
"user.email": "chris.bellamy@auav.com.au"
},
"host": "ip-10-1-46-225.ap-southeast-2.compute.internal",
"region": "ap-southeast-2",
"environment": "production"
}
동일 사용자/팀의 관련 에러 (Pointcloud 204400, 약 23분 전):
{
"timestamp": "2026-05-12T06:12:10.460Z",
"status": "error",
"message": "VoxelService::run | end - error: \"ENOENT: no such file or directory, stat '/tmp/workspace/pointcloud/204400/raw/pointcloud_204400_voxels.csv'\"",
"meta": {
"pointcloud.id": 204400,
"user.email": "chris.bellamy@auav.com.au",
"team.domain": "endeavourgroup"
}
}
주목할 점:
loadEntityParameters또는setCaptureVoxelsParams로그 없음 — validation이 먼저 실패하여 후속 처리에 도달 못함- SQS 메시지가 에러 후에도 삭제됨(06:35:44.494) — 재시도 불가
- 같은 시간대 capture 타입(ID 71473, 71525 등)은 정상 처리 — pointcloud 타입만 영향
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Potree 파일 업로드 완료(check_uploading) 전에 voxel agent가 트리거되는 race condition | 타임라인: state done(06:35:43) → voxel error(06:35:44) → check_uploading(06:35:45). potree_url은 revision 의존(potree.rb:14), revision은 potree_state uploaded 전환 시 증가(statable/pointcloud.rb:169). |
— | Confirmed |
| H2 | API 응답에서 potree_url 필드 누락 (API 직렬화 버그) |
potreeUrl이 빈 문자열로 반환됨 | PointCloudFields에 potree_url 명시적 포함(cupix-fields.ts:51). getter가 ?? ''로 null → 빈 문자열 변환. Rails potree_url 메서드가 revision.zero? 시 nil 반환하는 정상 동작. |
Rejected |
| H3 | DB replication lag로 committed revision이 읽기 replica에 미반영 | 가능성 있음 — SQS 전송 후 1초 이내 조회 | check_uploading 자체가 done 전환 이후에 발생하므로 revision이 아직 증가하지 않은 것이 원인. lag가 아닌 순서 문제. | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
파일: app/services/cupix/voxel_service.rb:3-11 또는 app/models/concerns/voxel_module.rb:37
calculate_voxels! 호출 시 pointcloud인 경우 potree_url.present?를 추가 검증. potree_state_uploaded? 체크만으로는 타이밍에 따라 불충분하므로, potree_url이 실제로 non-nil인지도 확인하여 빈 url 상태에서 enqueue되는 것을 차단.
단기 개선 (1주 이내)#
파일: packages/cupix-tesla-voxel-agent/src/voxel-service.ts:63-65
Voxel agent 측에서 potreeUrl validation 실패 시 SQS 메시지를 삭제하지 않고 visibility timeout으로 재시도할 수 있도록 에러 처리 방식 변경. 현재는 catch 블록에서 voxel_state → Error로 설정하고 메시지를 삭제하므로 자동 복구가 불가능.
장기 개선 (재발 방지)#
Voxel 계산 트리거를 main state done 전환이 아닌, potree_state → uploaded 전환의 after_transition 콜백에서 직접 호출하도록 아키텍처 변경. 현재는 main state done → start_finalization → editing 완료 → calculate_voxels 순서로 간접 트리거되어, potree 업로드 완료와의 타이밍 보장이 없다.
Monitoring#
- Voxel agent의
Invalid potreeUrl에러 빈도:
service:cupixworks-any-voxel-agent status:error "Invalid potreeUrl"
- Pointcloud voxel 처리 성공률 추적:
service:cupixworks-any-voxel-agent "VoxelService::run" ("successfully completed" OR "error")
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 단일 발생 이벤트이며 potree 업로드 완료와 voxel 트리거 사이의 타이밍 edge case. 재시도 또는 수동 voxel 재계산으로 복구 가능하나, 동일 팀의 다른 pointcloud(204400)도 유사 문제를 겪은 점에서 반복 가능성 있음.