ES /docs

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_uploadingpotree_state: uploadedrevision 증가)이 아직 완료되지 않은 시점에 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#

  1. 06:32:07Z — Pointcloud 204434 resource 업로드 확인, potree 변환 agent(PotreeR1XlargeAgent) 트리거, main state: initializing → queued
  2. 06:35:37Z — Potree upload credentials 요청 (potree 변환 결과 업로드 시작)
  3. 06:35:43Z — Main state: queued → done, start_finalization 실행 → calculate_voxels → SQS 전송
  4. 06:35:44.135Z — Voxel agent: CupixAuth::setSession 완료
  5. 06:35:44.170Z — Voxel agent: API로 pointcloud 조회 → potree_url 비어있음 → validation 실패
  6. 06:35:45Z — Client가 check_uploading 호출 → potree_state: uploaded, revision 증가, potree_url 설정 완료

Error Log#

Datadog Logs

text
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_stateError로 설정되어 자동 재시도 없이 수동 재실행이 필요한 상태.

Root Cause Summary#

Pointcloud의 potree 변환이 완료되어 main state가 done으로 전환되면서 calculate_voxels가 트리거되었으나, potree 파일의 실제 업로드 확인(check_uploading)이 아직 완료되지 않은 시점에 voxel agent가 API를 통해 pointcloud를 조회했다. Rails의 Pointcloud#potree_url 메서드는 revision이 0이면 nil을 반환하고, revisionpotree_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:69loadRealityCapture()로 Pointcloud 204434 조회
  • 인스턴스 생성: reality_capture.manager.ts:76createCPRealityCapture()validate() 호출
  • Failure point: cpreality-capture.ts:145-148potreeUrl이 빈 문자열이면 에러 throw
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);
		// ... 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);
	}
};
packages/cupix-tesla-voxel-agent/src/manager/reality_capture.manager.ts:69-79typescript
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:

packages/cupix-tesla-voxel-agent/src/model/cpreality-capture.ts:47typescript
get potreeUrl(): string { return (this._srvModel as TESLA.Pointcloud)?.potree_url ?? ''; }

Validation에서 falsy check로 빈 문자열을 잡아 throw:

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');
		}

		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_urlnil을 반환하는 조건:

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?
    Cupix::StorageService.object_url(storage_option: storage_option, key: potree_basepath(revision))
end

revisionpotree_state → uploaded 전환의 before_transition에서 증가:

app/models/concerns/statable/pointcloud.rb:169-171ruby
before_transition any => :uploaded do |pointcloud, transition|
  pointcloud.increase_revision
end

Rails 측 enqueue — potree_url 존재 여부를 검증하지 않음:

app/services/cupix/voxel_service.rb:3-11ruby
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로 전환되기 전일 수 있음:

app/models/concerns/voxel_module.rb:37ruby
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 쿼리:

text
service:cupixworks-any-voxel-agent status:error @environment:production "CPRealityCapture::validate"
text
service:cupixworks-any-voxel-agent @environment:production 204434

Voxel agent 실행 흐름 전체:

text
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-...

에러 로그 상세:

json
{
  "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분 전):

json
{
  "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_urlrevision 의존(potree.rb:14), revision은 potree_state uploaded 전환 시 증가(statable/pointcloud.rb:169). Confirmed
H2 API 응답에서 potree_url 필드 누락 (API 직렬화 버그) potreeUrl이 빈 문자열로 반환됨 PointCloudFieldspotree_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 에러 빈도:
text
service:cupixworks-any-voxel-agent status:error "Invalid potreeUrl"
  • Pointcloud voxel 처리 성공률 추적:
text
service:cupixworks-any-voxel-agent "VoxelService::run" ("successfully completed" OR "error")

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 단일 발생 이벤트이며 potree 업로드 완료와 voxel 트리거 사이의 타이밍 edge case. 재시도 또는 수동 voxel 재계산으로 복구 가능하나, 동일 팀의 다른 pointcloud(204400)도 유사 문제를 겪은 점에서 반복 가능성 있음.