ES /docs

Parent model is invalid: model(pointcloud)

RCA: Parent model is invalid: model(pointcloud)

Overview#

What Happened#

2026-05-04 05:24:41 UTC에 cupixworks-apiMigrationExportOperation에서 migration id 1339의 데이터베이스 내보내기 중 "Parent model is invalid: model(pointcloud)" 에러가 5회 발생했다. 이 에러는 MODEL_TREE 상수에서 pointcloud 모델의 parent가 cluster로 정의되어 있지만, read_records에서 내보내는 uploaded 타입 pointcloud는 cluster_id가 없어 parent 참조를 해소하지 못해 발생한다. 이 에러 자체는 non-fatal(로그만 기록하고 처리 계속)이나, 동일 export job은 Sidekiq::Shutdown으로 5회 연속 실패하며 retries가 소진되었다.

Quick Facts#

Field Value
exception.class MigrationExportOperation (custom error log)
exception.message Parent model is invalid: model(pointcloud)
top_frame app/operations/migration_export_operation.rb:519
runtime Ruby 3.3.0, Rails 7.2.2, Sidekiq 7.3.9
deploy production-us-west-2-20260504T0520Z0-57c6026d-cupixworks
env production, us-west-2

Timeline#

  1. 05:24:41 UTC — ExportWorker JID-5c20bfb40b930b271824c26b 시작, read_record 단계에서 uploaded pointcloud 처리 중 "Parent model is invalid: model(pointcloud)" 에러 5건 로그
  2. 05:25:27 UTC — ExportWorker 첫 번째 실패 (Sidekiq::Shutdown at read_panoscreate_pointcloud_data:632)
  3. 05:30:26 ~ 05:55:28 UTC — Sidekiq retry로 4회 추가 실행, 모두 동일 Sidekiq::Shutdown으로 실패
  4. 2026-05-04 — error-sweeper가 "Parent model is invalid" 에러 클러스터를 감지

Error Log#

Datadog Logs

text
Parent model is invalid: model(pointcloud)

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 5
  • 최초 발생: 2026-05-04T05:24:41.666Z
  • 최근 발생: 2026-05-04T05:24:41.666Z
  • Migration id 1339의 데이터 내보내기가 5회 retry 모두 실패하여 해당 마이그레이션이 완료되지 못함. 단일 마이그레이션 작업에 국한된 영향.

Root Cause Summary#

MODEL_TREE 상수에서 pointcloud 모델의 parent가 cluster(foreign_key: cluster_id)로 단일 정의되어 있지만, 실제로 pointcloud에는 두 가지 타입이 존재한다: (1) 3d_reconstructedcluster_id를 가지며 cluster의 자식, (2) uploadedrecord_id만 가지며 cluster_id가 없다. read_records 메서드(line 319)에서 uploaded pointcloud를 조회하여 create_model_data('pointcloud', pointclouds)를 호출하면, MODEL_TREE는 cluster_id를 foreign_key로 참조하므로 parent_idnil이 되어 검증 조건 parent_id.nil? && parent.present?가 true가 되고 에러가 로그된다. 이 에러 자체는 non-fatal이지만, 동일 export job은 read_panos 단계에서 Sidekiq::Shutdown(대량 데이터 처리로 인한 timeout)에 의해 반복 실패했다.

Technical Analysis#

Code Path#

  • Entry point: app/workers/export_worker.rb:19ExportWorker#perform
  • Step 1: read_record 단계에서 read_records 호출 (line 114)
  • Step 2: read_records에서 uploaded pointcloud 조회 (line 319)
app/operations/migration_export_operation.rb:319-335ruby
pointclouds = Pointcloud.where(record_id: record_ids).where(pointcloud_type: 'uploaded').where.not(cycle_state: %w[trashed purged]).order(:id).select('*').map(&:attributes)
pointcloud_ids = pointclouds.map { |pointcloud| pointcloud['id'] }
pointcloud_resources = fetch_resources('pointcloud', pointcloud_ids)
# ...
pointcloud_data = create_model_data('pointcloud', pointclouds)
  • Step 3: create_model_data에서 MODEL_TREE 조회 (line 477-478)
app/operations/migration_export_operation.rb:477-478ruby
parent = MODEL_TREE[model_name.to_sym]&.dig(:parent)        # => 'cluster'
foreign_key = MODEL_TREE[model_name.to_sym]&.dig(:foreign_key) # => 'cluster_id'
  • Step 4: uploaded pointcloud의 cluster_id가 nil이므로 parent_id가 nil (line 485)
app/operations/migration_export_operation.rb:485ruby
parent_id = foreign_key ? row[foreign_key.to_s] : nil  # row['cluster_id'] => nil (uploaded pointcloud)
  • Failure point: 검증 조건에서 에러 로그 발생 (line 517-519)
app/operations/migration_export_operation.rb:517-520ruby
# wrong parent model definition in model tree
if parent_id.nil? && parent.present?
  Cupix::Logger.error("Parent model is invalid: model(#{model_name})", class: self.class.name, method: __method__)
end
  • MODEL_TREE 정의 — pointcloud는 단일 항목으로 cluster의 자식으로만 정의됨 (line 127):
app/operations/migration_export_operation.rb:126-128ruby
cluster: { order: 1251, parent: 'capture', foreign_key: 'capture_id' },
pointcloud: { order: 1252, parent: 'cluster', foreign_key: 'cluster_id' },
pointcloud_permission: { order: 1253, parent: 'pointcloud', foreign_key: 'pointcloud_id' },
  • 코드 주석(line 26, 50)에서는 두 가지 경로를 명시하지만 MODEL_TREE에는 하나만 반영됨:
app/operations/migration_export_operation.rb:26,50text
#       |-- pointcloud(customer upload)          ← level/sketch의 자식, cluster_id 없음
#                       |-- pointcloud (3d reconstruction)  ← cluster의 자식, cluster_id 있음

기대 동작: uploaded pointcloud는 record를 parent로 하고 record_id를 foreign_key로 사용해야 한다. 실제 동작: MODEL_TREE에 단일 pointcloud 정의만 있어 두 타입 모두 cluster_id로 parent를 참조하며, uploaded pointcloud는 cluster_id가 없으므로 항상 invalid로 판정된다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api status:error "Parent model is invalid"
Time range: 2026-05-04T04:24:00Z to 2026-05-04T06:00:00Z

5건 모두 동일 속성:

json
{
  "timestamp": "2026-05-04T05:24:41.666Z",
  "status": "error",
  "message": "Parent model is invalid: model(pointcloud)",
  "class": "MigrationExportOperation",
  "method": "create_model_data",
  "service_role": "migrationworker",
  "request_id": "5c20bfb40b930b271824c26b",
  "tenant": "cupix",
  "pid": 298020
}

ExportWorker 실패 로그 (동일 JID):

text
service:cupixworks-api 5c20bfb40b930b271824c26b
Time range: 2026-05-04T04:20:00Z to 2026-05-04T06:00:00Z
text
ExportWorker JID-5c20bfb40b930b271824c26b: fail: 288.783 sec (14:55:28 KST)
ExportWorker JID-5c20bfb40b930b271824c26b: fail: 287.089 sec (14:50:28 KST)
ExportWorker JID-5c20bfb40b930b271824c26b: fail: 288.398 sec (14:40:28 KST)
ExportWorker JID-5c20bfb40b930b271824c26b: fail: 287.942 sec (14:35:28 KST)
ExportWorker JID-5c20bfb40b930b271824c26b: fail: 288.556 sec (14:30:26 KST)

모든 실패는 Sidekiq::Shutdownread_panos 단계에서 create_pointcloud_data(line 632) 또는 create_resource_data(line 551) 실행 중 DB 쿼리가 Sidekiq::Shutdown 시그널에 의해 중단됨:

text
Database export model failed: migration id(1339) last step(read_capture) - Sidekiq::Shutdown: Sidekiq::Shutdown
  .../migration_export_operation.rb:632:in `create_pointcloud_data'
  .../migration_export_operation.rb:495:in `block in create_model_data'
  .../migration_export_operation.rb:419:in `read_panos'
  .../export_worker.rb:138:in `block in perform'

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 MODEL_TREE에서 uploaded pointcloud의 parent/foreign_key가 잘못 정의됨 — cluster/cluster_id 대신 record/record_id여야 함 코드 주석(line 26)에서 두 종류 pointcloud 경로 명시; read_records(line 319)에서 pointcloud_type: 'uploaded'record_id로 조회; uploaded pointcloud는 cluster_id가 nil; 5건 모두 동일 조건에서 에러 MODEL_TREE는 처음부터 이 구조였을 수 있음 (기존 설계 의도 불명) Confirmed
H2 특정 pointcloud 레코드의 데이터 무결성 문제 — cluster_id가 있어야 하는데 null인 데이터 read_records에서 명시적으로 pointcloud_type: 'uploaded'만 조회(line 319); uploaded pointcloud는 설계상 cluster_id가 없음(belongs_to :cluster, optional: true at pointcloud.rb:51) Rejected
H3 Sidekiq::Shutdown이 "Parent model is invalid" 에러의 직접 원인 5건의 에러와 5회의 worker 실패가 동일 JID에서 발생 "Parent model is invalid" 에러는 non-fatal(line 518-519, 로그만 기록하고 계속 진행); Sidekiq::Shutdown은 read_panos 단계(line 419-420)에서 발생하며, 이는 read_record 이후 단계; 두 에러는 독립적 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

app/operations/migration_export_operation.rb:518-519에서 에러 로그 레벨을 warn으로 변경. 현재 이 조건은 uploaded pointcloud에서 설계상 항상 발생하므로 error가 아닌 warn이 적절하다. 이렇게 하면 error monitoring 노이즈가 줄어든다.

단기 개선 (1주 이내)#

  1. MODEL_TREE 이원화: pointcloud 항목을 pointcloud_type에 따라 분리하거나, read_records에서 uploaded pointcloud를 별도 모델명(예: uploaded_pointcloud)으로 매핑하여 parent/foreign_key를 올바르게 설정 (parent: 'record', foreign_key: 'record_id').

  2. Sidekiq::Shutdown 대응 개선: migration id 1339의 export는 read_panos 단계에서 ~288초 소요 후 Sidekiq::Shutdown에 의해 매번 실패한다. read_panos의 DB 쿼리 최적화나 pano 배치 크기 축소를 검토해야 한다. 현재 MIGRATION_CAPTURE_BATCH_SIZE 단위로 처리하지만, 대량 capture에서 timeout이 발생하고 있다.

장기 개선 (재발 방지)#

  • create_model_data에서 parent 검증 실패 시 에러가 아닌 데이터에 대한 skip 또는 fallback 전략 도입 고려. 현재는 에러를 로그하고 parent_id: nil로 model_info를 생성하여 import 시 문제가 될 수 있다.
  • Export job에 job-level timeout을 명시적으로 설정하여 Sidekiq::Shutdown 전에 graceful하게 checkpoint를 저장할 수 있도록 개선.

Monitoring#

  • Uploaded pointcloud parent mismatch 빈도 모니터링:
text
service:cupixworks-api "Parent model is invalid" status:error
  • ExportWorker Sidekiq::Shutdown 빈도 모니터링:
text
service:cupixworks-api "Database export model failed" "Sidekiq::Shutdown"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — MODEL_TREE 수정은 exported JSON 구조에 영향을 줄 수 있으므로 import 측 호환성 확인 필요. 로그 레벨 변경은 trivial.