Capture move failed: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed: Semantic t
RCA: Capture move failed - Semantic taxonomy state is invalid
Overview#
What Happened#
2026-06-08 13:14 KST 부터 약 13분 동안 cupixvista-api-migration-worker (stage 환경) 에서 MoveWorker 가 7개의 migration (id 1482, 1483, 1484, 1485, 1487, 1490, 1493) 에 대해 ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid 로 실패하며 총 32회 (각 migration × Sidekiq 최대 5회 재시도) 에러가 기록되었다. MOVE_TABLES 순서 중 Pointcloud 단계에서 record.update! 가 state_machine validation 에 의해 거부되었고, 트랜잭션이 롤백되어 해당 migration 들은 모두 'error' 상태로 종료되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ActiveRecord::RecordInvalid |
| exception.message | Validation failed: Semantic taxonomy state is invalid |
| top_frame | app/operations/migration_move_operation.rb:48 |
| caller | app/workers/move_worker.rb:106 (each MOVE_TABLES) |
| runtime | Ruby 3.3.0, Rails 7.2.2, state_machines-activerecord 0.8.0 |
| env | stage (us-west-2), tenant=cupix (CupixVista) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| CupixVista migration (stage) | 32 (7 migrations × ≤5 retries) | 7건의 record/capture facility 이동 작업이 실패하고 'error' 상태로 종료됨. 해당 migration 들은 자동 재시도 후에도 복구 불가 |
Timeline#
- 2026-03-16 —
pointclouds.semantic_taxonomy_state컬럼 추가 (default 없음, NULL 허용) —db/migrate/20260316071708_add_semantic_taxonomy_state_to_pointclouds.rb - 2026-04-20 —
semantic_taxonomy_statedefault 가"none"으로 변경됨 —db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb. 그러나 이미 존재하던 row 들의 NULL/legacy 값은 backfill 되지 않음 - 2026-06-08 13:14:07 KST — migration id(1482)
Record move begin(source_record_id=7241, target_facility_id=6647) - 2026-06-08 13:14:07 KST — Record/Spacetime/Capture/Video/Cluster 단계는 정상 완료, Pointcloud 단계에서
update!실패 → 트랜잭션 롤백 - 2026-06-08 13:14:07 ~ 13:27:31 KST — Sidekiq 재시도 5회 모두 동일하게 실패, 7개 migration (1482, 1483, 1484, 1485, 1487, 1490, 1493)
retries exhausted→MigrationOperation.check_move(result: 'error')
Error Log#
Capture move failed: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid
/var/app/current/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2/lib/active_record/validations.rb:87:in `raise_validation_error'
...
/var/app/current/vendor/bundle/ruby/3.3.0/gems/state_machines-activerecord-0.8.0/lib/state_machines/integrations/active_record.rb:515:in `block in save!'
...
/var/app/current/app/operations/migration_move_operation.rb:48:in `block in update_table!'
...
/var/app/current/app/workers/move_worker.rb:106:in `block (2 levels) in perform'
Impact#
- Service:
cupixvista-api-migration-worker - Environment: stage (us-west-2)
- 발생 횟수: 32
- 영향 받은 migration: 7건 (1482, 1483, 1484, 1485, 1487, 1490, 1493)
- 최초 발생: 2026-06-08 13:14 KST
- 최근 발생: 2026-06-08 13:27 KST
- 사용자 영향: Vista 이관 작업이 진행되지 않으며 데이터는 부분적으로 변경되지 않음 (단일 트랜잭션이라 롤백됨). 다만 운영자는 'error' 상태를 받게 되어 수동 개입이 필요.
Root Cause Summary#
MoveWorker 가 MOVE_TABLES 순서대로 Pointcloud 까지 처리할 때, MigrationMoveOperation#update_table! 의 record.update!(facility_id: ..., level_id: ...) (app/operations/migration_move_operation.rb:48) 가 state_machine 의 자동 validation 을 트리거한다. Pointcloud 모델은 Statable::Pointcloud concern (app/models/concerns/statable/pointcloud.rb:233-251) 을 통해 state_machine :semantic_taxonomy_state 를 정의하며 허용 값은 created, none, uploading, uploaded 4가지뿐이다. semantic_taxonomy_state 컬럼은 2026-03-16 에 추가되었고 (db/migrate/20260316071708_add_semantic_taxonomy_state_to_pointclouds.rb) default 가 없었기 때문에 그 시점 이전·이후로 생성된 일부 Pointcloud row 의 값이 NULL 이거나 허용 목록에 없는 legacy 값을 가지고 있다. 이런 row 를 update! 하면 (변경하려는 컬럼이 facility_id, level_id 뿐이어도) state_machine 이 모든 state 컬럼을 검증하므로 Semantic taxonomy state is invalid 가 발생한다. 2026-04-20 에 default 가 "none" 으로 바뀌었으나 (20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb) 기존 row 들에 대한 backfill update 는 수행되지 않아 잔존 데이터로 인해 stage 의 Vista facility 이관이 실패하고 있다.
Technical Analysis#
Code Path#
Entry point: app/workers/move_worker.rb:63 (MoveWorker#perform)
# Process all tables in a single transaction
ActiveRecord::Base.transaction do
MOVE_TABLES.each do |model_name|
move_operation.update_table!(model_name)
Cupix::Logger.info("Record move - migration id(#{migration_id}): completed #{model_name}", class: self.class.name, function: __method__)
end
move_operation.move_reviews!
Cupix::Logger.info("Record move - migration id(#{migration_id}): completed Review", class: self.class.name, function: __method__)
end
MOVE_TABLES 는 Record → Spacetime → Capture → Video → Cluster → Pointcloud → Mesh → EditingEntity → Pano 순서이며, Pointcloud 단계에서 실패가 발생함은 Datadog 로그가 Cluster 까지 completed 를 찍고 그 다음 직후 에러가 발생한 것으로 확인된다 (Log Evidence 참조).
Failure point: app/operations/migration_move_operation.rb:48
# Perform update individually to trigger callbacks (for Elasticsearch sync)
updated_count = 0
model_class.where(where_conditions).find_each do |record|
update_pointcloud_levels!(record) if model_name == 'Pointcloud'
changes = update_attrs.map { |key, new_value| "#{key}: #{record[key]} -> #{new_value}" }.join(', ')
record.update!(update_attrs) # ← 여기서 ActiveRecord::RecordInvalid 발생
updated_count += 1
Cupix::Logger.info("Updated #{model_name} id=#{record.id} (#{changes})", class: self.class.name, function: __method__)
end
update! 는 update_attrs 에 facility_id, level_id 만 포함하지만, ActiveRecord 의 save! 는 모델 전체에 대한 validation 을 수행한다. 이때 state_machine 라이브러리가 자동으로 등록한 inclusion validator 가 trigger 된다.
State 정의: app/models/concerns/statable/pointcloud.rb:233-251
state_machine :semantic_taxonomy_state, initial: :created, namespace: :semantic_taxonomy_state do
state :created,
:none,
:uploading,
:uploaded do
end
event :reset do
transition any => :created
end
event :none do
transition any - [:none] => :none
end
event :uploading do
transition any - [:uploading] => :uploading
end
event :uploaded do
transition any - [:uploaded] => :uploaded
end
end
state_machines-activerecord 는 위의 4개 state 외 값(NULL 포함)이 컬럼에 들어 있으면 save! 시 validation error 메시지 "Semantic taxonomy state is invalid" 를 발생시킨다 — 이는 Datadog 의 에러 메시지와 정확히 일치한다.
스키마 컨텍스트:
class AddSemanticTaxonomyStateToPointclouds < ActiveRecord::Migration[7.1]
def change
unless column_exists?(:pointclouds, :semantic_taxonomy_state)
add_column :pointclouds, :semantic_taxonomy_state, :string # default 없음 → NULL 가능
end
...
end
end
class ChangeDefaultSemanticTaxonomyStateOnPointclouds < ActiveRecord::Migration[7.2]
def change
change_column_default :pointclouds, :semantic_taxonomy_state, from: nil, to: "none"
# 기존 row backfill 없음 → 이전 row 들은 NULL 유지
end
end
기대 동작 vs 실제 동작:
- 기대: Pointcloud
update!가facility_id/level_id만 갱신하고 정상 저장된다. - 실제: state_machine 의 inclusion validator 가 컬럼의 현재(저장된)
semantic_taxonomy_state값을 검증하면서 NULL 또는 legacy 값을 거부 →RecordInvalid발생 → 트랜잭션 롤백.
Log Evidence#
Datadog query 1 (re-run 가능):
service:cupixvista-api-migration-worker "migration id(1482)"
migration id(1482) 의 진행 흐름 (timestamp 모두 2026-06-08 13:14:07 KST):
[info] Record move begin - migration id(1482) / source record id(7241) / target facility id(6647) / retry_limit(5)
[info] Record move - migration id(1482): completed Record
[info] Record move - migration id(1482): completed Spacetime
[info] Record move - migration id(1482): completed Capture
[info] Record move - migration id(1482): completed Video
[info] Record move - migration id(1482): completed Cluster
[error] Capture move failed: migration id(1482) - ActiveRecord::RecordInvalid:
Validation failed: Semantic taxonomy state is invalid
… app/operations/migration_move_operation.rb:48 in `block in update_table!'
… app/workers/move_worker.rb:106 in `block (2 levels) in perform'
[error] Capture move retries exhausted: migration id(1482) - ActiveRecord::RecordInvalid: ...
Cluster 까지는 completed 로그가 있지만 Pointcloud completed 로그는 없다. MOVE_TABLES 정의(app/workers/move_worker.rb:36-46) 상 Cluster 다음 순서가 Pointcloud 이므로 실패 지점은 Pointcloud 임이 확정된다.
Datadog query 2 — 다른 migration 들도 동일 패턴:
service:cupixvista-api-migration-worker "Capture move retries exhausted"
영향 받은 migration ids: 1482, 1483, 1484, 1485, 1487, 1490, 1493 (총 7건)
Datadog query 3 — 일부 Pointcloud 는 정상 update 됨 (state 컬럼이 valid 인 row):
service:cupixvista-api-migration-worker "Updated Pointcloud"
샘플 결과 (성공한 row):
[info] Updated Pointcloud id=26690 (facility_id: 4530 -> 711, level_id: 9058 -> 1420)
[info] Updated Pointcloud id=26689 (facility_id: 4530 -> 711, level_id: 9058 -> 1420)
[info] Updated Pointcloud id=28996 (facility_id: 6602 -> 6902, level_id: 13202 -> 13802)
find_each 가 row 단위로 처리하므로 일부는 성공하고 invalid state 를 가진 row 가 등장하는 순간 트랜잭션 전체가 롤백된다 → 성공 로그가 남았더라도 DB 변경은 롤백되어 적용되지 않는다.
주의: 위 stage 환경에서 invalid
semantic_taxonomy_state값을 가진 정확한 Pointcloud id 는 본 RCA 시점에 ESpointclouds인덱스에서 record_id=7241 로 조회되지 않아 (해당 인덱스가 production-us 데이터만 갖고 있을 가능성) 직접 확인하지 못했다 — uncertain -- needs verification (실제 invalid row 식별은 stage DB 직접 쿼리 필요:SELECT id, semantic_taxonomy_state FROM pointclouds WHERE record_id IN (...) AND (semantic_taxonomy_state IS NULL OR semantic_taxonomy_state NOT IN ('created','none','uploading','uploaded'))).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 일부 Pointcloud row 의 semantic_taxonomy_state 가 NULL 또는 legacy 값이라 state_machine validation 이 거부함 |
(1) state_machine 정의 app/models/concerns/statable/pointcloud.rb:233-251 의 4개 state 외 값을 거부; (2) 컬럼이 2026-03-16 default 없이 추가됨; (3) default 가 2026-04-20 에 "none" 으로 바뀌었지만 backfill 없음; (4) 일부 Pointcloud 는 정상 update 됨 (Updated Pointcloud id=26689 로그) — 즉 컬럼/state_machine 자체는 정상 동작 |
— | Confirmed |
| H2 | update_pointcloud_levels! 가 잘못된 level_id 를 setting 해서 다른 validation (예: MultiLevel#validate_levels) 이 실패 |
— | 에러 메시지가 명확히 "Semantic taxonomy state is invalid" 로 다른 validator 가 아님; 또한 stack trace 에 validate_levels 관련 frame 없음 |
Rejected |
| H3 | state_machine 이 transition 시도 중 실패 (예: 이벤트가 잘못 fire 됨) | — | update!(facility_id:, level_id:) 는 semantic_taxonomy_state 를 변경하지 않으므로 transition event 가 fire 되지 않음. 메시지 "is invalid" 는 inclusion validator 패턴 (transition 거부 시 메시지가 다름) |
Rejected |
| H4 | DB 컬럼 타입 mismatch / 인코딩 문제로 valid 값도 거부됨 | — | 같은 컬럼/같은 worker 실행 중 26689, 26690 등은 정상 처리됨 → 컬럼 자체는 정상 | Rejected |
| H5 | source/target facility 데이터 불일치로 update 자체가 실패 | — | Record/Spacetime/Capture/Video/Cluster 단계는 동일 source/target 으로 정상 완료됨 → facility 매개변수는 정상 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
Stage DB 의 invalid semantic_taxonomy_state row 를 backfill 하여 차단된 7개 migration 의 재실행을 가능하게 한다.
- 대상 row 식별 SQL (stage):
text
SELECT id, record_id, facility_id, semantic_taxonomy_state FROM pointclouds WHERE semantic_taxonomy_state IS NULL OR semantic_taxonomy_state NOT IN ('created','none','uploading','uploaded'); - backfill 정책: NULL →
'none'(2026-04-20 마이그레이션의 default 와 일치). legacy 값이 발견되면 그 값 별로 별도 매핑 결정 필요. - backfill 후 차단된 migration (1482, 1483, 1484, 1485, 1487, 1490, 1493) 을
MigrationOperation.check_move또는 운영 콘솔에서 재시도. - 근거:
db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb가 default 만 변경하고 backfill update 를 누락한 것이 잔존 데이터 원인이며, state_machine 허용값(created/none/uploading/uploaded)과 일관성을 맞춰야 함.
단기 개선 (1주 이내)#
- Pointclouds 전체 backfill 마이그레이션 추가: stage 뿐만 아니라 모든 환경(production-us, production-au, production-eu)에 대해
UPDATE pointclouds SET semantic_taxonomy_state = 'none' WHERE semantic_taxonomy_state IS NULL OR semantic_taxonomy_state NOT IN (...)마이그레이션 작성. data migration 으로 분리하여 schema migration 과 별개 PR 권장. - MoveWorker 의 partial-failure 가시성 개선: 현재는 어떤 모델이 실패했는지 에러 메시지에 포함되지 않음.
MigrationMoveOperation#update_table!의rescue ActiveRecord::RecordInvalid블록을 추가하여model_name,record.id,record.errors.full_messages를 로깅한 후 re-raise 하도록 개선. 향후 유사 사고에서 어느 row 가 문제인지 즉시 식별 가능.
장기 개선 (재발 방지)#
- state_machine 도입 시 backfill 의무화 가이드:
state_machine :xxx, initial: :yyy를 추가하는 마이그레이션 PR 에서는 (1) 컬럼 default 설정과 (2) 기존 row backfill 을 동일 PR 또는 후속 data migration 으로 반드시 포함하도록 codeowner/리뷰 가이드 추가. - 모델 dirty-attribute aware validation: 변경되지 않은 state 컬럼에 대해서는 validation 을 skip 하는 방식 검토 (단,
state_machines-activerecord라이브러리의 동작 변경이 필요하므로 신중 검토). 또는 facility/level 만 갱신하는 admin/migration code path 에서는update_columns(validation/callback skip) 사용을 검토 — 단, MigrationMoveOperation 의 주석 (Perform update individually to trigger callbacks (for Elasticsearch sync)) 처럼 ES 동기화 callback 이 필요한 경우update_columns는 부적합하므로 callback 만 수동 호출하는 helper 도입 고려.
Monitoring#
-
Datadog monitor 추가:
textservice:cupixvista-api-migration-worker "Capture move retries exhausted"threshold: 1 occurrence in 10m → P2 alert (Vista 이관 실패는 즉시 대응 필요).
-
사전 탐지용 daily 쿼리 (warehouse / Rails console):
textSELECT COUNT(*) FROM pointclouds WHERE semantic_taxonomy_state IS NULL OR semantic_taxonomy_state NOT IN ('created','none','uploading','uploaded');결과 > 0 이면 backfill 필요. CI 또는 nightly job 으로 모니터링.
-
추가로
MoveWorker진입/완료 ratio 메트릭:textstatus:info "Record move begin" service:cupixvista-api-migration-worker status:info "Record move success" service:cupixvista-api-migration-worker성공 비율이 떨어지면 알림.
Risk Assessment#
- Risk level: medium — stage 환경의 Vista facility 이관 자동화가 완전히 차단되어 있으나 production 영향은 아직 미확인 (production 에도 동일 잔존 데이터 패턴이 있을 가능성 있음 → 단기 개선 항목으로 production backfill 점검 필수).
- 예상 복잡도: standard — fix 자체는 단순한 data backfill 이지만, 모든 region (us/au/eu) 의
pointclouds테이블에 대해 안전하게 적용해야 하며, legacy 값의 정확한 매핑 결정이 선행되어야 한다.