ES /docs

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#performediting_id=5549로 실행되었으나 해당 Editing 레코드는 이미 2일 전(2026-07-03 19:43 KST)에 FlushCycleStateChildrenWorker에 의해 삭제(cycle_state=deleted)된 상태였다. 결과적으로 Editing.find(5549)ActiveRecord::RecordNotFound를 발생시켜 워커가 실패했으며, 동일 시각 LogEditingStateWorkerSavePartialJsonToFileWorker도 같은 원인(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#

  1. 2026-06-27 17:52 KST — Editing 5549에 대한 generate_partial_json이 정상 실행되며 편집이 활발히 사용되던 시점.
  2. 2026-07-03 19:43 KSTFlushCycleStateChildrenWorker가 Editing 5549의 children을 deleted로 flush ("end flushing children to deleted for Editing ID: 5549"). Editing row가 실질적으로 사라진 시점.
  3. 2026-07-05 18:44:29 KST — 오래 지연된 상태 전이 이벤트("state has transitioned from waiting to ready on Editing 5549")가 실행됨.
  4. 2026-07-05 18:44:29 KSTLogEditingStateWorker info "editing not found: 5549" (deleted row에 대한 find_by=nil).
  5. 2026-07-05 18:44:29 KSTSavePartialJsonToFileWorker가 retry 1 시작, EditingSplitWorker가 동일 시각에 Editing.find 실패로 에러 발생.
  6. 2026-07-05 18:44:35 KSTSavePartialJsonToFileWorker가 6회 재시도 후 "Editing with id 5549 not found after 6 retries" 최종 실패.

Error Log#

Datadog Logs

text
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에 이미 삭제된 상태였으므로 findActiveRecord::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 + RecordNotFound rescue 로 흡수하는 fix 가 병합되어 있다. 커밋 메시지에 따르면 실제 root cause 는 “서비스 생성자의 find 사용” 보다도 더 근원적인 “open transaction 내부에서의 조기 enqueue” 였다 — 워커가 커밋 이전(또는 롤백 이후)에 실행될 수 있었다. 본 인시던트가 발생한 production(master)에는 이 fix 가 아직 배포되지 않은 상태이므로 현상 자체는 유효하다.

Technical Analysis#

Code Path#

Entry: app/workers/editing_split_worker.rb:5

app/workers/editing_split_worker.rb:1-15ruby
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 발생.

app/services/cupix/editing_split_service.rb:12-15ruby
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 예외가 먼저 발생해 도달할 수 없다.

app/services/cupix/editing_split_service.rb:44-54ruby
# 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

동일 시나리오를 안전하게 처리하는 다른 워커들과 비교:

app/workers/log_editing_state_worker.rb:5-10ruby
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
app/workers/save_partial_json_to_file_worker.rb:41-60ruby
$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) 는 두 곳에서 큐잉된다.

app/models/concerns/finalization/editing_entity.rb:453-456ruby
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
app/workers/create_sitetrack_editing_entities_worker.rb:84ruby
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 (재현용):

text
service:cupixworks-worker "Editing" "5549"

핵심 로그(시간 역순, 원문):

text
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일 전):

text
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"

정상 사용 흔적(참고):

text
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):

text
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#initializeEditing.findRecordNotFound를 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:455counts_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) EditingSplitWorkerEditing.exists? guard + ActiveRecord::RecordNotFound rescue 를 추가하고, (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 된 경우도 조용히 종료된다.
  • 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 found info 로그 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 쿼리:
text
service:cupixworks-worker status:error "Editing split failed" "Couldn't find Editing"
  • 동일 원인의 partial-json 재시도 소진 카운트:
text
service:cupixworks-worker status:error "not found after 6 retries" @class:SavePartialJsonToFileWorker
  • 상태 전이 후 대상이 사라진 케이스(선행 지표):
text
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.rbexists? guard + RecordNotFound rescue 확인.
  • git show origin/develop:app/services/cupix/editing_split_service.rbinitialize 는 여전히 ::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, PR 88642 / 88656, hotfix branches.
  • git branch -r --contains 5b569d1bforigin/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 MASTERNOT ON MASTER.