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#
- 2026-07-29 13:18:00 KST —
Tsla13613Backfill1차 실행 시작 (dry_run: false, small batch: 3 scanned, 1 healed, 2 skipped, 0 errors). - 2026-07-29 13:18:01 KST —
Tsla13613Backfill2차 실행 시작 (dry_run: false, main batch). - 2026-07-29 13:18:15 KST — facility 21919 (
uemezf) 에 대해Cupix::Errors::NotFound "Facility not found"발생,Backfill facility defaults: failed로그 기록. - 2026-07-29 13:18:31 KST — facility 21947 (
14gejl) 에 대해 동일 예외 발생. - 2026-07-29 13:18:41 KST — 스크립트 종료 (
Backfill facility defaults done, scanned=405, healed=55, skipped=215, errors=2).
Error Log#
Backfill facility defaults: failed
대표 실패 log attributes (raw, 두 건 모두 동일 구조):
{
"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#
Tsla13613Backfill 는 default_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.show 가 Cupix::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_tagtsla13613_facility_defaults) - Healing 호출:
facility.create_default_models!—app/models/concerns/has_default/facility.rb:47 - 실패 지점:
create_default_level→LevelFactory#create!→FacilityRepository.show - Raise site:
app/repositories/base_repository.rb:355
Healing 대상은 default_level 만 missing (log field "missing": "default_level") 이므로 실제 실행되는 step 은 create_default_level 뿐:
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! 로 새로 만든다:
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) 로 다시 조회한다:
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.show 는 permission_joins 를 통과한 후 model.nil? 이면 ARG10002 를 raise 한다:
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을 생성 →Levelinsert 성공. - 실제:
LevelFactory가 facility 를permission_joins로 재조회 → row 0 →ARG10002raise → 스크립트가 healing 실패로 로깅. facility 는 DB에 존재하고 untrashed 이지만 (Kibana 검증), permission join 결과가 비어 있음.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-worker "Backfill facility defaults: failed"
service:cupixworks-worker "Backfill facility defaults"
Start / done 이벤트 (한 번의 backfill 실행 요약):
{
"@timestamp": "2026-07-29T04:18:01.333Z",
"message": "Backfill facility defaults start",
"class_name": "Tsla13613Backfill",
"dry_run": false
}
{
"@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).levelsindex 조회 시 이 facility 에는Groundlevel (id 88800, ground_level=true) 만 존재하며Defaultlevel 은 없음 → healing 이 정말 필요했고, 정말 실패한 상태. - facility 21947:
id=21947,key=14gejl,name=api-test-no-seed-27min Project,state=created,trashed_at=null.levelsindex 에는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_class 가 Cupix::Errors::NotFound 이며 ActiveRecord::Deadlocked 가 아님. with_deadlock_retry 는 Deadlocked 만 잡음. |
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) 트래킹.
service:cupixworks-worker @error_class:"Cupix::Errors::NotFound" "Facility not found"
service:cupixworks-worker @class_name:EnsureFacilityDefaultsWorker status:error
service:cupixworks-worker @backfill_tag:tsla13613_facility_defaults status:error
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
test-team test fixture 2 건에 국한, 고객 워크플로 영향 없음. 그러나 동일 재조회 경로가 EnsureFacilityDefaultsWorker (실 고객 healing 경로) 에도 동일하게 존재하므로 방치 시 실 고객 facility 에서도 재발 가능성 있음 — 단기 개선으로 진입점을 정리해두는 것이 안전하다.