ES /docs

Database import retries exhausted: migration id(1843) - ActiveRecord::RecordNotFound: Couldn't find

Fix Plan: ImportWorker cannot find newly-copied Pano because state=abandoned is excluded by default_scope

Summary#

RCA "즉시 조치 (Critical)" 권고에 따라 두 가지 방어를 함께 적용합니다.

  • Option A (근본 원인): migrate_panos 진입 직후 pano_data['pano'] 에서 state == 'abandoned' 인 항목을 걸러내 새 facility 로 복사되지 않도록 한다. pano_resource, mask, migrate_tile_object, migrate_mask_object, set_pano_states_done 모두 이 필터링된 원본 id 집합만 다루게 되어 정합성이 유지된다.
  • Option B (방어): 이후 Pano.find(new_id) 호출 3곳 (라인 708, 755, 793) 을 Pano.unscoped.find(new_id) 로 변경해 default_scope { where.not(state: :abandoned) } 에 걸리지 않도록 한다. Option A 로 정상 데이터에서는 abandoned 가 사라지지만, 향후 다른 경로로 abandoned row 가 복사되더라도 RecordNotFound 로 migration 전체가 중단되지 않도록 하기 위한 방어 코드다.

Changes#

tesla: app/operations/migration_import_operation.rb#

  • What: migrate_panos 진입 지점에서 pano_data['pano'] 를 non-destructive 하게 필터링해 state == 'abandoned' 인 원본 pano 를 migration 대상에서 제외한다. 걸러진 pano 수를 Cupix::Logger.info 로 남긴다. 필터링된 데이터를 로컬 변수(filtered_pano_data) 로 만들어 이후 모든 하위 호출(migrate_model('pano', ...), migrate_resource_object, migrate_tile_object, migrate_mask_object, set_pano_states_done) 에 그대로 전달한다.

  • Why: RCA Root Cause — 원본이 abandoned 인 pano 는 insert_model! 이 state 를 그대로 복사해 새 Pano 도 abandoned 로 저장되고, 이어지는 Pano.find(new_id)Statable::Pano 의 default_scope 에 걸려 RecordNotFound 를 던진다. 비즈니스 관점에서도 abandoned 는 "무시된" 상태이므로 새 facility 로 복사할 대상이 아니다.

  • Lines: migrate_panos 함수 (line 310-347) 진입 부분에 5-10 라인 추가.

  • What: 같은 파일의 migrate_tile_object (line 708), migrate_mask_object (line 755), set_pano_states_done (line 793) 세 지점의 Pano.find(new_id) (또는 model_name.camelize.constantize.find(new_id)) 호출을 unscoped.find(new_id) 를 사용하도록 변경한다.

  • Why: RCA Option B — 방어 코드. Option A 필터로 abandoned 는 걸러지지만, migration 이후 재시도/부분 실패 등으로 새 Pano row 의 state 가 일시적으로 abandoned 가 되는 경로가 남을 수 있다. unscoped 로 조회하면 default_scope 를 우회해 결정론적 실패를 방지한다.

  • Lines: 708, 755, 793 (각 1라인 수정).

Acceptance Criteria#

  • migrate_panospano_data['pano'] 원본 hash 에서 data['state'] == 'abandoned' 인 항목을 걸러낸 뒤 하위 함수를 호출한다.
  • 걸러낸 pano 수와 원본 id 를 info 로그로 기록한다 (Cupix::Logger.info, class/method 메타 포함, 기존 로그 스타일 준수).
  • grep -n "\\.find(new_id)" app/operations/migration_import_operation.rb 결과가 모두 unscoped.find(new_id) 형태이거나 다른 안전한 조회 방식으로 대체되어야 한다 (세 지점 모두).
  • 원본 pano_data 인자는 mutate 하지 않는다 (호출자가 pano_data 를 이후 다른 목적으로 참조할 수 있음).
  • 기존 동작 유지: abandoned 가 아닌 pano 들에 대해서는 이전과 동일하게 migrate_model, migrate_resource_object, migrate_tile_object, migrate_mask_object, set_pano_states_done 흐름이 실행된다.
  • bundle exec rubocop app/operations/migration_import_operation.rb 통과.
  • bundle exec srb tc app/operations/migration_import_operation.rb 통과.

Tests#

  • 기존 테스트: spec/operations/migration_import_operation_spec.rb — 존재하면 통과해야 함 (migrate_panos 를 커버하는 example 이 있으면 abandoned 필터링 시나리오도 함께 확인).
  • 신규 테스트: 없음. RCA 는 데이터 이상 상황의 방어 코드이며, 기존 테스트가 없거나 커버리지가 얕은 상황이므로, 최소 변경 원칙을 준수해 이번 PR 에서 신규 spec 은 추가하지 않는다. (필요 시 후속 이슈로 분리)