ES /docs

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:44set_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#

  1. 2026-06-24 22:50:48 KST — 첫 dead job 발생 (SidekiqDeathHandler#death_handler 로그).
  2. 2026-06-24 22:51:18 KST — 두 번째 dead job.
  3. 2026-06-24 22:51:26 KST — 세 번째.
  4. 2026-06-24 22:51:40 KST — 네 번째.
  5. 2026-06-24 22:51:48 KST — 다섯 번째이자 (현재까지) 마지막 dead job.

Error Log#

Datadog Logs

text
Sidekiq job died after all retries

대표 로그 (Datadog query: service:cupixworks-worker "deep_symbolize_keys", time range 2026-06-24T13:40Z–14:00Z):

json
{
  "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::ParametersHash 가 아니며 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.newDB I/O 시점에만 ActionController::ParametersHash 로 변환하기 때문에, 아직 dump 되지 않은 — 즉, 컨트롤러에서 params 로 직접 대입되어 in-memory 상태인 — meta 가 callback 단계에서 그대로 ActionController::Parameters 인스턴스로 노출된다. 그 결과 NoMethodError 가 발생하고, 모든 Sidekiq retry 가 동일 입력을 반복 처리하다 dead set 으로 이동했다.

Technical Analysis#

Code Path#

  1. Controller entryApi::V1::PanosController#create / #update 는 raw params (= ActionController::Parameters) 를 그대로 factory/repository 에 전달한다.
app/controllers/api/v1/panos_controller.rb:43-53ruby
def create
  @model = factory_instance.create!(params)

  super
end

def update
  @model = repository_instance.update(params)

  super
end
  1. Meta serialization 등록Metablemeta 컬럼에 FlexibleHash coder 를 사용한다. coder 의 load/dump 는 DB 직렬화 경계에서만 동작한다.
app/models/concerns/metable.rb:1-11ruby
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
lib/cupix/util/flexible_hash.rb:3-23ruby
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
  1. Failure point — Pano 의 before_save callback 이 in-memory meta 를 그대로 사용한다. 이 시점에는 아직 dump 가 수행되지 않았으므로, controller 에서 대입된 ActionController::Parameters 가 그대로 반환되고, 곧이어 deep_symbolize_keys 호출이 실패한다.
app/models/concerns/world_transformation/pano.rb:36-47ruby
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]
  1. Pano include 사슬 — Pano 는 Properties::Pano 를 거쳐 Metable 을 포함하고, WorldTransformation::Pano 를 직접 include 하므로 두 concern 이 모두 활성화된다.
app/models/pano.rb:1-38ruby
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 단계에서는 metaActionController::Parameters 인 채로 노출되어 NoMethodError 발생. ActionController::Parameters 는 Rails 7.x 에서 Hash 를 상속하지 않으며 deep_symbolize_keys 를 노출하지 않는다.

Log Evidence#

Datadog query:

text
service:cupixworks-worker "Sidekiq job died after all retries"

시간 범위 2026-06-24T13:40:00Z – 2026-06-24T14:00:00Z 에서 5건 모두 동일한 stack frame, 동일한 메시지로 매칭됨. 14일 전체 범위로 확장해도 동일 원인은 이 시점 5건만 확인됨:

text
service:cupixworks-worker status:error @class:SidekiqDeathHandler "world_transformation"

5건 전부의 top frame:

text
/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:44meta.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::Parametersdeep_symbolize_keys 를 더 이상 응답하지 않게 됨. Gemfile 은 rails 7.2.2. Rails 7.x 에서 ActionController::ParametersHash 를 상속하지 않고 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#loadActionController::Parameters 를 만나면 to_h 로 변환한다(line 7-8). 또한 DB 컬럼은 text 이며 YAML 직렬화이므로 저장 후 다시 읽으면 Hash 가 된다. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/world_transformation/pano.rb:44meta.deep_symbolize_keys 호출이 ActionController::Parameters 도 안전하게 처리하도록 변경. 방향(어느 한 가지를 선택):
    1. callback 내부에서 normalize: metaActionController::Parametersmeta.to_unsafe_h.deep_symbolize_keys, 아니면 기존 경로. 단, to_unsafe_h 사용은 strong params 우회이므로 신중히 결정해야 한다 — 이 경로가 internal write callback 이고 권한 검증이 controller 단에서 끝났음이 보장될 때만 허용.
    2. 또는 호출자 측(Pano factory / repository / controller)에서 meta = params[:meta] 시 항상 .permit!.to_h (또는 명시적 permit 후 .to_h)로 정규화해 모델에 넘기도록 강제. 이 경우 callback 의 호출 측 가정이 유지된다.
  • 어느 쪽이든 FlexibleHash#load/dump 와 정책이 일관되어야 한다(현재 dump 는 permit!.to_h 사용).

단기 개선 (1주 이내)#

  • Metableserializeassignment 시점에도 Hash 정규화를 보장하도록 attribute writer 를 override 하거나, Rails 의 Type API 로 cast 를 직접 구현해 in-memory 단계 노출을 막는다. 그러면 before_save callback 등 모든 in-memory 경로가 자동으로 안전해진다.
  • 동일 패턴이 다른 모델/콜백에서도 잠재돼 있는지 확인: deep_symbolize_keys 또는 symbolize_keys 가 in-memory 속성에 직접 호출되는 코드 전수 점검 (Grep 으로 1차 스캔 권장).

장기 개선 (재발 방지)#

  • Rails 7 에서 ActionController::ParametersHash 의 메서드를 응답하지 않는다는 점을 인지한 가이드/lint 룰 도입.
  • 컨트롤러에서 모델로 raw params 를 직접 넘기는 패턴(factory.create!(params), repository.update(params)) 대신, 명시적 permitto_h 를 거치도록 컨벤션 통일.

Monitoring#

추가/유지할 모니터링 (각 쿼리는 release dashboard timeseries widget 에 그대로 사용 가능한 형태로 작성):

text
service:cupixworks-worker status:error @class:SidekiqDeathHandler "deep_symbolize_keys"
text
service:cupixworks-worker status:error "world_transformation/pano.rb"
text
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) 결정과 회귀 테스트(다른 모델 동시 적용 여부) 가 필요.