Parent model is invalid: model(pointcloud)
RCA: Parent model is invalid: model(pointcloud)
Overview#
What Happened#
2026-07-13 20:12 KST, cupixworks-migration-worker 서비스(eu-central-1, tenant cupix)에서 facility 11162 를 migration id 1885 로 export 하는 도중 MigrationExportOperation#create_model_data 가 pointcloud 4건에 대해 Parent model is invalid: model(pointcloud) error 로그를 출력했다. 예외가 raise 되지는 않았고 export 는 계속 진행되었지만, 해당 pointcloud 들은 parent_id: nil 로 exported JSON 에 기록되어 import 측에서 문제가 될 수 있는 상태다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | MigrationExportOperation (로그 이벤트, exception 아님) |
| exception.message | Parent model is invalid: model(pointcloud) |
| top_frame | app/operations/migration_export_operation.rb:519 |
| deploy | production-eu-central-1-20260713T1002Z0-dcf0aa79-cupixworks |
| env | production / eu-central-1 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-migration-worker (facility 11162, migration 1885) | 4 | Export 는 완료되었으나 exported pointcloud 4건이 parent_id: nil 상태로 기록됨 — 이후 import 시 orphan 처리 필요 |
Timeline#
- 2026-07-13 20:12:53 KST —
ExportWorker#perform이 migration id 1885 시작 (Database export begin - migration id(1885) retry_limit(5)) - 2026-07-13 20:12:53 KST — facility 11162 의 record/pointcloud 를 읽는 단계에서
MigrationExportOperation#create_model_data('pointcloud', pointclouds)가 uploaded pointcloud 4건을 처리 - 2026-07-13 20:12:53 KST — 각 pointcloud 에서
parent_id.nil? && parent.present?조건이 성립하여 error 로그 4건 발생
Error Log#
Parent model is invalid: model(pointcloud)
Impact#
- Service:
cupixworks-migration-worker - 발생 횟수: 4
- 최초 발생: 2026-07-13 20:12 KST
- 최근 발생: 2026-07-13 20:12 KST
- 단일 migration(id=1885, facility=11162, tenant=cupix)에 국한. 예외가 아닌 error-level 로그이므로 job 은 실패하지 않지만, exported JSON 에 4개의 uploaded pointcloud 가
parent: "cluster", parent_id: nil로 기록되어 import 측 무결성에 잠재적 영향.
Root Cause Summary#
MigrationExportOperation::MODEL_TREE 는 pointcloud 의 parent 를 cluster, foreign_key 를 cluster_id 로 고정 정의하고 있다 (migration_export_operation.rb:127). 그러나 실제 Pointcloud 모델은 pointcloud_type 이 uploaded 인 것과 3d_reconstructed 인 것 두 종류가 있고, 파일 상단의 주석(라인 26 “pointcloud(customer upload)” 은 level 아래) 및 라인 50 “pointcloud (3d reconstruction)” 은 cluster 아래)이 이를 명시적으로 구분한다. read_records (라인 319) 는 Pointcloud.where(record_id: record_ids).where(pointcloud_type: 'uploaded') 로 record 에 직접 매달린 uploaded pointcloud 를 조회하는데, 이들은 cluster_id 가 nil 이다. 이 rows 를 create_model_data('pointcloud', pointclouds) 로 넘기면 라인 485 에서 parent_id = row['cluster_id'] → nil 이 되고, 라인 518-520 의 “wrong parent model definition” 가드에 걸려 error 로그가 남는다. 즉 MODEL_TREE 스키마가 pointcloud 의 두 가지 상위 관계(record 또는 cluster)를 표현하지 못하는 것이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
app/workers/export_worker.rb#perform(Sidekiq) — 로그의class: "ExportWorker", function: "perform"로 확인 - Facility 하위 데이터 export 중
MigrationExportOperation#read_records호출 read_records에서 uploaded pointcloud 조회 후create_model_data('pointcloud', pointclouds)호출create_model_data가 각 row 에 대해 MODEL_TREE 의 parent/foreign_key 를 참조해 parent_id 를 뽑는 지점에서 nil 검출
MODEL_TREE 상 pointcloud 정의:
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' },
read_records 는 uploaded 타입을 명시적으로 조회한다 — 이 pointcloud row 들은 record_id 는 있지만 cluster_id 는 nil:
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)
create_model_data 는 MODEL_TREE 만 참조하므로 row 의 실제 컬럼 값과 무관하게 foreign_key='cluster_id' 를 뽑는다:
parent = MODEL_TREE[model_name.to_sym]&.dig(:parent)
foreign_key = MODEL_TREE[model_name.to_sym]&.dig(:foreign_key)
# rows are hash type of to be exported model
rows.each do |row|
# Remove storage_option from sys column if present to allow target environment to set its own storage
sanitize_sys_column!(row, model_name)
parent_id = foreign_key ? row[foreign_key.to_s] : nil
# ...
# 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
기대 동작: uploaded pointcloud 는 record 에 매달려 있으므로 parent='record', parent_id=row['record_id'] 로 export 되어야 함. 실제 동작: MODEL_TREE 가 pointcloud 를 “cluster 자식” 하나로만 정의해, uploaded 케이스에서 parent_id=nil 이 되어 “wrong parent model definition” 가드가 유효 데이터를 잘못 flag 하고, exported JSON 에는 부모 없는 pointcloud entry 가 남는다.
Pointcloud 모델도 두 타입을 명시:
ALLOWED_POINTCLOUD_TYPE = %w[uploaded 3d_reconstructed].freeze
# ...
validates :record_id, presence: true, on: [:create]
validates :pointcloud_type, presence: true, inclusion: { in: ALLOWED_POINTCLOUD_TYPE, message: "Invalid pointcloud_type, use one of #{ALLOWED_POINTCLOUD_TYPE.join(' | ')}" }
scope :reconstructed, -> { where(pointcloud_type: '3d_reconstructed') }
scope :uploaded_pointcloud_type, -> { where(pointcloud_type: 'uploaded') }
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-migration-worker status:error @environment:production "Parent model is invalid: model(pointcloud)"
에러 로그(4건 동일):
{
"timestamp": "2026-07-13 20:12:53 KST",
"status": "error",
"message": "Parent model is invalid: model(pointcloud)",
"class": "MigrationExportOperation"
}
동일 시각(20:12:53 KST) migration 1885 흐름 (쿼리: service:cupixworks-migration-worker @environment:production "migration id"):
20:12:53 INFO Database export begin - migration id(1885) retry_limit(5)
20:12:53 INFO Database export - migration id(1885): proceed({:last_step=>"initialize", :facility_id=>11162, ...})
20:12:53 INFO Database export - migration id(1885): read record(record, record permission, spacetime, pointcloud, pointcloud permission) for facility id(11162)
20:12:53 INFO Database export - migration id(1885): read capture(capture, capture permission, capture_type, video, cluster, pointcloud, pointcloud permission) for record ids([5701])
20:12:53 ERROR Parent model is invalid: model(pointcloud) x4
- migration id: 1885, facility_id: 11162, record_ids: [5701], tenant: cupix, region: eu-central-1
- error 는
read_records(record 하위 pointcloud) 단계 로그 다음에 발생 — uploaded pointcloud 처리 경로와 일치.read_captures도 pointcloud 를 다루지만pointcloud_type: '3d_reconstructed'필터가 있어(라인 366)capture_id/cluster_id가 채워진 rows 만 오므로 이 에러의 소스일 수 없다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | MODEL_TREE 가 pointcloud 의 uploaded/3d_reconstructed 두 부모 관계를 표현하지 못해 uploaded pointcloud 의 cluster_id=nil 이 “wrong parent” 가드에 걸린다 | 코드: read_records 라인 319 가 uploaded 만 조회, MODEL_TREE 라인 127 이 parent=cluster 로 하드코딩, 파일 상단 주석(라인 26 vs 50)이 두 관계를 구분. 로그: 에러 4건이 read_records 단계 직후 발생, create_resource_data warn 도 4건 → 동일 batch 의 pointcloud 4건과 일치 |
— | Confirmed |
| H2 | read_captures 의 3d_reconstructed pointcloud 가 cluster_id 없이 저장되어 발생 |
코드적으로 같은 error path 를 공유 | 라인 366 에서 pointcloud_type: '3d_reconstructed' 로 필터되어 cluster_id 가 있어야 조회 대상이 됨; DB validation 상 3d_reconstructed 는 cluster_id presence 를 요구 (모델 스코프로 추정); 에러가 read capture 로그보다 앞선 read record 단계에서 발생 |
Rejected |
| H3 | Pointcloud row 가 실제로 손상되어 cluster_id 컬럼이 예상 밖으로 nil | 로그만으로는 배제 불가 — DB 확인 필요 | 4건 동시 발생 & tenant/facility 격리 → 스키마 레벨 이슈에 부합, row 단위 손상 시나리오는 4건 동시 확률이 낮음 | Inconclusive (needs DB verify) |
| H4 | 외부 dependency 장애 (status-board svc:cupixworks-migration-worker::unknown) |
최근 resolved 인시던트 2건 존재 (2026-07-13 04:07, 2026-07-10 05:13) | active 인시던트 없음, 원인 서비스도 migration-worker 자신, 에러 메시지가 dependency timeout/5xx 계열 아님 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/operations/migration_export_operation.rb의create_model_data(라인 470-536) 에서model_name == 'pointcloud'이고row['cluster_id']가 nil 인 경우 uploaded 케이스로 판단하여 parent='record', foreign_key='record_id' 로 재설정하도록 분기. 또는sanitize_sys_column!옆에 pointcloud 전용 override helper 를 두어 MODEL_TREE 참조를 우회. (구현 라인 5-10 정도)- 로그 레벨은 우선 그대로 두되, "wrong parent" 가드에는 실제로 스키마 정의가 잘못된 경우와 “정상 uploaded pointcloud” 케이스를 분리하는 조건을 추가할 것 — 후자는 error 가 아님.
단기 개선 (1주 이내)#
MODEL_TREE스키마를parent하나가 아닌 배열/조건 리스트로 확장하여 “model + row 조건 → parent” 매핑을 표현. 예: pointcloud 는pointcloud_type=='uploaded'이면 record, 그 외에는 cluster.- Export 결과 JSON validation 단계 추가:
parent_id: nil인 entry 를 export 종료 직전에 카운트해서 0 이 아니면 job 실패 처리 — 이런 케이스가 무성 무결성 이슈를 만들지 않도록. - Import 측(
MigrationImportOperation) 에서parent_id: nil처리 로직을 확인하고, 현재 어떻게 다루는지 문서화.
장기 개선 (재발 방지)#
- Pointcloud 처럼 다중 부모를 갖는 polymorphic-ish 모델을 위한 일반화된 tree 표현. Rails polymorphic association 을 그대로 반영하는 export DSL 을 검토.
- Migration export/import 왕복 테스트 (integration test) 를 uploaded pointcloud 가 포함된 facility 로 확장 — 현재 회귀 커버리지가 부족했음을 시사.
Monitoring#
- error 발생 시 migration_id, facility_id, tenant 를 로그 payload 에 포함하도록
Cupix::Logger.error콜을 확장 (현재는 model 이름만 있음) — 추적 용이성 향상.
Datadog 쿼리 예시:
service:cupixworks-migration-worker status:error "Parent model is invalid"
시계열 카운트(release dashboard timeseries widget 용):
count:logs("service:cupixworks-migration-worker status:error \"Parent model is invalid\"").index("*").rollup("count").by("environment")
Risk Assessment#
- Risk level: medium — 데이터 export 자체는 계속 성공하지만 exported JSON 이
parent_id: nil를 포함해 import 측에서 orphan 처리에 따라 조용한 데이터 손실 가능성. - 예상 복잡도: standard — MODEL_TREE 를 확장하거나 pointcloud 특수 분기를 추가하는 국지적 수정. Import 측 동작 확인이 병행 필요.