ES /docs

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#

  1. 2026-05-23T22:56:48.111Z — Level 156 생성, facility review cache 무효화, User 23 권한 캐시 flush
  2. 2026-05-23T22:56:48.112Zcreate_default_models 콜백 실행 중 Workarea.create! 실패
  3. 2026-05-27 — RCA 완료

Error Log#

Datadog Logs

text
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:6after_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-64create_not_set_workareaWorkarea.create!
app/models/concerns/has_default/level.rb:16-22ruby
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를 생성한다:

app/models/concerns/has_default/level.rb:50-66ruby
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 검증이 실패하는 지점:

app/models/workarea.rb:17-18ruby
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_statecreated, archiving, archived 또는 nil(legacy)인 레코드만 반환한다:

app/models/concerns/cyclable/base_scope.rb:35-59ruby
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 쿼리:

text
service:cupixworks-api status:error "failed to create default models on level"
text
service:cupixworks-api "level" "156"

에러 로그 (request_id: f59e5a29-0313-4421-b057-3e2b8decd880):

json
{
  "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 전):

text
[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_workareadefault_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:56Workarea.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#

  • 동일 에러 재발 모니터링:
text
service:cupixworks-api status:error "failed to create default models on level"
  • Workarea 생성 실패 추적:
text
service:cupixworks-api status:error "Workarea group must exist"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 단일 발생이며, create!create로 변경하거나 개별 rescue 추가로 해결 가능. Level 생성 자체는 성공하므로 사용자 영향은 해당 level의 workarea 기능에 국한됨.