Parent model is invalid: model(pointcloud)
RCA: Parent model is invalid: model(pointcloud)
Overview#
What Happened#
2026-05-04 05:24:41 UTC에 cupixworks-api의 MigrationExportOperation에서 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#
- 05:24:41 UTC — ExportWorker JID-5c20bfb40b930b271824c26b 시작,
read_record단계에서 uploaded pointcloud 처리 중 "Parent model is invalid: model(pointcloud)" 에러 5건 로그 - 05:25:27 UTC — ExportWorker 첫 번째 실패 (
Sidekiq::Shutdownatread_panos→create_pointcloud_data:632) - 05:30:26 ~ 05:55:28 UTC — Sidekiq retry로 4회 추가 실행, 모두 동일
Sidekiq::Shutdown으로 실패 - 2026-05-04 — error-sweeper가 "Parent model is invalid" 에러 클러스터를 감지
Error Log#
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_reconstructed — cluster_id를 가지며 cluster의 자식, (2) uploaded — record_id만 가지며 cluster_id가 없다. read_records 메서드(line 319)에서 uploaded pointcloud를 조회하여 create_model_data('pointcloud', pointclouds)를 호출하면, MODEL_TREE는 cluster_id를 foreign_key로 참조하므로 parent_id가 nil이 되어 검증 조건 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:19—ExportWorker#perform - Step 1:
read_record단계에서read_records호출 (line 114) - Step 2:
read_records에서 uploaded pointcloud 조회 (line 319)
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)
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)
parent_id = foreign_key ? row[foreign_key.to_s] : nil # row['cluster_id'] => nil (uploaded pointcloud)
- Failure point: 검증 조건에서 에러 로그 발생 (line 517-519)
# 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):
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에는 하나만 반영됨:
# |-- 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 쿼리:
service:cupixworks-api status:error "Parent model is invalid"
Time range: 2026-05-04T04:24:00Z to 2026-05-04T06:00:00Z
5건 모두 동일 속성:
{
"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):
service:cupixworks-api 5c20bfb40b930b271824c26b
Time range: 2026-05-04T04:20:00Z to 2026-05-04T06:00:00Z
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::Shutdown — read_panos 단계에서 create_pointcloud_data(line 632) 또는 create_resource_data(line 551) 실행 중 DB 쿼리가 Sidekiq::Shutdown 시그널에 의해 중단됨:
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주 이내)#
-
MODEL_TREE 이원화:
pointcloud항목을pointcloud_type에 따라 분리하거나,read_records에서 uploaded pointcloud를 별도 모델명(예:uploaded_pointcloud)으로 매핑하여 parent/foreign_key를 올바르게 설정 (parent: 'record',foreign_key: 'record_id'). -
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 빈도 모니터링:
service:cupixworks-api "Parent model is invalid" status:error
- ExportWorker Sidekiq::Shutdown 빈도 모니터링:
service:cupixworks-api "Database export model failed" "Sidekiq::Shutdown"
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard — MODEL_TREE 수정은 exported JSON 구조에 영향을 줄 수 있으므로 import 측 호환성 확인 필요. 로그 레벨 변경은 trivial.