ES /docs

error on sitetrack_id: 21354 - undefined method `untrashed?' for nil

RCA: undefined method `untrashed?' for nil in CreateSitetrackEditingEntitiesWorker

Overview#

What Happened#

2026-07-16 01:29 KST에 production us-west-2 리전 cupixworks-worker 에서 CreateSitetrackEditingEntitiesWorker 가 sitetrack_id 21354 를 처리하다가 NoMethodError: undefined method 'untrashed?' for nil 로 실패했다. 해당 sitetrack 의 editing entity 자동 배정(assign) 로직에서 참조 무결성이 깨진 editing_id 를 만나 nil 에 .untrashed? 를 호출했다. 이번 창(window)에서 확인된 발생 횟수는 1회이며 동일 sitetrack 에 대해 30분 간격으로 두 번 실행된 흔적이 남아 있다.

Quick Facts#

Field Value
exception.class NoMethodError
exception.message undefined method 'untrashed?' for nil
top_frame app/models/concerns/finalization/editing_entity.rb:39
runtime Ruby on Rails / Sidekiq worker
deploy production-us-west-2-20260715T0858Z0-f2b18e95-cupixworks
env production, us-west-2
tenant cupix

Affected Teams#

Team / Domain Error Count Impact
cupixworks-worker / Sitetrack Editing pipeline 1 단일 sitetrack (21354) 의 editing entity 생성 job 이 예외로 중단됨. retry: 1 설정으로 인해 자동 재시도가 1회 발생. 해당 sitetrack 의 후속 파이프라인(EditingSplitWorker 등)이 트리거되지 않음.

Timeline#

  1. 2026-07-16 00:55:40 KSTCreateSitetrackEditingEntitiesWorker 첫 실행 시작 (start creating editing entities on sitetrack_id: 21354).
  2. 2026-07-16 01:00:40 KST — 두 번째 실행 시작 (Sidekiq retry: 1 또는 재큐잉으로 추정, 5분 뒤).
  3. 2026-07-16 01:29:36 KST — 두 번째 실행에서 NoMethodError 발생, 워커의 rescue StandardError 블록이 에러 로그를 기록하고 종료.
  4. 2026-07-16 (이후) — 동일 sitetrack 에 대한 후속 로그 없음 (14일 검색창 내 확인).

Error Log#

Datadog Logs

text
error on sitetrack_id: 21354 - undefined method `untrashed?' for nil

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 1
  • 최초 발생: 2026-07-16 01:29 KST
  • 최근 발생: 2026-07-16 01:29 KST

Root Cause Summary#

Finalization::EditingEntity#_do_assign_editing_to_editing_entity 의 첫 줄 (app/models/concerns/finalization/editing_entity.rb:39) 은 self.editing_id.present? && self.editing.untrashed? 로 short-circuit early-return 을 시도한다. 그러나 EditingEntity#editing_id 는 값이 있어도 belongs_to :editing, optional: true 관계가 nil 을 반환할 수 있다 — 참조 대상 Editing 레코드가 하드 삭제되었거나(purge) 캐시 stale 상태이면 그렇다. 바로 다음 라인 (line 42) 은 !self.editing&.untrashed? 로 safe navigation 을 쓰지만, line 39 는 &. 없이 .untrashed? 를 호출하기 때문에 nil.untrashed?NoMethodError 가 발생한다. 즉 line 42 의 방어 로직이 있음에도 line 39 에서 먼저 raise 되어 도달 불가능한 상태다.

Technical Analysis#

Code Path#

  • Entry point: app/workers/create_sitetrack_editing_entities_worker.rb:26perform(sitetrack_id)
  • Task 단위 lock 획득 후 각 task 에 대해 create_editing_entityassign_editing_to_editing_entity 호출: app/workers/create_sitetrack_editing_entities_worker.rb:64-72
  • assign_editing_to_editing_entity 는 lock retry 래퍼로 _do_assign_editing_to_editing_entity 를 호출: app/models/concerns/finalization/editing_entity.rb:16-36
  • Failure point: app/models/concerns/finalization/editing_entity.rb:39self.editing.untrashed? (nil 에서 호출됨)

Worker 의 task 처리 루프:

app/workers/create_sitetrack_editing_entities_worker.rb:64-79ruby
uniq_task_ids.each do |task_id|
  task_lock = _acquire_task_lock(task_id, sitetrack_id)
  begin
    task = ::Task.find_by_id(task_id)
    level_id = ::Workarea.find_by_id(task.workarea_id).level_id

    editing_entity = task.create_editing_entity(sitetrack.target_id, task.category_id, task.workarea_id, level_id, element_count[task_id])
    editing_entity.assign_editing_to_editing_entity
    editing_entity.ready_state!
    editing_ids << editing_entity.editing_id if editing_entity.editing_id.present?

    Cupix::Logger.info('editing_entity is created', class: self.class.name, function: __method__, sitetrack: { id: sitetrack_id }, task: { id: task_id }, editing_entity: { id: editing_entity.id, count: editing_entity.entity_count, editing_id: editing_entity.editing_id })
  ensure
    _release_task_lock(task_lock)
  end
end

문제의 두 줄 (line 39 는 raw call, line 42 는 safe navigation) — 서로 상충하는 nil 처리:

app/models/concerns/finalization/editing_entity.rb:38-42ruby
def _do_assign_editing_to_editing_entity
  return if self.editing_id.present? && self.editing.untrashed?

  # Clear stale editing_id if editing is trashed, so create_editing can proceed
  self.update_column(:editing_id, nil) if self.editing_id.present? && !self.editing&.untrashed?
  • 기대 동작: editing_id 가 설정되어 있고 참조된 Editing 이 유효(untrashed?)하면 early-return, 그렇지 않으면(참조가 trashed 이거나 유령 상태) line 42 에서 editing_id 를 nil 로 클리어하고 새로 배정을 진행.
  • 실제 동작: editing_id 가 있으나 Editing 레코드가 존재하지 않아 self.editing 이 nil 을 반환. line 39 가 nil.untrashed? 를 호출하여 NoMethodError 발생. line 42 의 stale-clear 로직에 도달하지 못함. 결과적으로 워커의 rescue StandardError 가 예외를 잡아 error 로그로 기록.

참고 — 트래시 상태 판별 API:

app/models/concerns/trashable.rb:9-53ruby
def self.untrashed
  where(trashed_at: nil, purged_at: nil)
end
# ...
def untrashed?
  trashed_at.nil? && purged_at.nil?
end

untrashed? 는 인스턴스 메서드이므로 nil 수신자에서는 항상 NoMethodError 를 던진다.

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-worker "21354"

시간 범위 (2026-07-15T15:00Z ~ 2026-07-15T18:00Z) 내 전체 로그 (3건, KST 로 변환):

text
2026-07-16 01:29:36 KST  error  error on sitetrack_id: 21354 - undefined method `untrashed?' for nil
                                class=CreateSitetrackEditingEntitiesWorker function=perform
2026-07-16 01:00:40 KST  info   start creating editing entities on sitetrack_id: 21354
2026-07-16 00:55:40 KST  info   start creating editing entities on sitetrack_id: 21354

Raw 필드 (첫 번째 로그):

json
{
  "level": "error",
  "environment": "production",
  "service": "cupixworks-worker",
  "class": "CreateSitetrackEditingEntitiesWorker",
  "function": "perform",
  "sitetrack": { "id": 21354 },
  "request_id": "acea4d700aefdd323c791057",
  "tenant": "cupix",
  "@timestamp": "2026-07-15T16:29:36.818Z",
  "version": "production-us-west-2-20260715T0858Z0-f2b18e95-cupixworks",
  "host": "ip-10-1-18-149.us-west-2.compute.internal"
}

관찰 포인트:

  • 같은 sitetrack_id: 21354 에 대해 5분 간격으로 두 개의 start creating editing entities info 로그가 존재. Sidekiq retry: 1 옵션(app/workers/create_sitetrack_editing_entities_worker.rb:3) 및 재큐잉 가능성과 일치.
  • 두 실행 모두에 대해 uniq_task_ids length: ... info 로그(create_sitetrack_editing_entities_worker.rb:43)나 editing_entity is created 로그(create_sitetrack_editing_entities_worker.rb:75)가 남지 않았다 — 첫 실행에서 이미 예외가 발생해 로그가 절단되었을 가능성이 있으나 error 로그는 마지막(01:29:36) 한 건만 캡처됨.
  • 첫 실행(00:55:40) 과 두 번째 실행(01:00:40) 사이의 5분, 그리고 두 번째 실행부터 에러(01:29:36) 까지의 약 29분 사이 어느 시점에 EditingEntity#editing_id 가 참조하던 Editing 레코드가 사라진 것으로 추정 — 다만 해당 레코드 하드 삭제/purge 로그는 이 검색창에서 직접 확인되지 않았다 (uncertain -- needs verification via Kibana editings index).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 _do_assign_editing_to_editing_entity line 39 가 .untrashed? 를 safe navigation 없이 호출해 참조 대상 Editing 이 사라진 경우 nil raise 에러 메시지 문구가 정확히 undefined method 'untrashed?' for nil, top-level rescue 는 워커에 하나뿐이며 assign_editing_to_editing_entity 를 호출하는 경로에서 .untrashed? 를 부르는 위치는 line 39 뿐임 (grep 결과: line 39, 42, 463 중 42 는 &., 463 은 self 대상). Line 42 가 &. 로 방어하는데 line 39 는 안 함 — 코드 자체가 불일치 Confirmed
H2 sitetrack 이 nil (::Sitetrack.find_by_id 결과) 이어서 sitetrack.target_id 에서 nil 오류 발생 워커 라인 30 이 find_by_id 사용, nil 이면 라인 70 에서 참조 시 예외 에러 메시지가 sitetrack 이 아니라 untrashed? 를 지목. 두 번의 start creating editing entities 로그가 파라미터 없이 sitetrack_id 만 로깅하므로 sitetrack 존재 여부 확인 불가하나, 에러 시점은 uniq_task_ids info 로그 이후에 발생하는 assign 단계임. 실제 예외 메시지가 다름 Rejected
H3 task 또는 workarea find_by_id 가 nil 반환 (task.workarea_id / .level_id 접근에서 실패) find_by_id 는 못 찾으면 nil, Task.find_by_id(nil).workarea_idNoMethodError 발생 에러 메시지가 untrashed? 를 지목. workarea_id/level_id 호출에서 nil 이면 다른 메서드 이름이 메시지에 나옴 Rejected
H4 _editing_in_ready.state_waiting? 에서 nil 발생 (_choose_editing 이 nil 반환) Line 95 에서 _editing_in_ready.ready_state! 앞에 .state_waiting? 호출, nil 이면 유사한 오류 에러 메시지가 untrashed? 지목. state_waiting? 이면 메서드 이름이 다름 Rejected
H5 Sitetrack 21354 처리 중 참조된 Editing 이 다른 워커에 의해 _trash_empty_victim_editings (line 472) 로 trash + purge 되어 EditingEntity.editing_id 는 남고 대상 레코드가 사라짐 워커에서 Editing.trash! (line 466, 480) + eventual purge 파이프라인 존재. Line 42 의 코멘트 "Clear stale editing_id if editing is trashed" 자체가 이 시나리오를 방어하려는 의도임을 시사 정확한 purge 타임라인 로그는 이 검색창에서 확인 못 함 (uncertain — Kibana editings index 조회 필요) Inconclusive (contributing factor to H1)

Verdict: root cause 는 H1. H5 는 nil 이 발생한 상태(editing_id 는 있으나 editing 이 nil)를 만든 상위 시나리오로 유력하지만 직접 로그 증거가 부족해 별도 검증 필요.

Fix Recommendation#

즉시 조치 (Critical)#

app/models/concerns/finalization/editing_entity.rb:39.untrashed? 호출을 safe navigation 으로 변경해 nil 수신자에서 raise 되지 않도록 한다. Line 42 가 이미 동일 패턴(self.editing&.untrashed?)을 사용하고 있으므로 line 39 도 동일하게 맞춘다.

app/models/concerns/finalization/editing_entity.rb:38-42
 def _do_assign_editing_to_editing_entity-  return if self.editing_id.present? && self.editing.untrashed?+  return if self.editing_id.present? && self.editing&.untrashed?   # Clear stale editing_id if editing is trashed, so create_editing can proceed   self.update_column(:editing_id, nil) if self.editing_id.present? && !self.editing&.untrashed?

이 변경으로 editing 이 nil 인 orphan 상태에서도 line 42 가 정상적으로 editing_id 를 nil 로 클리어하고 새 Editing 배정으로 진행한다.

단기 개선 (1주 이내)#

  • EditingEntity 가 orphan editing_id 를 갖게 되는 상위 원인(H5) 규명: _trash_empty_victim_editings (line 472-485) 및 관련 purge 파이프라인이 참조하는 EditingEntity.editing_id 를 함께 nil 로 셋해주는지, 아니면 Editing 만 trash/purge 하고 EditingEntity 는 그대로 두는지 확인. 후자면 EditingEntity 쪽 정리가 누락되어 있는 것.
  • 배정 로직에 diagnostic 로그 추가: editing_id.present? 인데 editing.nil? 인 상황을 warn 레벨로 남겨 재발 시 즉시 관찰 가능하도록 한다 (에러 대신 warn 이면 자동 복구 흐름 유지).
  • assign_editing_to_editing_entity 의 rescue 는 워커 단에만 있어 실패 sitetrack 에 대한 파이프라인 재진입이 없다. 실패 sitetrack 을 대상으로 하는 재실행/알림 채널을 재검토.

장기 개선 (재발 방지)#

  • EditingEntity.editing_idEditing.id 간 참조 무결성 정책 명문화: DB FK 로 강제할지, 애플리케이션 level 에서 파괴/purge 시 clear-cascade 를 강제할지 결정.
  • Sitetrack 21354 같은 특정 케이스가 반복되는지 추적하기 위해 실패 sitetrack 목록을 별도 지표/대시보드로 노출.
  • 유사 패턴(some_assoc.some_predicate?) 을 static 분석에서 잡아낼 수 있도록 Rubocop custom cop 또는 코드 리뷰 체크리스트에 추가 — 최소한 belongs_to ... optional: true 인 association 에 대한 predicate 호출은 safe navigation 필수화.

Monitoring#

새 알림/메트릭:

  • CreateSitetrackEditingEntitiesWorker 에서의 error 발생 카운트 추이 (재발 여부 확인).
text
service:cupixworks-worker status:error @class:CreateSitetrackEditingEntitiesWorker
  • orphan editing_id 진단 로그 (개선안 반영 후):
text
service:cupixworks-worker status:warn "editing_id present but editing nil"
  • 배포 검증용 — 수정 후 동일 문구 재발 여부:
text
service:cupixworks-worker "undefined method `untrashed?' for nil"

Risk Assessment#

  • Risk level: low (단일 sitetrack 1회, 파이프라인 전체 영향 아님)
  • 예상 복잡도: trivial (1글자 수정 — .&.)
  • Regression risk: 매우 낮음. 변경 후 semantics 는 line 42 와 정합되고, 기존 로직(“editing_id 는 있으나 editing 은 nil”) 을 orphan 케이스로 취급하여 재배정 경로로 흘려보내는 것이 이 함수의 의도임 (line 41 의 코멘트로 확인).