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#
- 2026-07-16 00:55:40 KST —
CreateSitetrackEditingEntitiesWorker첫 실행 시작 (start creating editing entities on sitetrack_id: 21354). - 2026-07-16 01:00:40 KST — 두 번째 실행 시작 (Sidekiq
retry: 1또는 재큐잉으로 추정, 5분 뒤). - 2026-07-16 01:29:36 KST — 두 번째 실행에서
NoMethodError발생, 워커의rescue StandardError블록이 에러 로그를 기록하고 종료. - 2026-07-16 (이후) — 동일 sitetrack 에 대한 후속 로그 없음 (14일 검색창 내 확인).
Error Log#
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:26—perform(sitetrack_id) - Task 단위 lock 획득 후 각 task 에 대해
create_editing_entity→assign_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:39—self.editing.untrashed?(nil 에서 호출됨)
Worker 의 task 처리 루프:
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 처리:
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:
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 쿼리:
service:cupixworks-worker "21354"
시간 범위 (2026-07-15T15:00Z ~ 2026-07-15T18:00Z) 내 전체 로그 (3건, KST 로 변환):
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 필드 (첫 번째 로그):
{
"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 entitiesinfo 로그가 존재. Sidekiqretry: 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 Kibanaeditingsindex).
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_id 는 NoMethodError 발생 |
에러 메시지가 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 도 동일하게 맞춘다.
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_id와Editing.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 발생 카운트 추이 (재발 여부 확인).
service:cupixworks-worker status:error @class:CreateSitetrackEditingEntitiesWorker
- orphan editing_id 진단 로그 (개선안 반영 후):
service:cupixworks-worker status:warn "editing_id present but editing nil"
- 배포 검증용 — 수정 후 동일 문구 재발 여부:
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 의 코멘트로 확인).