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#
- 2026-06-08 13:14:07 KST — migration id(1482) MoveWorker 첫 실패 (Pointcloud step). Sidekiq retry 시작. (
Capture move failed) - 2026-06-08 13:14:07 KST — 5회 retry 모두 동일 validation 오류로 실패,
sidekiq_retries_exhausted콜백 호출 →Capture move retries exhausted - 2026-06-08 13:27:31 KST — migration id(1493) 동일 오류로 retry 소진 (cluster 마지막 발생)
Error Log#
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 집합에 속하는지를 검증합니다. MoveWorker → MigrationMoveOperation#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_each→record.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 직전에 예외가 발생합니다.
# 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
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 검증에 걸려 실패합니다.
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
class ChangeDefaultSemanticTaxonomyStateOnPointclouds < ActiveRecord::Migration[7.2]
def change
change_column_default :pointclouds, :semantic_taxonomy_state, from: nil, to: "none"
# default 변경만으로는 기존 NULL 행이 갱신되지 않음 (UPDATE 누락)
end
end
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 (재현):
service:cupixvista-api-migration-worker "1482"
migration id(1482) 실행의 단계별 진행을 보여주는 info 로그 (성공한 단계까지):
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 로그는 존재하지 않으며, 곧바로 다음 에러 로그가 따라옵니다:
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 소진 후:
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 시간 범위의 마지막 항목):
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_state가 NULL이라서 state_machine inclusion validator를 통과하지 못함 |
(1) add_semantic_taxonomy_state_to_pointclouds 마이그레이션이 default 없이 컬럼 추가 — 기존 행은 NULL. (2) 후속 change_default_… 마이그레이션은 default만 변경하고 기존 NULL을 갱신하지 않음. (3) Statable::Pointcloud의 state_machine :semantic_taxonomy_state는 created/none/uploading/uploaded만 허용 → state_machines-activerecord가 inclusion validator 자동 추가. (4) 스택트레이스 최상단이 migration_move_operation.rb:48의 record.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:3207에 t.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주 이내)#
- 컬럼 추가 마이그레이션의 backfill 누락 보완:
add_semantic_taxonomy_state_to_pointclouds마이그레이션의 후속으로 NULL →"none"UPDATE를 수행하는 보정 마이그레이션을 작성.change_column_default만으로는 기존 행이 갱신되지 않는다는 점을 동일 패턴의 다른 컬럼에서도 점검 필요. - 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를 명확히 결정. - MigrationOperation result 처리 일관성 확인: retry 소진 시
MigrationOperation.check_move(... result: 'error')로 종료되는데, 부분 진행 상태(롤백된 row vs 외부 시스템 사이드이펙트)에 대한 오퍼레이션 가시성 보강 — 특히notify_capture_purchase_move는 transaction 후에 호출되므로 실패 케이스에서는 호출되지 않음을 문서화.
장기 개선 (재발 방지)#
- state_machine 컬럼 추가 시의 표준 패턴 정립:
state_machine이 적용되는 컬럼은 (a)null: false, (b) 명시적 default(initial state)로 추가하고, (c) 동일 마이그레이션에서 backfill을 동시에 수행하는 것을 표준화. 코드 리뷰 체크리스트 또는 Rubocop 사용자 정의 규칙 도입 고려. - migration smoke test: production 데이터 스냅샷 기반의 staging에서 record move 류 마이그레이션 워커를 정기적으로 dry-run하여 모델 검증 회귀를 사전 탐지.
- 데이터 무결성 검사 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 쿼리 예시:
textservice:cupixvista-api-migration-worker status:error "Semantic taxonomy state is invalid"textservice: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 동기화 의존성을 면밀히 검토해야 하므로 표준 수준 작업.