ES /docs

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#

  1. 2026-03-16pointclouds.semantic_taxonomy_state 컬럼 추가 (default 없음, NULL 허용) — db/migrate/20260316071708_add_semantic_taxonomy_state_to_pointclouds.rb
  2. 2026-04-20semantic_taxonomy_state default 가 "none" 으로 변경됨 — db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb. 그러나 이미 존재하던 row 들의 NULL/legacy 값은 backfill 되지 않음
  3. 2026-06-08 13:14:07 KST — migration id(1482) Record move begin (source_record_id=7241, target_facility_id=6647)
  4. 2026-06-08 13:14:07 KST — Record/Spacetime/Capture/Video/Cluster 단계는 정상 완료, Pointcloud 단계에서 update! 실패 → 트랜잭션 롤백
  5. 2026-06-08 13:14:07 ~ 13:27:31 KST — Sidekiq 재시도 5회 모두 동일하게 실패, 7개 migration (1482, 1483, 1484, 1485, 1487, 1490, 1493) retries exhaustedMigrationOperation.check_move(result: 'error')

Error Log#

Datadog Logs

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

MoveWorkerMOVE_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)

app/workers/move_worker.rb:103-112ruby
# 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_TABLESRecord → Spacetime → Capture → Video → Cluster → Pointcloud → Mesh → EditingEntity → Pano 순서이며, Pointcloud 단계에서 실패가 발생함은 Datadog 로그가 Cluster 까지 completed 를 찍고 그 다음 직후 에러가 발생한 것으로 확인된다 (Log Evidence 참조).

Failure point: app/operations/migration_move_operation.rb:48

app/operations/migration_move_operation.rb:42-54ruby
# 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_attrsfacility_id, level_id 만 포함하지만, ActiveRecord 의 save! 는 모델 전체에 대한 validation 을 수행한다. 이때 state_machine 라이브러리가 자동으로 등록한 inclusion validator 가 trigger 된다.

State 정의: app/models/concerns/statable/pointcloud.rb:233-251

app/models/concerns/statable/pointcloud.rb:233-251ruby
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 의 에러 메시지와 정확히 일치한다.

스키마 컨텍스트:

db/migrate/20260316071708_add_semantic_taxonomy_state_to_pointclouds.rbruby
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
db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rbruby
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 가능):

text
service:cupixvista-api-migration-worker "migration id(1482)"

migration id(1482) 의 진행 흐름 (timestamp 모두 2026-06-08 13:14:07 KST):

text
[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 들도 동일 패턴:

text
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):

text
service:cupixvista-api-migration-worker "Updated Pointcloud"

샘플 결과 (성공한 row):

text
[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 시점에 ES pointclouds 인덱스에서 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 추가:

    text
    service:cupixvista-api-migration-worker "Capture move retries exhausted"
    

    threshold: 1 occurrence in 10m → P2 alert (Vista 이관 실패는 즉시 대응 필요).

  • 사전 탐지용 daily 쿼리 (warehouse / Rails console):

    text
    SELECT 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 메트릭:

    text
    status: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 값의 정확한 매핑 결정이 선행되어야 한다.