ReviewPermission with id 131231 not found after 6 retries
RCA: ReviewPermission with id 131231 not found after 6 retries
Overview#
What Happened#
SavePartialJsonToFileWorker Sidekiq worker가 방금 생성된 ReviewPermission 레코드(id 131231~131236, 총 6건)를 find_by_id로 조회하지 못해 6회 재시도 후 최종 실패했다. 재시도 지연은 exponential backoff(0.1s → 3.2s, 총 약 6.3s)였고, 그 사이 레코드가 결국 조회 가능해지지 않아 error 로그를 남기며 종료됐다. 이로 인해 해당 ReviewPermission 6건은 CDC(partial JSON) 파이프라인에 전달되지 않아 데이터 웨어하우스 반영이 누락됐다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | RuntimeError (message-only 로그, no stack) |
| exception.message | ReviewPermission with id 131231 not found after 6 retries |
| top_frame | app/workers/save_partial_json_to_file_worker.rb:57 |
| runtime | Ruby / Sidekiq (queue: data_changes, retry: false) |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-worker (Data Warehouse CDC) | 6 | ReviewPermission 6건이 partial JSON 파일로 저장되지 않아 DWH 파이프라인에서 누락 |
Timeline#
- 2026-07-17 03:54:41 KST — Sidekiq worker가
ReviewPermission#131231조회 시작, retry 1/6 실패 (not found, retrying in 0.1 seconds). - 2026-07-17 03:54:41~45 KST — retry 2~6 순차 실패 (delay 0.2s → 3.2s).
- 2026-07-17 03:54:49 KST —
#131231최종 실패, error 로그 (not found after 6 retries). - 2026-07-17 03:54:49~51 KST — 동일 패턴으로
#131232~#131236도 실패, 총 6건. - 2026-07-10 19:18:35~37 KST — 동일 워커에서
ReviewPermission#130447,#130448실패한 이전 사례 확인 (재발 패턴).
Error Log#
ReviewPermission with id 131231 not found after 6 retries
Impact#
- Service:
cupixworks-worker - 발생 횟수: 6
- 최초 발생: 2026-07-17 03:54:49 KST
- 최근 발생: 2026-07-17 03:54:51 KST
Root Cause Summary#
ReviewPermission 모델은 DataWareHouse::PartialJson concern을 통해 after_commit :save_partial_json_to_file_as_created, on: :create 콜백을 등록하며, 이 콜백이 SavePartialJsonToFileWorker.perform_async로 Sidekiq 작업을 enqueue한다. 워커는 실행 시점에 ReviewPermission.find_by_id(id)로 원본 레코드를 조회하는데, 6회(총 약 6.3s) 재시도해도 nil이 반환됐다. tesla는 단일 MySQL 인스턴스(config/database.yml 참조 — read_replica/makara/connects_to 설정 없음)이므로 read replica lag 는 원인이 아니다. 남은 유력 가설은 (1) ReviewPermission가 짧은 수명 뒤 삭제되어(예: permissions.destroy_all — transferable/facility.rb:99,206, cyclable/user.rb:39) 워커 실행 전에 사라졌거나, (2) 매우 긴 outer transaction 안에서 생성돼 after_commit이 enqueue한 job이 실제 커밋 전에 실행됐거나 커밋 자체가 6.3s 이상 지연됐거나, (3) 트랜잭션이 롤백되어 레코드가 존재하지 않는 상태. after_commit은 outermost 커밋 이후에만 fire되므로 (2)는 커밋이 여전히 지연됐을 가능성을 시사한다. 로그상 6건이 순차 ID(131231~131236)로 동시(초 단위 내) 발생한 점, 이전 7/10일에도 순차 ID(130447/130448)에서 동일 실패가 있었던 점은 "대량 생성 후 즉시 삭제/롤백" 시나리오(가설 1/3)와 정합적이다.
Technical Analysis#
Code Path#
Entry point: app/models/concerns/data_ware_house/partial_json.rb:6-8 (after_commit 등록)
included do
after_commit :save_partial_json_to_file_as_created, on: :create
after_commit :save_partial_json_to_file_as_updated, on: :update, if: :not_new_record?
after_commit :save_partial_json_to_file_as_destroyed, on: :destroy
end
Enqueue: app/models/concerns/data_ware_house/partial_json.rb:116-124
def save_partial_json_to_file_in_worker(operation: '(updated)', all_data: false, changes: nil, timestamp: nil)
if !all_data && changes.blank? && operation == '(updated)'
Cupix::Logger.debug('No changes detected, skipping partial JSON generation', ...)
return nil
end
SavePartialJsonToFileWorker.perform_async(self.class.name, self.id, { operation: operation, all_data: all_data, changes: changes, timestamp: timestamp }.to_json)
end
Model: ReviewPermission includes DataWareHouse::ReviewPermission → DataWareHouse → DataWareHouse::PartialJson.
class ReviewPermission < ApplicationRecord
include ::Searchable::Permission
include ::PermissionsHelper::ReviewPermission
include ::DataWareHouse::ReviewPermission
belongs_to :review, optional: true
belongs_to :facility_permission, optional: true
belongs_to :accessor, polymorphic: true
before_create :create_facility_permission
after_save :check_facility_permission, if: :saved_change_to_permission?
Failure point: app/workers/save_partial_json_to_file_worker.rb:41-60
model = nil
$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
기대 동작: after_commit(on: :create)은 outermost 트랜잭션 커밋 이후에만 실행되므로 워커가 job을 pick up 할 때에는 레코드가 primary DB에 존재해야 한다. 최대 6.3s 지연이면 임시적인 지연/락 해제 시간으로 충분하다.
실제 동작: 6.3s 뒤에도 find_by_id가 nil 반환. tesla는 단일 MySQL이므로 replication lag 배제. 남는 시나리오는 (a) 레코드가 삭제됨(destroy_all in transferable/facility.rb:99 등), (b) 트랜잭션이 rollback되어 실제 레코드가 존재하지 않음, (c) 매우 긴 outer transaction 이 6.3s 이후에도 미커밋 상태. Sidekiq은 sidekiq_options queue: :data_changes, retry: false로 실패 시 재시도가 없으므로 이 6건은 CDC에서 영구 손실된다.
관련 파괴 경로:
if current_class.respond_to?(:permissions) && transfer_model.team_changed?
current_class.where(facility_id: id) do |model|
model.permissions.destroy_all
end
end
# ...
def _remove_permissions
permissions.destroy_all
end
model.review_permissions.destroy if model.review_permissions.exists?
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-worker "ReviewPermission" "not found after"
service:cupixworks-worker "131231"
Retry 시퀀스 (단일 record 131231):
2026-07-17 03:54:41 warn Retry 1 - ReviewPermission with id 131231 not found, retrying in 0.1 seconds
2026-07-17 03:54:41 warn Retry 2 - ReviewPermission with id 131231 not found, retrying in 0.2 seconds
2026-07-17 03:54:41 warn Retry 3 - ReviewPermission with id 131231 not found, retrying in 0.4 seconds
2026-07-17 03:54:43 warn Retry 4 - ReviewPermission with id 131231 not found, retrying in 0.8 seconds
2026-07-17 03:54:43 warn Retry 5 - ReviewPermission with id 131231 not found, retrying in 1.6 seconds
2026-07-17 03:54:45 warn Retry 6 - ReviewPermission with id 131231 not found, retrying in 3.2 seconds
2026-07-17 03:54:49 error ReviewPermission with id 131231 not found after 6 retries
동시 발생한 순차 ID들 (모두 class:SavePartialJsonToFileWorker function:perform):
2026-07-17 03:54:49 ReviewPermission with id 131231 not found after 6 retries
2026-07-17 03:54:49 ReviewPermission with id 131232 not found after 6 retries
2026-07-17 03:54:49 ReviewPermission with id 131233 not found after 6 retries
2026-07-17 03:54:49 ReviewPermission with id 131234 not found after 6 retries
2026-07-17 03:54:49 ReviewPermission with id 131235 not found after 6 retries
2026-07-17 03:54:51 ReviewPermission with id 131236 not found after 6 retries
이전 재발 사례 (7/10):
2026-07-10 10:18:35 ReviewPermission with id 130447 not found after 6 retries
2026-07-10 10:18:37 ReviewPermission with id 130448 not found after 6 retries
주변 시간대(±5분)에서 tesla DB 관련 error(예: ActiveRecord::Deadlocked, LockWaitTimeout)는 발견되지 않음. 검색 쿼리:
service:cupixworks-worker status:error @environment:production (time window 2026-07-16 17:54:00 ~ 19:55:00 UTC)
결과: AwsTask#batch_pull! 관련 IAM 및 taskId 길이 에러만 다수 존재. ReviewPermission 이슈와 무관.
DB 설정 확인 — 단일 MySQL, replica 미사용:
development: &development
<<: *default
reconnect: true
adapter: mysql2
pool: 50
host: <%= ENV['RAILS_DB_HOST'] || '127.0.0.1' %>
...
config 전체에서 replica, reading_role, writing_role, connects_to, makara 매칭 없음.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Read replica lag — 워커가 replica에서 조회해 write 반영 전 nil | 유사한 "after_commit → worker not found" 패턴의 전형적 원인 | tesla는 config/database.yml상 단일 MySQL. replica/makara/connects_to 설정 없음 |
Rejected |
| H2 | 레코드가 생성 직후 삭제됨 (destroy_all 경로) | transferable/facility.rb:99,206, cyclable/user.rb:39에서 permissions.destroy_all 사용. 순차 ID 6건 동시 실패는 bulk 작업 시나리오와 정합 |
삭제 시점을 확정할 audit/binlog 로그 미확보 | Inconclusive |
| H3 | Outer transaction이 rollback되어 after_commit이 fire되지 않았어야 하지만 어떤 이유로 job이 남음, 또는 트랜잭션이 6.3s 이상 지속 | Sidekiq redis enqueue와 DB commit이 다른 시점에 처리되면 이론상 가능. 순차 ID가 한 트랜잭션(bulk create)이었을 가능성 시사 | Rails after_commit은 outermost commit 후에만 fire됨이 표준. Rollback 시 fire되지 않음 |
Inconclusive |
| H4 | DB deadlock/LockWaitTimeout 로 인한 커밋 지연 | — | 동일 시간대 tesla worker error 로그에 lock 관련 에러 미검출 | Rejected |
| H5 | after_commit 콜백을 우회하는 insert_all/upsert_all로 워커가 잘못된 ID로 enqueue됨 |
bulkable_repository.rb, bulkable_factory.rb에서 BulkSavePartialJsonToFileWorker 사용 패턴 존재 |
실패 워커는 SavePartialJsonToFileWorker(단수형). 콜백 경로에서만 호출됨 (partial_json.rb:123). insert_all/upsert_all이 이 콜백을 발화시키지 않으므로 이 경로로는 enqueue 되지 않음 |
Rejected |
Confirmed root cause: H2 또는 H3 중 하나 — "생성된 ReviewPermission 6건이 워커 실행 시점(약 6.3s 후) 이전에 삭제되었거나 트랜잭션이 rollback/미커밋 상태". 정확한 판별에는 MySQL binlog 또는 애플리케이션 트랜잭션 로그가 필요 (uncertain -- needs verification).
Fix Recommendation#
즉시 조치 (Critical)#
app/workers/save_partial_json_to_file_worker.rb:57— 최종 실패 로그에 판별용 컨텍스트를 추가한다: 조회 시점에 삭제 여부 확인(model_class.unscoped.find_by(id: id, deleted_at: !nil)또는paranoid스코프 검사), 그리고options_json원본, worker jid,Time.now - created_at같은 진단 필드를 로그에 남겨 다음 재발 시 삭제/롤백/지연을 구분할 수 있게 한다. 파괴적 코드 변경 없이 진단 향상만 수행.errors/의resolution필드는 파이프라인이 관리하므로 이 RCA에서 직접 수정하지 않는다.
단기 개선 (1주 이내)#
app/workers/save_partial_json_to_file_worker.rb:41-60— 재시도 반복 대신 Sidekiq 표준 재시도(sidekiq_options retry: N)와 exponential backoff로 전환하고,find_by_idnil 시 예외를 raise하여 Sidekiq이 job을 재시도 큐로 옮기도록 한다. 현재retry: false+ 인라인 sleep 방식은 프로세스 스레드를 6.3s 동안 점유하고 최종 실패 시 job이 영구 손실된다.ReviewPermission생성 코드를 감사(audit):create_facility_permissionbefore_create 콜백은 별도 트랜잭션 저장(FacilityPermission.create!)을 수행하므로, 부모 트랜잭션 지연이 커질 수 있다. bulk 생성 경로가 있다면BulkSavePartialJsonToFileWorker처럼 배치화 검토.
장기 개선 (재발 방지)#
- CDC(partial JSON) 파이프라인의 손실 감지: 워커 최종 실패 이벤트를 별도 dead-letter 큐 또는 DWH reconciliation 잡으로 흘려보내 누락된 레코드를 주기적으로 backfill.
after_commit + Sidekiq패턴 대신 outbox pattern(같은 트랜잭션 내 outbox 테이블 insert → 별도 relay가 CDC 발행)로 전환 검토. 트랜잭션 커밋 시점과 레코드 가시성이 일치.
Monitoring#
- Datadog에서 워커 최종 실패 카운트 알림 (권장: 15분 window에 1건 이상):
service:cupixworks-worker "not found after 6 retries" @environment:production
- 특정 모델별 실패 비율 추적:
service:cupixworks-worker "ReviewPermission" "not found after"
- 관련 재시도 warn 로그 추이 (선행 지표):
service:cupixworks-worker @class:SavePartialJsonToFileWorker "Retry"
Risk Assessment#
- Risk level: medium — 데이터 손실(DWH 누락)이 발생하지만 사용자 대면 기능에는 즉각적 영향 없음. 재발 이력(7/10, 7/17) 존재.
- 예상 복잡도: standard — 워커 로직 개선(재시도/재큐잉)과 진단 로깅 강화는 격리된 파일 변경.