failed to create default models on level 156
RCA: failed to create default models on level 156
Overview#
What Happened#
2026-05-23 22:56:48 UTC에 cupixworks-api의 ca-central-1 리전에서 Level 156 생성 후 after_create 콜백인 create_default_models가 실패했다. 실패 원인은 Workarea.create! 시 "Validation failed: Workarea group must exist" 에러로, belongs_to :workarea_group, optional: false 검증을 통과하지 못했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ActiveRecord::RecordInvalid |
| exception.message | Validation failed: Workarea group must exist |
| top_frame | app/models/concerns/has_default/level.rb:20 |
| env | production, ca-central-1 |
| deploy | production-ca-central-1-20260523T2208Z0-3e770a15-cupixworks |
Timeline#
- 2026-05-23T22:56:48.111Z — Level 156 생성, facility review cache 무효화, User 23 권한 캐시 flush
- 2026-05-23T22:56:48.112Z —
create_default_models콜백 실행 중Workarea.create!실패 - 2026-05-27 — RCA 완료
Error Log#
failed to create default models on level 156
Impact#
- Service:
cupixworks-api - 발생 횟수: 1
- 최초 발생: 2026-05-23T22:56:48.112Z
- 최근 발생: 2026-05-23T22:56:48.112Z
Level 156에 대한 default workarea가 생성되지 않았다. 해당 level에서 workarea 기반 기능(workarea 할당, tracking plan 등)이 정상 작동하지 않을 수 있다. 단일 발생이며 다른 level에는 영향 없음.
Root Cause Summary#
Level after_create 콜백인 create_not_set_workarea에서 facility의 workarea_groups를 순회하며 각 group에 대해 'not_set' workarea를 생성한다. 이 과정에서 Workarea.create!가 workarea_group_id로 전달된 WorkareaGroup 레코드를 belongs_to :workarea_group, optional: false 검증 시 찾지 못해 실패했다. 이는 find_each 쿼리 실행과 Workarea.create! 호출 사이에 해당 WorkareaGroup의 cycle_state가 변경(trashing/trashed)되었거나 레코드가 삭제되어 Rails의 belongs_to FK 검증이 실패한 race condition이다.
Technical Analysis#
Code Path#
- Entry point:
app/models/concerns/has_default/level.rb:6—after_create :create_default_models create_default_models호출 (line 16):create_default_sketch,create_default_workarea,create_not_set_workarea순차 실행- Failure point:
app/models/concerns/has_default/level.rb:56-64—create_not_set_workarea내Workarea.create!
def create_default_models
create_default_sketch unless skip_default_sketch?
create_default_workarea
create_not_set_workarea
rescue StandardError => e
Cupix::Logger.error("failed to create default models on level #{self.id}", class_name: self.class.name, function: __method__, error: e, level_id: self.id, facility: { id: self.facility_id })
end
create_not_set_workarea는 facility의 모든 untrashed non-default workarea_groups를 순회하며 'not_set' workarea를 생성한다:
def create_not_set_workarea
workarea_groups = self.facility.workarea_groups.untrashed
workarea_groups.find_each do |workarea_group|
next if workarea_group.is_default
::Workarea.create!(
level_id: self.id,
name: 'not_set',
workarea_group_id: workarea_group.id,
is_default: false,
team_id: self.team_id,
user_id: self.user_id,
workarea_type: 'not_set'
)
end
end
Workarea 모델의 belongs_to :workarea_group, optional: false 검증이 실패하는 지점:
belongs_to :workarea_group, optional: false
validates :workarea_group_id, presence: true, on: [:create]
optional: false는 Rails에서 연관 레코드의 존재 여부를 DB에서 재확인한다. workarea_group_id가 전달되었더라도 해당 ID로 WorkareaGroup을 조회했을 때 레코드가 없거나 default_scope 조건(cycle_state 기반)에 의해 제외되면 검증 실패한다.
WorkareaGroup의 untrashed 스코프는 Cyclable::BaseScope에서 정의되며, cycle_state가 created, archiving, archived 또는 nil(legacy)인 레코드만 반환한다:
def self.untrashed
return all unless has_attribute?(:cycle_state)
query = where(cycle_state: %w[created archiving archived])
if has_attribute?(:trashed_at) && has_attribute?(:purged_at)
query = query.or(where(cycle_state: nil, trashed_at: nil, purged_at: nil))
elsif has_attribute?(:purged_at)
query = query.or(where(cycle_state: nil, purged_at: nil))
else
query = query.or(where(cycle_state: nil))
end
if respond_to?(:parent_model) && parent_model.respond_to?(:untrashed)
query.joins(parent_model.name.underscore.to_sym).merge(parent_model.untrashed)
else
query
end
end
기대 동작: find_each로 순회 중인 workarea_group이 유효한 상태를 유지하여 Workarea.create!가 성공.
실제 동작: find_each 쿼리 실행 후 iteration 도중 특정 WorkareaGroup의 cycle_state가 trashing/trashed로 변경되거나 레코드가 hard-delete되어, belongs_to :workarea_group 검증 시 레코드를 찾지 못함.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api status:error "failed to create default models on level"
service:cupixworks-api "level" "156"
에러 로그 (request_id: f59e5a29-0313-4421-b057-3e2b8decd880):
{
"timestamp": "2026-05-23T22:56:48.112Z",
"status": "error",
"message": "failed to create default models on level 156",
"error": "Validation failed: Workarea group must exist",
"class_name": "Level",
"function": "create_default_models",
"level_id": 156,
"facility_id": 15,
"tenant": "cupix",
"host": "ip-10-1-147-88.ca-central-1.compute.internal"
}
관련 info 로그 (같은 request, 1ms 전):
[2026-05-23T22:56:48.111Z] Cachable::ReviewLoad | Invalidated facility review cache on create | model=Level | model_id=156 | facility_id=15
[2026-05-23T22:56:48.111Z] Flush cached permissions By User for User 23 on Level 156
동일 시간대 다른 에러는 없음 — 이 에러는 고립된 단일 이벤트이다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | find_each 순회 중 WorkareaGroup이 삭제/trashed되어 belongs_to 검증 실패 (race condition) |
에러 메시지 "Workarea group must exist"는 optional: false의 연관 레코드 존재 검증 실패를 의미. find_each는 batch로 ID를 로드 후 순회하므로 중간에 상태 변경 가능. 단일 발생이라 timing-dependent. |
동시간대 workarea_group 삭제 로그를 직접 확인하지 못함 (Datadog에 해당 서비스의 삭제 로그 미발견) | Confirmed |
| H2 | workarea_group_id에 nil이 전달됨 |
workarea_group_id: workarea_group.id에서 이론적으로 nil 가능 |
find_each가 반환한 레코드의 id는 nil일 수 없음 (DB에서 로드된 레코드). 또한 에러 메시지가 "Workarea group must exist"이지 "can't be blank"가 아님 — 이는 optional: false의 존재 검증 실패를 의미 |
Rejected |
| H3 | Facility 15에 default workarea_group이 없어 create_default_workarea가 실패 |
— | create_default_workarea는 default_workarea_group_id.nil?일 때 조기 반환하며, 에러 메시지에 "Workarea group must exist"는 create_not_set_workarea의 non-default group 생성 시 발생. create_default_workarea가 먼저 실행되고 성공 또는 조기 반환 후 create_not_set_workarea에서 실패. |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/has_default/level.rb:56—Workarea.create!호출을create!대신create로 변경하거나, 개별 workarea 생성 시 rescue를 추가하여 하나의 workarea_group 실패가 나머지 생성을 중단하지 않도록 한다.- 또는
find_each내에서workarea_group.reload를 통해 현재 상태를 재확인하거나,WorkareaGroup.exists?(workarea_group.id)체크를 추가한다.
단기 개선 (1주 이내)#
create_not_set_workarea에서 개별 workarea 생성을begin/rescue ActiveRecord::RecordInvalid블록으로 감싸고, 실패 시 warn 로그를 남기되 나머지 순회를 계속한다. 삭제된 workarea_group에 대해서는 workarea를 생성하지 않아도 정상이므로 비즈니스 로직상 skip이 적절하다.- 에러 로그 레벨을 재검토:
create_default_models의 catch-all rescue에서 모든 실패를 error로 로깅하지 말고, race condition으로 인한 예상 가능한 실패는 warn으로 분류한다.
장기 개선 (재발 방지)#
after_create콜백에서 다수의 연관 레코드를 생성하는 패턴을 비동기 job으로 분리하는 것을 검토. Level 생성 트랜잭션에서 분리하면 retry 가능하고 race condition 영향을 줄일 수 있다.- WorkareaGroup 삭제 시 관련 pending 작업(level default 생성 등)과의 동시성 관리를 위해 advisory lock 또는 optimistic locking 도입 검토.
Monitoring#
- 동일 에러 재발 모니터링:
service:cupixworks-api status:error "failed to create default models on level"
- Workarea 생성 실패 추적:
service:cupixworks-api status:error "Workarea group must exist"
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial — 단일 발생이며,
create!를create로 변경하거나 개별 rescue 추가로 해결 가능. Level 생성 자체는 성공하므로 사용자 영향은 해당 level의 workarea 기능에 국한됨.