ES /docs

Capture move retries exhausted: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed:

RCA: Capture move retries exhausted — Semantic taxonomy state is invalid

Overview#

What Happened#

2026-06-08 KST 오전 시간대에 CupixVista의 MoveWorker가 record/capture move 작업을 처리하던 중, Pointcloud 테이블 업데이트 단계에서 ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid 검증 실패가 반복적으로 발생하여 5회 retry를 모두 소진하고 migration이 error 상태로 종료되었습니다. 동일 원인으로 migration id(1482)와 migration id(1493) 등 최소 2건 이상이 영향받았습니다.

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
runtime Ruby 3.3.0 / Rails (activerecord-7.2.2) / Sidekiq 7.3.9
env production / us-west-2 (tenant: cupix)

Affected Teams#

Team / Domain Error Count Impact
cupixvista-api-migration-worker 7 (cluster) record/capture move 작업이 retry 소진 후 실패 처리됨. 영향 migration id: 1482, 1493 (확인됨)

Timeline#

  1. 2026-06-08 13:14:07 KST — migration id(1482) MoveWorker 첫 실패 (Pointcloud step). Sidekiq retry 시작. (Capture move failed)
  2. 2026-06-08 13:14:07 KST — 5회 retry 모두 동일 validation 오류로 실패, sidekiq_retries_exhausted 콜백 호출 → Capture move retries exhausted
  3. 2026-06-08 13:27:31 KST — migration id(1493) 동일 오류로 retry 소진 (cluster 마지막 발생)

Error Log#

Datadog Logs

text
Capture move retries exhausted: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid

Impact#

  • Service: cupixvista-api-migration-worker
  • 발생 횟수: 7
  • 최초 발생: 2026-06-08 13:14:07 KST
  • 최근 발생: 2026-06-08 13:27:31 KST

영향:

  • record/capture move 작업이 5회 retry 후 error 상태로 marking됨 (MigrationOperation.check_move(... result: 'error')).
  • transaction 내에서 실패하므로 같은 migration의 부분 업데이트는 모두 롤백되지만, 사용자 입장에서는 facility 간 record 이동이 완료되지 않음.
  • 동일 원인으로 production에서 다수 migration이 영향(최소 1482, 1493 확인). semantic_taxonomy_state가 nil인 모든 기존 Pointcloud 레코드에 대해 재현될 수 있음.

Root Cause Summary#

pointclouds.semantic_taxonomy_state 컬럼은 20260316071708_add_semantic_taxonomy_state_to_pointclouds.rb 마이그레이션에서 default 없이 string으로 추가되었기 때문에, 마이그레이션 시점에 이미 존재하던 모든 Pointcloud 행은 이 컬럼이 NULL인 상태로 남아 있습니다. 이후 20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb에서 default를 "none"으로 바꾸었지만 default 변경은 신규 INSERT에만 적용되고 기존 행의 NULL은 그대로 유지됩니다. Pointcloud 모델은 Statable::Pointcloud concern을 통해 state_machine :semantic_taxonomy_state (states: :created, :none, :uploading, :uploaded)를 선언하는데, state_machines-activerecord는 자동으로 inclusion validator를 추가하여 해당 컬럼 값이 선언된 state 집합에 속하는지를 검증합니다. MoveWorkerMigrationMoveOperation#update_table!이 Pointcloud 단계에서 record.update!(facility_id:..., level_id:...)를 호출할 때 Rails가 모든 validator를 실행하고, semantic_taxonomy_state == nil인 기존 행은 이 inclusion 검증에 실패하여 Validation failed: Semantic taxonomy state is invalid 예외가 발생합니다. update 자체는 facility_id/level_id만 변경하지만, 모델 단위 validation이므로 무관한 컬럼의 invalid한 기존 값도 함께 검사됩니다.

Technical Analysis#

Code Path#

  • Entry point: app/workers/move_worker.rb:63 (MoveWorker#perform)
  • Pointcloud iteration: app/operations/migration_move_operation.rb:44 (update_table!find_eachrecord.update!)
  • Failure point: app/operations/migration_move_operation.rb:48 (record.update!(update_attrs))
  • 검증 규칙 위치: app/models/concerns/statable/pointcloud.rb:233-251 (state_machine :semantic_taxonomy_state)
  • 컬럼 정의: db/migrate/20260316071708_add_semantic_taxonomy_state_to_pointclouds.rb (default 없음), db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb (default "none" — 기존 행에는 적용 안 됨)
  • Sidekiq retry exhaustion 콜백: app/workers/move_worker.rb:48-54

MOVE_TABLES는 순서대로 Record → Spacetime → Capture → Video → Cluster → Pointcloud → Mesh → EditingEntity → Pano이며, Datadog 로그에서 completed Cluster까지는 정보 로그가 남고 그 다음 단계인 Pointcloud 직전에 예외가 발생합니다.

app/operations/migration_move_operation.rb:42-58ruby
# 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)   # ← Pointcloud 단계에서 여기서 RecordInvalid 발생
  updated_count += 1
  Cupix::Logger.info("Updated #{model_name} id=#{record.id} (#{changes})", class: self.class.name, function: __method__)
end
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
  # ... events ...
end

state_machines-activerecord 통합은 위 선언을 기반으로 validates_inclusion_of :semantic_taxonomy_state, in: %w[created none uploading uploaded]에 해당하는 validator를 자동 추가합니다. record.update!(facility_id:..., level_id:...)는 update될 attribute가 무엇이든 모델의 모든 validator를 실행하므로, 기존에 저장된 semantic_taxonomy_state = NULL이 inclusion 검증에 걸려 실패합니다.

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
    unless index_exists?(:pointclouds, :semantic_taxonomy_state)
      add_index :pointclouds, :semantic_taxonomy_state
    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"
    # default 변경만으로는 기존 NULL 행이 갱신되지 않음 (UPDATE 누락)
  end
end
app/workers/move_worker.rb:103-112ruby
ActiveRecord::Base.transaction do
  MOVE_TABLES.each do |model_name|
    move_operation.update_table!(model_name)  # 'Pointcloud' 차례에서 RecordInvalid → 트랜잭션 롤백
    Cupix::Logger.info("Record move - migration id(#{migration_id}): completed #{model_name}", ...)
  end
  # ...
end

Log Evidence#

Datadog query (재현):

text
service:cupixvista-api-migration-worker "1482"

migration id(1482) 실행의 단계별 진행을 보여주는 info 로그 (성공한 단계까지):

text
2026-06-08 13:14:07 INFO  Record move begin - migration id(1482) / source record id(7241) / target facility id(6647) / retry_limit(5)
2026-06-08 13:14:07 INFO  Updated 1 Record records for migration_id=1482
2026-06-08 13:14:07 INFO  Record move - migration id(1482): completed Record
2026-06-08 13:14:07 INFO  Updated 1 Spacetime records for migration_id=1482
2026-06-08 13:14:07 INFO  Record move - migration id(1482): completed Spacetime
2026-06-08 13:14:07 INFO  Updated 1 Capture records for migration_id=1482
2026-06-08 13:14:07 INFO  Record move - migration id(1482): completed Capture
2026-06-08 13:14:07 INFO  Updated 2 Video records for migration_id=1482
2026-06-08 13:14:07 INFO  Record move - migration id(1482): completed Video
2026-06-08 13:14:07 INFO  Updated 1 Cluster records for migration_id=1482
2026-06-08 13:14:07 INFO  Record move - migration id(1482): completed Cluster

completed Pointcloud 로그는 존재하지 않으며, 곧바로 다음 에러 로그가 따라옵니다:

text
2026-06-08 13:14:07 ERROR Capture move failed: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid
  /var/app/current/app/operations/migration_move_operation.rb:48:in `block in update_table!'
  /var/app/current/app/operations/migration_move_operation.rb:44:in `update_table!'
  /var/app/current/app/workers/move_worker.rb:106:in `block (2 levels) in perform'
  /var/app/current/app/workers/move_worker.rb:105:in `each'
  /var/app/current/app/workers/move_worker.rb:105:in `block in perform'

5회 retry 소진 후:

text
2026-06-08 13:14:07 ERROR Capture move retries exhausted: migration id(1482) - ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid

동일 패턴이 다른 migration에서도 재현 (cluster 시간 범위의 마지막 항목):

text
2026-06-08 13:27:31 ERROR Capture move retries exhausted: migration id(1493) - ActiveRecord::RecordInvalid: Validation failed: Semantic taxonomy state is invalid

진행 단계가 completed Cluster까지만 찍히고 그 다음 단계인 Pointcloud(MOVE_TABLES에서 6번째)에서 update가 실패함을 보여주는 직접 증거입니다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 기존 Pointcloud 행의 semantic_taxonomy_stateNULL이라서 state_machine inclusion validator를 통과하지 못함 (1) add_semantic_taxonomy_state_to_pointclouds 마이그레이션이 default 없이 컬럼 추가 — 기존 행은 NULL. (2) 후속 change_default_… 마이그레이션은 default만 변경하고 기존 NULL을 갱신하지 않음. (3) Statable::Pointcloudstate_machine :semantic_taxonomy_statecreated/none/uploading/uploaded만 허용 → state_machines-activerecord가 inclusion validator 자동 추가. (4) 스택트레이스 최상단이 migration_move_operation.rb:48record.update!이고 직전 로그가 completed Cluster로, 다음 단계인 Pointcloud에서 실패. Confirmed
H2 MoveWorker가 잘못된 값(예: target_level_id에 다른 facility의 level)을 설정해서 모델이 거부 다른 multi-level 검증 로직(update_pointcloud_levels!)이 실제로 존재함 에러 메시지가 명시적으로 Semantic taxonomy state is invalid이며 level/facility 관련 메시지가 아님. update_attrs에는 facility_id/level_id만 포함되며 semantic_taxonomy_state를 건드리지 않음. Rejected
H3 새 migration(20260316/20260420)이 잘못 적용되어 컬럼이 누락/타입 불일치 db/schema.rb:3207t.string "semantic_taxonomy_state", default: "none"로 정상 반영. 마이그레이션 자체의 정합성 문제 아님. Rejected
H4 데이터베이스 connection / lock 이슈로 일시적 검증 실패 retry 5회 모두 동일 메시지로 실패는 일시적 장애 가능성도 시사 정확히 동일한 validation 메시지가 결정론적으로 반복되며, 다른 migration(1493)에서도 동일 재현. 일시적 장애가 아닌 데이터 상태 문제. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

기존 데이터의 pointclouds.semantic_taxonomy_state IS NULL 행을 안전한 값으로 일괄 갱신하여 추가 migration 실패를 즉시 차단합니다.

  • 영향 테이블: pointclouds (CupixVista DB)
  • 권장 값: "none" (change_default_semantic_taxonomy_state_on_pointclouds가 신규 행에 적용한 default와 동일, 의미상 "semantic taxonomy 미사용/없음")
  • 실행 방법: 데이터 마이그레이션 (Rails migration 또는 1회성 backfill task). 트랜잭션/배치 처리 권장.
  • 근거: state_machine 정의(app/models/concerns/statable/pointcloud.rb:233-251)에서 :none이 명시적으로 허용 state임. created/uploaded는 실제 업로드 흐름과 의미가 결합되어 있어 backfill 값으로 부적절.
  • 기준 위치 참고: db/migrate/20260420031447_change_default_semantic_taxonomy_state_on_pointclouds.rb

단기 개선 (1주 이내)#

  1. 컬럼 추가 마이그레이션의 backfill 누락 보완: add_semantic_taxonomy_state_to_pointclouds 마이그레이션의 후속으로 NULL → "none" UPDATE를 수행하는 보정 마이그레이션을 작성. change_column_default만으로는 기존 행이 갱신되지 않는다는 점을 동일 패턴의 다른 컬럼에서도 점검 필요.
  2. MoveWorker validation 회피 검토: MigrationMoveOperation#update_table!에서 facility_id/level_id만 변경하면 충분한 모델은 update_columns(콜백/검증 우회)을 사용하는 옵션 검토. 단, 현재 코드는 Updated #{model} 콜백을 통한 Elasticsearch 동기화를 의도적으로 트리거하므로(주석: "Perform update individually to trigger callbacks") 우회 시 ES 동기화 회귀 가능성 분석 필수. 무관 컬럼의 기존 invalid 데이터로 인해 정당한 update가 막히지 않도록 trade-off를 명확히 결정.
  3. MigrationOperation result 처리 일관성 확인: retry 소진 시 MigrationOperation.check_move(... result: 'error')로 종료되는데, 부분 진행 상태(롤백된 row vs 외부 시스템 사이드이펙트)에 대한 오퍼레이션 가시성 보강 — 특히 notify_capture_purchase_move는 transaction 후에 호출되므로 실패 케이스에서는 호출되지 않음을 문서화.

장기 개선 (재발 방지)#

  1. state_machine 컬럼 추가 시의 표준 패턴 정립: state_machine이 적용되는 컬럼은 (a) null: false, (b) 명시적 default(initial state)로 추가하고, (c) 동일 마이그레이션에서 backfill을 동시에 수행하는 것을 표준화. 코드 리뷰 체크리스트 또는 Rubocop 사용자 정의 규칙 도입 고려.
  2. migration smoke test: production 데이터 스냅샷 기반의 staging에서 record move 류 마이그레이션 워커를 정기적으로 dry-run하여 모델 검증 회귀를 사전 탐지.
  3. 데이터 무결성 검사 cron: state_machine이 정의된 모든 컬럼에 대해 정의된 state 집합 외 값(또는 NULL) 행 수를 주기적으로 메트릭으로 publish.

Monitoring#

  • Datadog 모니터: service:cupixvista-api-migration-worker status:error "Capture move retries exhausted" 발생 시 알림.

  • 추가 메트릭: Pointcloud.where(semantic_taxonomy_state: nil).count를 주기적으로 측정하여 backfill 미완료/회귀 감지.

  • Datadog 쿼리 예시:

    text
    service:cupixvista-api-migration-worker status:error "Semantic taxonomy state is invalid"
    
    text
    service:cupixvista-api-migration-worker "Capture move retries exhausted"
    

Risk Assessment#

  • Risk level: medium
    • 사용자 측 데이터 손실은 없음 (트랜잭션 롤백). 단, 기능적으로 record move가 실패하여 운영 작업이 차단됨.
    • 동일 원인으로 NULL 상태의 모든 기존 Pointcloud 보유 record에 대해 결정론적으로 재현되므로, 영향 migration이 추가 발생할 가능성 높음.
  • 예상 복잡도: standard
    • 즉시 조치는 단일 SQL UPDATE 또는 짧은 Rails 마이그레이션. validation 우회 옵션은 ES 동기화 의존성을 면밀히 검토해야 하므로 표준 수준 작업.