ES /docs

Backfill facility defaults: failed

RCA: Backfill facility defaults: failed

Overview#

What Happened#

2026-07-29 13:18 KST, prod us-west-2 에서 Tsla13613Backfill 라는 일회성 healing 스크립트가 실행되었다. 이 스크립트는 TSLA-13594/TSLA-13664 여파로 default_level 이 누락된 facility 를 스캔하여 facility.create_default_models! 로 자가치유(heal)를 시도한다. 405개 facility 중 55개가 healed, 215개는 이미 정상이라 skipped, 그리고 2개Cupix::Errors::NotFound (ARG10002, "Facility not found") 로 실패했다. 실패는 LevelFactory#create! 내부의 permission-scoped FacilityRepository.show(facility.key) 재조회에서 발생했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::NotFound
exception.message {:code=>"ARG10002", :reason=>"Facility not found"}
top_frame app/repositories/base_repository.rb:355
runtime Sidekiq / Rails runner (cupixworks-worker), pid 643917
deploy production-us-west-2-20260729T0417Z0-812cb7d9-cupixworks (SHA 812cb7d9)
env production, us-west-2
script Tsla13613Backfill (backfill_tag tsla13613_facility_defaults, ad-hoc, not in tracked repo)

Affected Teams#

Team / Domain Error Count Impact
solarturbines-tst (test team) 2 Test facility 2개가 default_level 을 얻지 못하고 healing 대상에서 제외됨. 실제 고객 워크플로에는 영향 없음 (facility_size=0, name 이 sean-api-test-fac-..., api-test-no-seed-27min 인 test fixture).

Timeline#

  1. 2026-07-29 13:18:00 KSTTsla13613Backfill 1차 실행 시작 (dry_run: false, small batch: 3 scanned, 1 healed, 2 skipped, 0 errors).
  2. 2026-07-29 13:18:01 KSTTsla13613Backfill 2차 실행 시작 (dry_run: false, main batch).
  3. 2026-07-29 13:18:15 KST — facility 21919 (uemezf) 에 대해 Cupix::Errors::NotFound "Facility not found" 발생, Backfill facility defaults: failed 로그 기록.
  4. 2026-07-29 13:18:31 KST — facility 21947 (14gejl) 에 대해 동일 예외 발생.
  5. 2026-07-29 13:18:41 KST — 스크립트 종료 (Backfill facility defaults done, scanned=405, healed=55, skipped=215, errors=2).

Error Log#

Datadog Logs

text
Backfill facility defaults: failed

대표 실패 log attributes (raw, 두 건 모두 동일 구조):

json
{
  "message": "Backfill facility defaults: failed",
  "status": "error",
  "class_name": "Tsla13613Backfill",
  "backfill_tag": "tsla13613_facility_defaults",
  "function": "backfill",
  "error_class": "Cupix::Errors::NotFound",
  "error": { "msg": "{:code=>\"ARG10002\", :reason=>\"Facility not found\"}" },
  "missing": "default_level",
  "facility_id": 21947,
  "facility_key": "14gejl",
  "default_building_id": 44,
  "default_workarea_group_id": 17782,
  "ground_level_id": 88888,
  "tenant": "cupix",
  "@timestamp": "2026-07-29T04:18:31.380Z"
}

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 2
  • 최초 발생: 2026-07-29 13:18 KST
  • 최근 발생: 2026-07-29 13:18 KST

전체 지표로는 405개 대상 중 실패 2건 → 실패율 약 0.49%. 실패한 두 facility 모두 QA/API 자동화 테스트가 만든 test fixture (team solarturbines-tst) 이며, 실 고객 데이터 아님. Kibana 조회 결과 facility 21947 은 이후 다른 경로로 Default level (id 88889) 이 생성되어 있어 실제 orphan 상태로 남아 있지는 않음. facility 21919 은 여전히 Default level 이 없고 Ground level (id 88800) 만 존재 — 남은 orphan 1개.

Root Cause Summary#

Tsla13613Backfilldefault_level 이 없는 facility 를 facility.create_default_models! 로 healing 한다. 이 메서드가 호출하는 LevelFactory#create! (app/factories/level_factory.rb:11) 는 이미 로드된 facility 인스턴스를 그대로 쓰지 않고 FacilityRepository.new(current_user: self.user).show(facility.key)permission-scoped 재조회 를 수행한다. 이 재조회는 BaseRepository.permission_joins 로 team/workspace/facility user permission 테이블을 join 하여 current_user 가 그 facility 를 볼 수 있는지 확인한다. 두 facility 모두 DB 상에는 존재하지만(state: created, trashed_at: null), permission join 결과 row 가 0 이 되어 BaseRepository.showCupix::Errors::NotFound (ARG10002, "Facility not found") 을 raise 했다. create_default_models 의 rescue 는 EnsureFacilityDefaultsWorker 를 재큐잉하지만 backfill 스크립트는 그 rescue 없이 직접 create_default_models! 를 부르는 것으로 보이며 (healed/failed 로그 쌍이 backfill 스크립트 자체에서 나옴), 이 예외가 그대로 Backfill facility defaults: failed 로 로깅되었다. 즉 root cause 는 "backfill 스크립트가 이미 로드한 facility 인스턴스를 healing 경로에서 재조회 없이 사용하지 않고, LevelFactory 내부의 permission-scoped facility 재조회 경로를 통과시켜서, permission chain 이 깨진 test facility 에서 예외가 새어나온 것" 이다.

Technical Analysis#

Code Path#

  • Entry point (ad-hoc script, not in git): Tsla13613Backfill#backfill (function=backfill, backfill_tag tsla13613_facility_defaults)
  • Healing 호출: facility.create_default_models!app/models/concerns/has_default/facility.rb:47
  • 실패 지점: create_default_levelLevelFactory#create!FacilityRepository.show
  • Raise site: app/repositories/base_repository.rb:355

Healing 대상은 default_level 만 missing (log field "missing": "default_level") 이므로 실제 실행되는 step 은 create_default_level 뿐:

app/models/concerns/has_default/facility.rb:47-52ruby
def create_default_models!
  with_deadlock_retry { create_default_building }
  with_deadlock_retry { create_default_workarea_group }
  with_deadlock_retry { create_ground_level } unless skip_ground_level?
  with_deadlock_retry { create_default_level } unless skip_default_level?
end

create_default_level 은 아직 default_level 이 없으면 LevelFactory.create! 로 새로 만든다:

app/models/concerns/has_default/facility.rb:89-100ruby
def create_default_level
  return if levels.untrashed.any?(&:default_level?)

  LevelFactory.new(current_user: self.user).create!({
    facility: self,
    team: self.team,
    name: 'Default',
    default_level: true,
    ceiling_height: 3.0,
    elevation: 0.0
  })
end

문제의 재조회는 LevelFactory#create! 초입에서 발생한다. facility 를 파라미터로 이미 받았음에도 FacilityRepository.show(facility.key) 로 다시 조회한다:

app/factories/level_factory.rb:5-14ruby
def create!(params = {})
  self.model = ::Level.new

  if params[:facility_key].present?
    self.parent = FacilityRepository.new(current_user: self.current_user).show(params[:facility_key])
  elsif params[:facility].present?
    self.parent = FacilityRepository.new(current_user: self.current_user).show(params[:facility].key)
  else
    raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'facility_key is required')
  end

BaseRepository.showpermission_joins 를 통과한 후 model.nil? 이면 ARG10002 를 raise 한다:

app/repositories/base_repository.rb:331-357ruby
query =
  if skip_permission || current_user == ::User.unauthorized_user
    where(attrs)
  elsif current_class == ::Review || (review_id || capture_id).present?
    permission_joins(default_joins(current_class), current_user, review_id: review_id || -1, capture_id: capture_id || -1).where(attrs)
  elsif current_user.present?
    permission_joins(default_joins(current_class), current_user).where(attrs)
  else
    raise Cupix::Errors::System.new(code: 'SYS30000', reason: 'current_user or review is required on Repository')
  end

scope = current_class.visibility_scope(visibility)
model = query.merge(scope).first
# ...
if model.nil?
  if self.where(attrs).in_trash.present?
    raise Cupix::Errors::NotFound.new(code: 'ENT4000', reason: "#{current_class.name} not found")
  else
    raise Cupix::Errors::NotFound.new(code: 'ARG10002', reason: "#{current_class.name} not found")
  end
end

기대 동작 vs 실제 동작

  • 기대: healing 스크립트가 이미 DB 에서 로드한 facility 인스턴스를 그대로 사용하여 default_level 을 생성 → Level insert 성공.
  • 실제: LevelFactory 가 facility 를 permission_joins 로 재조회 → row 0 → ARG10002 raise → 스크립트가 healing 실패로 로깅. facility 는 DB에 존재하고 untrashed 이지만 (Kibana 검증), permission join 결과가 비어 있음.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-worker "Backfill facility defaults: failed"
text
service:cupixworks-worker "Backfill facility defaults"

Start / done 이벤트 (한 번의 backfill 실행 요약):

json
{
  "@timestamp": "2026-07-29T04:18:01.333Z",
  "message": "Backfill facility defaults start",
  "class_name": "Tsla13613Backfill",
  "dry_run": false
}
json
{
  "@timestamp": "2026-07-29T04:18:41.396Z",
  "message": "Backfill facility defaults done",
  "class_name": "Tsla13613Backfill",
  "scanned": 405,
  "healed": 55,
  "skipped": 215,
  "errors": 2,
  "dry_run": false
}

두 실패 이벤트 요약 (동일 tenant, 동일 team, 동일 예외):

@timestamp (UTC) KST facility_id facility_key ground_level_id missing error.msg
2026-07-29T04:18:15.351Z 2026-07-29 13:18:15 21919 uemezf 88799 default_level {:code=>"ARG10002", :reason=>"Facility not found"}
2026-07-29T04:18:31.380Z 2026-07-29 13:18:31 21947 14gejl 88888 default_level {:code=>"ARG10002", :reason=>"Facility not found"}

Kibana 확인 (facilities / levels index, prod):

  • facility 21919: id=21919, key=uemezf, name=sean-api-test-fac-20260724-194829, state=created, trashed_at=null, team=solarturbines-tst (id 1148), user=Sean Ko (41973). levels index 조회 시 이 facility 에는 Ground level (id 88800, ground_level=true) 만 존재하며 Default level 은 없음 → healing 이 정말 필요했고, 정말 실패한 상태.
  • facility 21947: id=21947, key=14gejl, name=api-test-no-seed-27min Project, state=created, trashed_at=null. levels index 에는 Ground (88888) 과 Default (88889) 이 둘 다 존재 — 이 두 번째 facility 는 backfill 실패 이후 다른 경로 (예: 사용자 재요청) 로 default 가 만들어져 있음.

두 facility 모두 이름 / 사이즈 (facility_size=0) 로 보아 QA/자동화 테스트가 만든 test fixture 이고, team 도메인은 solarturbines-tst (내부 QA 팀).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 LevelFactory#create! 가 permission-scoped FacilityRepository.show 로 facility 를 재조회하는데, 두 test facility 에서 permission_joins 결과가 비어 ARG10002 를 raise 했다 (root cause). level_factory.rb:11 에서 show(facility.key) 호출 → base_repository.rb:355 에서 raise. error message 문자열 "Facility not found"base_repository.rb:355"#{current_class.name} not found" 형식과 정확히 일치. 두 log 모두 facility_id/facility_key 필드가 채워져 있어 facility 인스턴스는 존재했음이 확인됨. Kibana: 두 facility 모두 DB 에 존재. Confirmed
H2 Facility 가 실제로 삭제 (trashed/purged) 되었다 error code ARG10002 (일반 not found) — 만약 trashed 였다면 code ENT4000 로 raise 되었을 것 (base_repository.rb:352-354). Kibana: trashed_at=null, purged_at=null, cycle_state="created". code 도 ENT4000 이 아닌 ARG10002. Rejected
H3 Deadlock retry 소진으로 실패했다 (TSLA-13664 시나리오) error_classCupix::Errors::NotFound 이며 ActiveRecord::Deadlocked 가 아님. with_deadlock_retryDeadlocked 만 잡음. Rejected
H4 Facility 의 user (Sean Ko) 가 그 facility 에 대한 permission 을 잃어서 permission_joins 가 row 0 을 반환했다 두 facility 모두 test fixture (API automation 이 만들고 잊혀진 것) 이고 team 은 solarturbines-tst. Test fixture 는 종종 permission row 를 완전히 세팅하지 않거나 후처리로 정리됨. facility.user (Sean Ko) 가 아직 team member 이지만 workspace/facility 권한 row 가 회수/누락되었을 가능성 있음. 직접적으로 permission 테이블 dump 가 없어 100% 확정은 불가 — 검증 필요. Inconclusive (H1 의 하위 원인, 검증 필요)

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 실패 2건은 test-team 의 test fixture 이며 실제 고객에게 영향 없음. Backfill 스크립트는 이미 종료되었고 healed=55 로 대다수는 성공. 남은 orphan (facility 21919) 은 test fixture 이므로 정리 대상.
  • 필요하다면 남은 test facility 는 QA 팀이 API 로 삭제하거나 관리자가 직접 정리.

단기 개선 (1주 이내)#

  • LevelFactory#create! (app/factories/level_factory.rb:5-14) 의 FacilityRepository.show 재조회는 controller 경유 flow 에서 권한을 재확인하려는 의도가 명백하지만, create_default_level 처럼 model 콜백 / healing worker 가 이미 로드한 facility 를 들고 부르는 내부 경로에서는 불필요하며 permission chain 지연/누락에 취약하다. 방향:
    • LevelFactory#create!params[:facility] 를 직접 받을 때는 .show(facility.key) 대신 그 인스턴스를 그대로 self.parent 로 쓰거나, show(..., skip_permission: true) 를 명시적으로 전달할 수 있는 별도 진입점을 마련. BaseRepository.show 는 이미 skip_permission: true 옵션을 지원한다 (base_repository.rb:306).
    • create_default_level 같은 시스템 healing 경로에서는 LevelFactory 대신 sibling 인 create_default_building / create_default_workarea_group 처럼 Level 을 직접 create! 로 삽입하거나, skip_permission 을 통과시키는 factory 진입점을 사용하여 대칭 맞추기.
  • 백필 스크립트 Tsla13613Backfill 이 tesla repo 에 tracked 되어 있지 않다면 (grep 결과 존재 안 함), 재현 가능성을 위해 lib/tasks/tesla/backfill_facility_defaults.rake 형태로 커밋하는 것도 고려 (예: backfill_editing_stats.rake 패턴).

장기 개선 (재발 방지)#

  • 신규 facility 생성 시 permission row 가 항상 완결적으로 만들어지는지 회귀 테스트 / 모니터. facility_user_permissions, workspace_user_permissions, team_user_permissions 세 chain 중 어느 하나라도 누락되면 owner 도 permission_joins 실패 가능.
  • Healing worker (EnsureFacilityDefaultsWorker) 도 동일한 경로로 실패할 수 있으므로 (같은 create_default_models!LevelFactory 재조회 체인), 단기 개선의 진입점 수정이 healing worker 재발도 예방한다.

Monitoring#

  • Datadog cupixworks-worker 에서 Cupix::Errors::NotFound + Facility not found 조합을 healing 경로 태그와 함께 알림.
  • EnsureFacilityDefaultsWorker 의 실패율 (Sidekiq retry / dead set) 트래킹.
text
service:cupixworks-worker @error_class:"Cupix::Errors::NotFound" "Facility not found"
text
service:cupixworks-worker @class_name:EnsureFacilityDefaultsWorker status:error
text
service:cupixworks-worker @backfill_tag:tsla13613_facility_defaults status:error

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard

test-team test fixture 2 건에 국한, 고객 워크플로 영향 없음. 그러나 동일 재조회 경로가 EnsureFacilityDefaultsWorker (실 고객 healing 경로) 에도 동일하게 존재하므로 방치 시 실 고객 facility 에서도 재발 가능성 있음 — 단기 개선으로 진입점을 정리해두는 것이 안전하다.