Editing split failed: Couldn't find Editing with 'id'=5549
RCA: Editing split failed — Couldn't find Editing with 'id'=5549
Overview#
What Happened#
2026-07-05 18:44 KST, cupixworks-worker(Sidekiq) 의 EditingSplitWorker#perform가 editing_id=5549로 실행되었으나 해당 Editing 레코드는 이미 2일 전(2026-07-03 19:43 KST)에 FlushCycleStateChildrenWorker에 의해 삭제(cycle_state=deleted)된 상태였다. 결과적으로 Editing.find(5549)가 ActiveRecord::RecordNotFound를 발생시켜 워커가 실패했으며, 동일 시각 LogEditingStateWorker와 SavePartialJsonToFileWorker도 같은 원인(row missing)으로 각각 “editing not found”/“not found after 6 retries” 로그를 남겼다. 발생 횟수는 1건, 리전은 ap-southeast-1.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | ActiveRecord::RecordNotFound |
| exception.message | Couldn't find Editing with 'id'=5549 |
| top_frame | app/services/cupix/editing_split_service.rb:13 |
| env | production / ap-southeast-1 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-worker (SiteInsights / Editing split) | 1 | 단일 Editing(5549)에 대한 split 잡 실패. 이미 삭제된 editing이므로 실질적인 유저 영향은 없으나 error 로그 노이즈로 인해 알림 신뢰도 저하 |
Timeline#
- 2026-06-27 17:52 KST — Editing 5549에 대한
generate_partial_json이 정상 실행되며 편집이 활발히 사용되던 시점. - 2026-07-03 19:43 KST —
FlushCycleStateChildrenWorker가 Editing 5549의 children을 deleted로 flush ("end flushing children to deleted for Editing ID: 5549"). Editing row가 실질적으로 사라진 시점. - 2026-07-05 18:44:29 KST — 오래 지연된 상태 전이 이벤트("state has transitioned from waiting to ready on Editing 5549")가 실행됨.
- 2026-07-05 18:44:29 KST —
LogEditingStateWorkerinfo "editing not found: 5549" (deleted row에 대한 find_by=nil). - 2026-07-05 18:44:29 KST —
SavePartialJsonToFileWorker가 retry 1 시작,EditingSplitWorker가 동일 시각에Editing.find실패로 에러 발생. - 2026-07-05 18:44:35 KST —
SavePartialJsonToFileWorker가 6회 재시도 후 "Editing with id 5549 not found after 6 retries" 최종 실패.
Error Log#
Editing split failed: Couldn't find Editing with 'id'=5549
Impact#
- Service:
cupixworks-worker - 발생 횟수: 1
- 최초 발생: 2026-07-05 18:44 KST
- 최근 발생: 2026-07-05 18:44 KST
Root Cause Summary#
EditingSplitWorker#perform은 ::Cupix::EditingSplitService.new(editing_id: editing_id) 를 호출하고, 서비스 생성자는 ::Editing.find(editing_id) 로 레코드를 즉시 로드한다. 대상 Editing(5549)은 2026-07-03에 이미 삭제된 상태였으므로 find가 ActiveRecord::RecordNotFound를 raise 했고, 서비스의 cycle_state_created? 가드(라인 47)나 splittable? 가드가 실행되기 전에 워커가 실패했다. 워커 관점에서는 “이미 삭제된 editing에 대해 뒤늦게 큐잉/재시도된 잡”이라는 예상 가능한 시나리오이지만, 서비스 생성자가 find 를 사용해 이를 error로 격상시키고 있다. 동시 시각의 LogEditingStateWorker(find_by(id: ...) 사용, "editing not found: 5549" info 로그)와 SavePartialJsonToFileWorker(find_by_id로 6회 재시도)가 같은 상황을 조용히 처리한 것과 대비된다.
Revision 1 note: TSLA-13467 (
5b569d1bf, 2026-07-03) 에 의해develop상에는 이미 (1) enqueue-site 를after_all_transactions_commit로 감싸 assign 트랜잭션 커밋 후에만 워커를 큐잉하고, (2) 워커에서Editing.exists?guard +RecordNotFoundrescue 로 흡수하는 fix 가 병합되어 있다. 커밋 메시지에 따르면 실제 root cause 는 “서비스 생성자의 find 사용” 보다도 더 근원적인 “open transaction 내부에서의 조기 enqueue” 였다 — 워커가 커밋 이전(또는 롤백 이후)에 실행될 수 있었다. 본 인시던트가 발생한 production(master)에는 이 fix 가 아직 배포되지 않은 상태이므로 현상 자체는 유효하다.
Technical Analysis#
Code Path#
Entry: app/workers/editing_split_worker.rb:5
class EditingSplitWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: 2
def perform(editing_id)
Cupix::Logger.info('Starting editing split', class: self.class.name, function: __method__, editing_id: editing_id)
::Cupix::EditingSplitService.new(editing_id: editing_id).split!
Cupix::Logger.info('Editing split finished', class: self.class.name, function: __method__, editing_id: editing_id)
rescue StandardError => e
Cupix::Logger.error("Editing split failed: #{e.message}", class: self.class.name, function: __method__, editing_id: editing_id)
raise
end
end
Failure point: app/services/cupix/editing_split_service.rb:13 — ::Editing.find(editing_id) 에서 ActiveRecord::RecordNotFound 발생.
def initialize(editing_id:, current_user: nil)
@editing = ::Editing.find(editing_id)
@current_user = current_user || @editing.editor || @editing.user
end
Service의 split! 는 삭제된 editing 을 대비한 가드가 있으나, 이 가드는 initialize 다음 단계에서 실행되므로 find 예외가 먼저 발생해 도달할 수 없다.
# Bail out if the editing is already trashed/deleted (e.g. Sidekiq retry
# on a job whose previous run already completed Phase 4 cleanup).
# Re-entering would attempt an illegal :trashing transition from :deleted.
unless editing.cycle_state_created?
Cupix::Logger.info('Split skipped (editing not in created cycle_state)',
class: self.class.name,
function: __method__,
editing_id: editing.id,
cycle_state: editing.cycle_state)
return [editing]
end
동일 시나리오를 안전하게 처리하는 다른 워커들과 비교:
def perform(editing_id, transition_from, transition_to, editor_id = nil)
editing = ::Editing.find_by(id: editing_id)
if editing.blank?
Cupix::Logger.info("editing not found: #{editing_id}", class: self.class.name, function: __method__)
return
end
$MAX_RETRIES.times do |retries|
model = model_class.find_by_id(id)
if model.nil?
delay = 0.1 * (2**retries)
Cupix::Logger.warn("Retry #{retries + 1} - #{class_name} with id #{id} not found, retrying in #{delay} seconds", class: self.class, function: 'perform')
sleep(delay)
next
else
break
end
end
if model.nil?
Cupix::Logger.error("#{class_name} with id #{id} not found after #{$MAX_RETRIES} retries", class: self.class, function: 'perform')
return false
end
기대 동작: 삭제된 editing에 대한 지연/재시도 잡은 서비스의 cycle_state_created? 가드가 [editing] 을 반환해 조용히 종료해야 한다.
실제 동작: initialize 단계의 find 예외로 인해 error 로그 + Sidekiq retry 2회 (retry: 2) 가 발생한다.
Entry to worker: EditingSplitWorker.perform_async(editing.id) 는 두 곳에서 큐잉된다.
if SQA_GEO_GROUPING_ENABLED && editing.editing_type == 'siteinsights' &&
(counts_by_editing[editing.id] || 0) > ::Cupix::EditingSplitService::MAX_ELEMENTS_PER_EDITING
::EditingSplitWorker.perform_async(editing.id)
end
EditingSplitWorker.perform_async(editing_id)
이번 인시던트의 정확한 큐잉 시각은 로그에서 확인되지 않는다 — uncertain -- needs verification. 그러나 동일 시각의 LogEditingStateWorker "editing not found" 및 state has transitioned from waiting to ready on Editing 5549 로그가 함께 나온 것으로 보아, 삭제된 Editing에 대해 상태 전이 이벤트가 함께 발화되며 다수의 후속 워커가 동시에 큐잉된 정황이 뚜렷하다.
Log Evidence#
Datadog query (재현용):
service:cupixworks-worker "Editing" "5549"
핵심 로그(시간 역순, 원문):
2026-07-05 18:44:35 error SavePartialJsonToFileWorker#perform
"Editing with id 5549 not found after 6 retries"
2026-07-05 18:44:33 warn SavePartialJsonToFileWorker#perform
"Retry 6 - Editing with id 5549 not found, retrying in 3.2 seconds"
2026-07-05 18:44:29 error EditingSplitWorker#perform
"Editing split failed: Couldn't find Editing with 'id'=5549"
2026-07-05 18:44:29 warn SavePartialJsonToFileWorker#perform
"Retry 1 - Editing with id 5549 not found, retrying in 0.1 seconds"
2026-07-05 18:44:29 info LogEditingStateWorker#perform
"editing not found: 5549"
2026-07-05 18:44:29 info (state machine)
"state has transitioned from waiting to ready on Editing 5549"
Editing 5549 삭제/flush 로그(약 2일 전):
2026-07-03 10:43:01 UTC (2026-07-03 19:43 KST) info FlushCycleStateChildrenWorker#perform
"end flushing children to deleted for Editing ID: 5549"
2026-07-03 10:43:01 UTC warn Editing#flush_child_cycle_state
"ES cycle_state mismatch: 1 editing_entities expected deleted for Editing ID(s): 5549"
2026-07-03 10:43:01 UTC info Editing#flush_child_cycle_state
"Begin flushing editing_entities with deleted cycle_state for Editing ID(s): 5549"
정상 사용 흔적(참고):
2026-06-27 08:52:52 / 08:57:11 / 09:44:29 UTC info generate_partial_json
"Editing processed record ID: 5549"
동일 패턴이 최근 24시간 다른 editing에서도 반복됨(rare, but recurring):
2026-07-05 16:21:56 error EditingSplitWorker#perform
"Editing split failed: Couldn't find Editing with 'id'=1211500"
2026-07-05 15:00:27 error EditingSplitWorker#perform
"Editing split failed: Couldn't find Editing with 'id'=1211428"
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Editing 5549가 이미 삭제/flush된 후 지연된 Sidekiq 잡이 실행되어 EditingSplitService#initialize의 Editing.find가 RecordNotFound를 raise함 |
2026-07-03 19:43 KST FlushCycleStateChildrenWorker "end flushing children to deleted for Editing ID: 5549" 로그, 2026-07-05 18:44 KST 동일 시각 LogEditingStateWorker "editing not found: 5549" 및 SavePartialJsonToFileWorker 6회 재시도 로그, 코드 editing_split_service.rb:13 ::Editing.find(editing_id) |
— | Confirmed |
| H2 | DB 일시 장애/replication lag로 인해 순간적으로 row가 안 보였다 | SavePartialJsonToFileWorker가 6회(≈6.3초) 재시도해도 못 찾음 (6번 재시도 후 최종 실패) |
삭제 로그가 2일 전에 명확히 존재하며, 이후 어떤 재활성 로그도 없음. Replication lag 시나리오와 불일치 | Rejected |
| H3 | 워커에 잘못된 editing_id가 전달됨 (오탈자/타입 캐스트 이슈) |
— | state has transitioned from waiting to ready on Editing 5549 로그가 동일한 5549를 참조하며, SavePartialJsonToFileWorker도 동일 id로 6회 조회. id 전달은 정확 |
Rejected |
| H4 | 코드 상 EditingSplitWorker 진입점의 로직 오류 (오호출) |
— | 코드 경로 editing_entity.rb:455는 counts_by_editing[editing.id] > MAX_ELEMENTS_PER_EDITING인 시점에만 큐잉하며, 이는 삭제 시점 이전 시점에 큐잉되어 워커 실행 시점에는 이미 삭제 완료된 상태로 간주 가능. 즉 큐잉 자체는 정상 |
Rejected |
Fix Recommendation#
Status update (Revision 1): 본 결함은 이미
origin/develop에 커밋5b569d1bf([TSLA-13467] fix: defer split/save enqueues until after assign transaction commits, Adam Lee, 2026-07-03 21:22 KST) 로 수정되어 있다. 커밋은 (1)EditingSplitWorker에Editing.exists?guard +ActiveRecord::RecordNotFoundrescue 를 추가하고, (2)app/models/concerns/finalization/editing_entity.rb의 enqueue 지점을ActiveRecord.after_all_transactions_commit로 감싸 assign 트랜잭션 커밋 이후에만 워커가 큐잉되도록 한다. 다만 이 커밋은origin/master(production)에는 아직 병합되지 않았다 — 본 인시던트(2026-07-05 09:44 UTC, production)는 아직 fix 를 받지 못한 master 코드에서 발생했다. 따라서 아래 권장은 "신규 fix 개발"이 아니라 develop 의 TSLA-13467 을 production 으로 배포/백포트하는 조치" 로 대체된다.
즉시 조치 (Critical)#
develop에 이미 존재하는 커밋5b569d1bf(TSLA-13467) 을 production 배포 라인(master)으로 병합/백포트한다. 관련 hotfix 브랜치(origin/hotfix/TSLA-13467,origin/hotfix/TSLA-13467-on-qa,origin/deploy/qa) 이미 존재하므로 새 브랜치 생성 없이 병합 절차만 필요하다. 커밋이 커버하는 두 축:- Enqueue-site 수정 —
app/models/concerns/finalization/editing_entity.rb:EditingSplitWorker.perform_async(editing.id)를ActiveRecord.after_all_transactions_commit블록으로 감싸 assign 트랜잭션 커밋 이후에만 큐잉. 롤백 시에는 큐잉 자체가 발생하지 않는다. - Worker-level defense —
app/workers/editing_split_worker.rb:unless ::Editing.exists?(editing_id) → info 로그 후 return, 및rescue ActiveRecord::RecordNotFound → info 로그 후 no-op(retry 유발하지 않음). 이번 인시던트처럼 큐잉 후 2일이 지나 대상이 legitimate 하게 trashed/purged 된 경우도 조용히 종료된다.
- Enqueue-site 수정 —
- master 병합 전까지의 임시 대응이 필요하다면 Sidekiq 큐에서 stale
EditingSplitWorker잡을 스크립트로 정리(대상 editing 존재 여부 확인 후 skip)하는 워크어라운드가 가능하다. 새로운 코드 fix 는 불필요.
단기 개선 (배포 이후)#
- 배포 후
service:cupixworks-worker status:error "Editing split failed" "Couldn't find Editing"쿼리로 fix 유효성 검증(0 hits 예상). SavePartialJsonToFileWorker 의not found after 6 retries카운트(TSLA-13467 커밋 메시지 기준 7일간 1.9k,editing not foundinfo 로그 24k) 도 함께 감소해야 한다. - develop 의
EditingSplitService#initialize는 여전히::Editing.find(editing_id)를 사용한다(app/services/cupix/editing_split_service.rb:14, develop). 서비스 계층은 그대로 두고 worker 진입점에서 흡수하는 것이 TSLA-13467 의 설계 선택 — 다른 호출자(예:create_sitetrack_editing_entities_worker.rb:84) 도 별도의 guard 를 두거나 동일 패턴을 따르는지 재검토 대상.
장기 개선 (재발 방지)#
- 상태 전이 훅에서 async 워커를 enqueue 하는 다른 지점(
statable/editing.rb:102등) 에 대해서도 open transaction 내 큐잉 여부를 audit 하고 필요시after_all_transactions_commit을 적용. - Sidekiq middleware 수준에서 "editing_id" 등 도메인 엔티티 id를 인자로 받는 워커에 대해 실행 시점에 존재 여부를 사전 검증하고, 없으면 info 로그 후 종료하는 공통 헬퍼(예:
SafeEntityLookup) 도입 검토 — TSLA-13467 이 워커별로 개별 처리한 패턴을 공통화.
Monitoring#
- 삭제 후 실행된 stale job 발생률 추적을 위한 error rate 쿼리:
service:cupixworks-worker status:error "Editing split failed" "Couldn't find Editing"
- 동일 원인의 partial-json 재시도 소진 카운트:
service:cupixworks-worker status:error "not found after 6 retries" @class:SavePartialJsonToFileWorker
- 상태 전이 후 대상이 사라진 케이스(선행 지표):
service:cupixworks-worker status:info "editing not found" @class:LogEditingStateWorker
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (배포 액션만 필요, 신규 코드 작성 불필요)
Revision History#
Revision 1#
Feedback: "이거 develop branch 에서 해결된거 아닌지 확인"
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
develop 브랜치에서 이 이슈가 이미 해결되었는지 확인 |
수용 | origin/develop 상에 커밋 5b569d1bf [TSLA-13467] fix: defer split/save enqueues until after assign transaction commits (Adam Lee, 2026-07-03 21:22 KST, 병합됨) 존재. 변경 파일: app/workers/editing_split_worker.rb (+15), app/models/concerns/finalization/editing_entity.rb (+24/-4), 그리고 spec 2건. worker 변경분에는 unless ::Editing.exists?(editing_id) → info 로그 후 return guard 와 rescue ActiveRecord::RecordNotFound → info 로그 후 no-op (retry 방지) 이 추가되어 이번 인시던트의 실패 경로를 정확히 차단한다. enqueue-site 는 after_all_transactions_commit 로 감싸 assign 트랜잭션 커밋 이후에만 큐잉되도록 변경됨. git merge-base --is-ancestor 5b569d1bf origin/master 는 "NOT ON MASTER" 반환 — 즉, fix 는 develop(및 origin/deploy/qa, origin/hotfix/TSLA-13467, origin/hotfix/TSLA-13467-on-qa)에는 있으나 production 라인 master 에는 아직 없다. 본 인시던트 발생 시각(2026-07-05 09:44 UTC, production, ap-southeast-1)은 이 fix 를 받지 못한 상태의 master 배포와 정합적이다. |
변경 사항:
Root Cause Summary에 Revision 1 note 를 추가해 TSLA-13467 로 인해 근원 원인이 “서비스의find사용” 이 아니라 “assign 트랜잭션 내부에서의 조기 enqueue” 임을 명시.Fix Recommendation을 전면 재작성 — 신규 코드 fix 개발이 아니라 develop 의 TSLA-13467 을 master 로 병합/배포하는 것이 즉시 조치. 기존에 제안했던find_by(id: ...)대체안은 develop 의 실제 설계 선택(서비스 계층은 유지, 워커에서 흡수)과 다르므로 폐기.Risk Assessment의 예상 복잡도에 "배포 액션만 필요, 신규 코드 작성 불필요" 추가.
추가 조사 내용:
/home/ec2-user/repos/tesla에서origin/develop(6aadce44d) 와origin/master(a1160ff56) 를 fetch.git show origin/develop:app/workers/editing_split_worker.rb—exists?guard +RecordNotFoundrescue 확인.git show origin/develop:app/services/cupix/editing_split_service.rb—initialize는 여전히::Editing.find(editing_id)사용 (line 14). 서비스 계층은 손대지 않는 설계.git log origin/master..origin/develop --grep="13465|13467|RecordNotFound.*Editing"— 관련 티켓 커밋 목록 확보:5b569d1bf(develop),9b42d3efd,f50e99969,87e3429f6,2b298a8aa,16c3fbcbc,cae8c2cf6, PR88642/88656, hotfix branches.git branch -r --contains 5b569d1bf—origin/develop,origin/feature/TSLA-13155,origin/feature/TSLA-13481-on-develop, 기타 develop 파생 브랜치 확인. master 는 미포함.git branch -r --contains 9b42d3efd— hotfix 및origin/deploy/qa만 포함 (별도 커밋 라인, develop 에는5b569d1bf로 통합되어 들어감).- 최종 확인:
git merge-base --is-ancestor 5b569d1bf origin/master && echo ON MASTER || echo NOT ON MASTER→NOT ON MASTER.