Sidekiq job died after all retries
RCA: Sidekiq job died after all retries — deep_symbolize_keys on ActionController::Parameters
Overview#
What Happened#
2026-06-24 22:50 KST 부터 약 1분 간 cupixworks-worker 에서 Sidekiq job 들이 모든 재시도를 소진하고 dead set 으로 넘어갔다. 모든 죽은 job 은 동일한 NoMethodError: undefined method 'deep_symbolize_keys' for an instance of ActionController::Parameters 를 raise 했고, 스택의 top frame 은 app/models/concerns/world_transformation/pano.rb:44 의 set_world_transformation_from_meta (Pano 모델의 before_save callback) 였다. 5건 발생.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | NoMethodError |
| exception.message | undefined method 'deep_symbolize_keys' for an instance of ActionController::Parameters |
| top_frame | app/models/concerns/world_transformation/pano.rb:44 |
| runtime | Ruby 3.3.0 / Rails (activesupport) 7.2.2 |
| env | production / us-west-2 |
| sidekiq event | SidekiqDeathHandler#death_handler (모든 retry 소진 후 dead set 진입 시 호출) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-worker (Pano 도메인) | 5 | 해당 Pano 를 저장하는 Sidekiq job 이 모든 재시도 후 영구 실패. 저장이 이루어지지 않아 후속 처리(world_transformation 갱신, 이벤트 발행 등) 가 누락될 수 있다. |
영향 범위 추정은 dead set 진입 건수 기준이며, 동일 원인으로 retry 단계에서 실패한 후 결국 성공한 케이스는 포함되어 있지 않다 — uncertain, needs verification.
Timeline#
- 2026-06-24 22:50:48 KST — 첫 dead job 발생 (
SidekiqDeathHandler#death_handler로그). - 2026-06-24 22:51:18 KST — 두 번째 dead job.
- 2026-06-24 22:51:26 KST — 세 번째.
- 2026-06-24 22:51:40 KST — 네 번째.
- 2026-06-24 22:51:48 KST — 다섯 번째이자 (현재까지) 마지막 dead job.
Error Log#
Sidekiq job died after all retries
대표 로그 (Datadog query: service:cupixworks-worker "deep_symbolize_keys", time range 2026-06-24T13:40Z–14:00Z):
{
"timestamp": "2026-06-24 22:51:48 KST",
"status": "error",
"message": "Sidekiq job died after all retries",
"class": "SidekiqDeathHandler",
"function": "death_handler",
"error": {
"msg": "undefined method `deep_symbolize_keys' for an instance of ActionController::Parameters",
"stack": [
"/var/app/current/app/models/concerns/world_transformation/pano.rb:44:in `set_world_transformation_from_meta'",
"/var/app/current/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2/lib/active_support/callbacks.rb:362:in `block in make_lambda'",
"/var/app/current/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2/lib/active_support/callbacks.rb:179:in `block in call'",
"/var/app/current/vendor/bundle/ruby/3.3.0/gems/activesupport-7.2.2/lib/active_support/callbacks.rb:559:in `block in invoke_before'"
]
}
}
Impact#
- Service:
cupixworks-worker - 발생 횟수: 5
- 최초 발생: 2026-06-24 22:50:48 KST
- 최근 발생: 2026-06-24 22:51:48 KST
Root Cause Summary#
Rails 7.2 의 ActionController::Parameters 는 Hash 가 아니며 deep_symbolize_keys 를 응답하지 않는다. 그런데 Pano 모델의 before_save :set_world_transformation_from_meta callback 은 in-memory meta 속성에 대해 meta.deep_symbolize_keys 를 직접 호출한다 (app/models/concerns/world_transformation/pano.rb:44). Metable concern 의 serialize :meta, coder: Cupix::Util::FlexibleHash.new 는 DB I/O 시점에만 ActionController::Parameters 를 Hash 로 변환하기 때문에, 아직 dump 되지 않은 — 즉, 컨트롤러에서 params 로 직접 대입되어 in-memory 상태인 — meta 가 callback 단계에서 그대로 ActionController::Parameters 인스턴스로 노출된다. 그 결과 NoMethodError 가 발생하고, 모든 Sidekiq retry 가 동일 입력을 반복 처리하다 dead set 으로 이동했다.
Technical Analysis#
Code Path#
- Controller entry —
Api::V1::PanosController#create/#update는 rawparams(=ActionController::Parameters) 를 그대로 factory/repository 에 전달한다.
def create
@model = factory_instance.create!(params)
super
end
def update
@model = repository_instance.update(params)
super
end
- Meta serialization 등록 —
Metable은meta컬럼에FlexibleHashcoder 를 사용한다. coder 의load/dump는 DB 직렬화 경계에서만 동작한다.
module Metable
extend ActiveSupport::Concern
module ClassMethods
end
included do
serialize :sys, coder: Cupix::Util::FlexibleHash.new
serialize :meta, coder: Cupix::Util::FlexibleHash.new
end
end
def load(value)
case value
when String
YAML.safe_load(value, permitted_classes: $ALLOWED_PSYCH_CLASSES, aliases: true)
when ActionController::Parameters
value.to_h
when Hash, ActiveSupport::HashWithIndifferentAccess
value
else
{}
end
end
def dump(value)
case value
when ActionController::Parameters
value.permit!.to_h
else
value
end.to_yaml
end
- Failure point — Pano 의
before_savecallback 이 in-memorymeta를 그대로 사용한다. 이 시점에는 아직dump가 수행되지 않았으므로, controller 에서 대입된ActionController::Parameters가 그대로 반환되고, 곧이어deep_symbolize_keys호출이 실패한다.
if meta.blank?
unless skip_log
Cupix::Logger.info('Skip setting world_transformation from meta as meta is blank', ...)
end
return nil
end
symbolized_meta = meta.deep_symbolize_keys
prop_values = (symbolized_meta[:prop] || {}).slice(:wtm, :user_tm, :tm)
skat_values = (symbolized_meta[:skat] || {}).slice(:wtm, :tm)
wtm_candidate = prop_values[:wtm] || prop_values[:user_tm] || prop_values[:tm] || skat_values[:wtm] || skat_values[:tm]
- Pano include 사슬 — Pano 는
Properties::Pano를 거쳐Metable을 포함하고,WorldTransformation::Pano를 직접 include 하므로 두 concern 이 모두 활성화된다.
class Pano < ApplicationRecord
include CaptureEntity
...
include Properties::Pano # → include Metable (serialize :meta, FlexibleHash)
...
include ::WorldTransformation::Pano # → before_save :set_world_transformation_from_meta
기대 동작 vs 실제 동작
- 기대:
meta가 항상Hash또는HashWithIndifferentAccess로 정규화되어 callback 에서deep_symbolize_keys가 안전하게 동작. - 실제: in-memory 단계에서는
meta가ActionController::Parameters인 채로 노출되어NoMethodError발생.ActionController::Parameters는 Rails 7.x 에서Hash를 상속하지 않으며deep_symbolize_keys를 노출하지 않는다.
Log Evidence#
Datadog query:
service:cupixworks-worker "Sidekiq job died after all retries"
시간 범위 2026-06-24T13:40:00Z – 2026-06-24T14:00:00Z 에서 5건 모두 동일한 stack frame, 동일한 메시지로 매칭됨. 14일 전체 범위로 확장해도 동일 원인은 이 시점 5건만 확인됨:
service:cupixworks-worker status:error @class:SidekiqDeathHandler "world_transformation"
5건 전부의 top frame:
/var/app/current/app/models/concerns/world_transformation/pano.rb:44:in `set_world_transformation_from_meta'
다른 시점/다른 클래스의 동일 원인 발생은 14일 검색 범위 내에서 추가로 확인되지 않았다 — 즉, 본 이슈는 본 시간대에 국한된 신규 패턴 또는 본 시간대에만 트리거된 호출 경로일 가능성이 있다. 정확한 호출자(어떤 Sidekiq worker class / wrapped job 인지) 는 Datadog 로그 필드만으로는 확정되지 않았다 — uncertain, needs verification (Sidekiq 의 wrapped class / args payload 확인 필요).
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | meta 속성이 ActionController::Parameters 인 채로 before_save callback 에 도달해 deep_symbolize_keys 호출이 실패한다. |
5건의 동일 stack trace 가 world_transformation/pano.rb:44 의 meta.deep_symbolize_keys 를 가리키고, 에러 메시지가 정확히 for an instance of ActionController::Parameters 를 명시. FlexibleHash coder 는 DB 경계에서만 변환을 수행하므로 in-memory 단계의 meta 는 변환 전 객체일 수 있음. controller (PanosController#create/#update) 는 raw params 를 그대로 factory/repository 로 넘김. |
— | Confirmed |
| H2 | 기존부터 존재하던 만성 결함이고, 트래픽 변동으로 노출됨. | — | git log 기준 world_transformation/pano.rb 는 2025-04-21 이후 변경 없음 → 코드 자체는 오래된 것이 맞다. |
Inconclusive (코드 변경은 없으나, 14일 범위 내에서는 본 시점에만 5건 폭증) |
| H3 | Rails 7.1 → 7.2 업그레이드 등 의존성 변경으로 ActionController::Parameters 가 deep_symbolize_keys 를 더 이상 응답하지 않게 됨. |
Gemfile 은 rails 7.2.2. Rails 7.x 에서 ActionController::Parameters 는 Hash 를 상속하지 않고 deep_symbolize_keys 를 노출하지 않는다 (직접 to_unsafe_h.deep_symbolize_keys / to_h.deep_symbolize_keys 가 필요). |
14일 범위 내에는 본 시점 외 동일 에러가 없어, 업그레이드 시점과 폭증 시점의 직접 상관은 확인 불가. | Inconclusive (가능성 있음, 사실 확인 필요) |
| H4 | Sidekiq job 의 args 로 직렬화된 Parameters 가 deserialize 되며 본 callback 을 트리거. |
Sidekiq 은 args 를 JSON 직렬화하므로 ActionController::Parameters 가 그대로 살아남기 어려움. callback 실패는 Sidekiq 의 args 가 아니라 모델에 in-memory 로 대입된 meta 에서 비롯. |
Sidekiq args 가 원인이라면 controller 미경유 경로에서도 동일 stack 이 나와야 하나 frame 은 before_save 콜백으로 모델 저장 경로만 가리킴. |
Rejected |
| H5 | DB 에 이미 저장된 meta 가 YAML 로 ActionController::Parameters 를 갖고 있어 load 가 그대로 반환. |
FlexibleHash#load 는 ActionController::Parameters 를 만나면 to_h 로 변환한다(line 7-8). 또한 DB 컬럼은 text 이며 YAML 직렬화이므로 저장 후 다시 읽으면 Hash 가 된다. |
— | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/world_transformation/pano.rb:44의meta.deep_symbolize_keys호출이ActionController::Parameters도 안전하게 처리하도록 변경. 방향(어느 한 가지를 선택):- callback 내부에서 normalize:
meta가ActionController::Parameters면meta.to_unsafe_h.deep_symbolize_keys, 아니면 기존 경로. 단,to_unsafe_h사용은 strong params 우회이므로 신중히 결정해야 한다 — 이 경로가 internal write callback 이고 권한 검증이 controller 단에서 끝났음이 보장될 때만 허용. - 또는 호출자 측(Pano factory / repository / controller)에서
meta = params[:meta]시 항상.permit!.to_h(또는 명시적 permit 후.to_h)로 정규화해 모델에 넘기도록 강제. 이 경우 callback 의 호출 측 가정이 유지된다.
- callback 내부에서 normalize:
- 어느 쪽이든
FlexibleHash#load/dump와 정책이 일관되어야 한다(현재 dump 는permit!.to_h사용).
단기 개선 (1주 이내)#
Metable의serialize가 assignment 시점에도Hash정규화를 보장하도록 attribute writer 를 override 하거나, Rails 의TypeAPI 로cast를 직접 구현해 in-memory 단계 노출을 막는다. 그러면before_savecallback 등 모든 in-memory 경로가 자동으로 안전해진다.- 동일 패턴이 다른 모델/콜백에서도 잠재돼 있는지 확인:
deep_symbolize_keys또는symbolize_keys가 in-memory 속성에 직접 호출되는 코드 전수 점검 (Grep으로 1차 스캔 권장).
장기 개선 (재발 방지)#
- Rails 7 에서
ActionController::Parameters가Hash의 메서드를 응답하지 않는다는 점을 인지한 가이드/lint 룰 도입. - 컨트롤러에서 모델로 raw
params를 직접 넘기는 패턴(factory.create!(params),repository.update(params)) 대신, 명시적permit후to_h를 거치도록 컨벤션 통일.
Monitoring#
추가/유지할 모니터링 (각 쿼리는 release dashboard timeseries widget 에 그대로 사용 가능한 형태로 작성):
service:cupixworks-worker status:error @class:SidekiqDeathHandler "deep_symbolize_keys"
service:cupixworks-worker status:error "world_transformation/pano.rb"
service:cupixworks-worker status:error "Sidekiq job died after all retries"
- 위 3개 쿼리에 대해 발생 시 알림(예: 5분 윈도우 1회 이상) 설정.
- 수정 배포 후 24시간 동안 dead job 카운트가 0 으로 유지되는지 dashboard widget 으로 확인.
Risk Assessment#
- Risk level: medium — 사용자 직접 영향(요청 실패) 은 controller 가 아닌 비동기 Sidekiq job 이므로 즉각적이진 않으나, 모든 retry 소진 시 데이터 일관성 손실(world_transformation 미설정 등 후속 상태 누락) 위험이 있음.
- 예상 복잡도: standard — 코드 변경 자체는 작지만, normalize 위치(callback vs writer vs caller) 결정과 회귀 테스트(다른 모델 동시 적용 여부) 가 필요.