ES /docs

processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)

RCA: processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)

Overview#

What Happened#

2026-07-13 14:51 KST (ap-southeast-1, production) cupixworks-api 에서 PhaseMetric 레코드가 생성될 때 발행되는 phase_metric.created pub/sub 알림이 subscriber 로 전달되는 과정에서 ArgumentError 가 발생했다. Cupix::PubSub::Subscribers::ElementRecordSynchronizer#created 는 인자 0개를 받도록 정의되어 있는데, base subscriber 는 항상 ActiveSupport::Notifications::Event 를 1개 넘긴다. 두 건이 14초 간격으로 관측되었다.

Quick Facts#

Field Value
exception.class ArgumentError
exception.message wrong number of arguments (given 1, expected 0)
top_frame lib/cupix/pub_sub/subscribers/element_record_synchronizer.rb:5
env production, ap-southeast-1

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api / SiteInsights (PhaseMetric) 2 PhaseMetric 생성 시 SiteInsights element record 동기화 실패 — ElementRecordMetaSyncWorker 가 enqueue 되지 않음

Timeline#

  1. 2026-07-13 14:51:50 KST — 첫 번째 phase_metric.created 알림 처리 실패 (ArgumentError)
  2. 2026-07-13 14:52:04 KST — 두 번째 실패, 이후 관측 없음
  3. 2026-07-13 (RCA) — code path 확인 및 원인 특정

Error Log#

Datadog Logs

text
processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 2
  • 최초 발생: 2026-07-13 14:51:50 KST
  • 최근 발생: 2026-07-13 14:52:04 KST

PhaseMetric 이 생성된 두 케이스에서 SiteInsights 쪽 element record 동기화 워커(ElementRecordMetaSyncWorker)가 enqueue 되지 않았다. 사용자에게 노출되는 API 응답은 실패하지 않았지만 (subscriber 는 base 에서 rescue StandardError 로 삼켜짐), 해당 Phase 의 element record 메타 데이터는 최신 상태로 재계산되지 않아 SiteInsights 화면/집계에서 stale 상태가 될 수 있다.

Root Cause Summary#

Cupix::PubSub::Subscribers::ElementRecordSynchronizer#created 메서드가 파라미터 없이 정의(def created)되어 있는데, base subscriber 의 call 은 항상 ActiveSupport::Notifications::Event 를 인자로 넘겨서 handler.send(method_name, event) 를 호출한다. 결과적으로 첫 dispatch 에서 ArgumentError: wrong number of arguments (given 1, expected 0) 가 발생하고, 메서드 본문의 event.payload[:model] 조차 실행되지 못한다. 같은 파일의 formula_changed(event) 는 정상적으로 event 파라미터를 받고 있어 created 메서드의 파라미터 누락은 명백한 오타/누락 버그이다.

Technical Analysis#

Code Path#

  • Publish 지점: PhaseMetric 저장 후 Cupix::PubSub::NotificationsManager#publish_notificationsCupix::PubSub::Publisher.broadcast_event('phase_metric', 'created', { model: ... })
  • Dispatch entry: lib/cupix/pub_sub/subscribers/base.rb:20
  • Failure point: lib/cupix/pub_sub/subscribers/element_record_synchronizer.rb:5

Publisher 는 ActiveSupport::Notifications.instrument 로 이벤트를 발행한다.

lib/cupix/pub_sub/publisher.rb:5-11ruby
def broadcast_event(namespace, event_name, payload = {}, &block)
  event_name = [namespace, event_name].compact.join('.')

  ActiveSupport::Notifications.instrument(event_name, payload) do
    yield if block_given?
  end
end

Base subscriber 는 subscription 콜백에서 반드시 ActiveSupport::Notifications::Event 객체를 만들어 handler 에 넘긴다.

lib/cupix/pub_sub/subscribers/base.rb:20-27ruby
def call(subscription_name, *args)
  method_name = subscription_name.gsub("#{namespace}.", '')
  handler = self.class.new(namespace)
  Cupix::Logger.debug("triggered by: '#{subscription_name}'", class: self.class.name, function: method_name)
  handler.send(method_name, ActiveSupport::Notifications::Event.new(subscription_name, *args))
rescue StandardError => e
  Cupix::Logger.error("processing '#{subscription_name}' failed: #{e.message}", class: self.class.name, function: method_name, error: e)
end

Subscriber 는 initializer 에서 phase_metric namespace 로 attach 된다.

config/initializers/subscribers/phase_metric.rb:1-3ruby
Rails.application.config.to_prepare do
  Cupix::PubSub::Subscribers::ElementRecordSynchronizer.attach_to('phase_metric')
end

created 메서드 정의는 파라미터 없이 선언되어 있으며, 본문에서는 정의되지 않은 로컬 변수 event 를 참조한다. 같은 클래스의 formula_changed(event) 와 비교하면 파라미터 선언이 누락된 상태임이 명확하다.

lib/cupix/pub_sub/subscribers/element_record_synchronizer.rb:1-30ruby
module Cupix
  module PubSub
    module Subscribers
      class ElementRecordSynchronizer < Subscribers::Base
        def created
          send("_sync_by_#{self.namespace}", event.payload[:model])
        end

        def formula_changed(event)
          model = event.payload[:model]
          user = event.payload[:user]

          raise Cupix::Errors::Argument.new(code: 'ARG10300', reason: 'only PhaseMetric is allowed') unless model.is_a?(::PhaseMetric)

          jid = ::ElementRecordMetaSyncWorker.perform_async('Phase', model.phase_id, user.try(:id) || model.user_id, Current.si_trace_id, Current.si_trace_origin)

          Cupix::Logger.info("ElementRecordMetaSyncWorker (jid: #{jid}) requested for Phase with id: #{model.phase_id}", class: self.class.name, function: __method__, model: { name: 'Phase', id: model.phase_id }, jid: jid)
        end

        private

        def _sync_by_phase_metric(model)
          jid = ::ElementRecordMetaSyncWorker.perform_async('Phase', model.phase_id, model.user_id, Current.si_trace_id, Current.si_trace_origin)

          Cupix::Logger.info("ElementRecordMetaSyncWorker (jid: #{jid}) requested for Phase with id: #{model.phase_id}", class: self.class.name, function: __method__, model: { name: 'Phase', id: model.phase_id }, jid: jid)
        end
      end
    end
  end
end

기대 동작 vs 실제 동작

  • 기대: created(event) 가 이벤트를 받아 event.payload[:model]PhaseMetric 인스턴스를 꺼내고, _sync_by_phase_metric(model) 을 통해 ElementRecordMetaSyncWorker 를 enqueue.
  • 실제: send(method_name, event) 호출 시점에 def created 가 0-arity 이므로 ArgumentError 발생. 본문에 도달하지 못하고 base 의 rescue StandardError 에서 처리 → error 로그만 남고 worker 는 스케줄되지 않음.

Blame 상 이 메서드 형태는 98ec22c4 (2025-02-21 eddy.lee) 부터 존재하며, 이후 d6ebb2ec (2026-02-25, TSLA-11418) 커밋은 같은 파일에서 _sync_by_phase_metricperform_async 인자에 Current.si_trace_id/origin 을 추가했을 뿐 created 시그니처는 손대지 않았다. 즉 잠재 결함이 오래 존재했지만, PhaseMetric.create_event_publishable?false 로 설정되어 있어 평소에는 dispatch 되지 않다가 최근 이벤트 게이팅이 열린 케이스에서만 노출된 것으로 보인다 (app/models/concerns/eventable/siteinsights/phase_metric.rb:7-9).

app/models/concerns/eventable/siteinsights/phase_metric.rb:6-17ruby
def create_event_publishable?
  false
end

def update_event_publishable?
  false
end

def delete_event_publishable?
  true
end

create_event_publishable?false 임에도 실제로 phase_metric.created 알림이 발행된 정확한 코드 경로는 이 RCA 시점에 특정하지 못했다 (uncertain -- needs verification). Publisher 자체는 다른 경로(직접 broadcast_event('phase_metric', 'created', ...) 호출 또는 eventable 이 아닌 별도 pub/sub 등)로도 트리거될 수 있으므로, 조사가 필요하다.

Log Evidence#

Datadog query (재현)

text
service:cupixworks-api status:error @environment:production "processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)"

시간 범위: from_ts=1783918260000&to_ts=1783925580000 (클러스터 파일의 ## Datadog URL 그대로).

핵심 로그 항목 (원문)

json
{
  "timestamp": "2026-07-13 14:52:04",
  "status": "error",
  "message": "processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)",
  "class": "Cupix::PubSub::Subscribers::ElementRecordSynchronizer",
  "function": "created",
  "error": { "msg": "wrong number of arguments (given 1, expected 0)" }
}
json
{
  "timestamp": "2026-07-13 14:51:50",
  "status": "error",
  "message": "processing 'phase_metric.created' failed: wrong number of arguments (given 1, expected 0)",
  "class": "Cupix::PubSub::Subscribers::ElementRecordSynchronizer",
  "function": "created",
  "error": { "msg": "wrong number of arguments (given 1, expected 0)" }
}

두 로그 모두 class/function 태그가 base subscriber 의 Cupix::Logger.error(..., class: self.class.name, function: method_name, ...) 에서 유래하며, 실패 지점은 ElementRecordSynchronizer#created 로 정확히 지목된다 (lib/cupix/pub_sub/subscribers/base.rb:26).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 ElementRecordSynchronizer#created 가 0-arity 로 정의되어 있어 base subscriber 가 넘기는 Event 인자와 시그니처가 어긋난다 element_record_synchronizer.rb:5 def created, base.rb:24 handler.send(method_name, Event.new(...)), 로그의 class/function 태그가 정확히 이 지점을 가리킴, 에러 메시지 문자열이 Ruby ArgumentError arity 미스매치 표준 문구 Confirmed
H2 Payload 자체가 손상되어 Event.payload[:model] 접근에서 실패 본문 참조 시 payload 접근이 존재 ArgumentError 는 메서드 진입 시점(arity check)에서 발생. event.payload 는 실행조차 되지 못함. 에러 문구도 NoMethodError/KeyError 가 아닌 arity 문구 Rejected
H3 Publisher 가 잘못된 event_name 을 사용하거나 subscriber 이중 attach attach_to 는 initializer 에서 1회, broadcast_event 는 namespace/event_name 을 결합 (publisher.rb:6) 로그의 subscription_name 이 정확히 phase_metric.created 로 정상 조합. 이중 attach 였다면 로그는 arity 가 아닌 다른 에러였을 것 Rejected
H4 formula_changed(event) 처럼 최근 리팩터링에서 파라미터가 잘못 제거됨 git blame 결과 def created98ec22c4 (2025-02-21, eddy.lee) 부터 존재. 이후 파일 변경(TSLA-11418, d6ebb2ec)은 _sync_by_phase_metric 만 손댐 최근 회귀가 아닌, 최초 도입 시부터의 결함 Rejected (원인 부분만 부분 확정: 회귀 아님)

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/pub_sub/subscribers/element_record_synchronizer.rb:5def created 시그니처에 event 파라미터를 추가하여 base subscriber 의 dispatch 계약(handler.send(method_name, ActiveSupport::Notifications::Event.new(...)))과 일치시킨다. 같은 파일 formula_changed(event) 가 이미 올바른 패턴을 따르고 있으므로 동일한 스타일을 적용한다.
lib/cupix/pub_sub/subscribers/element_record_synchronizer.rb:5-7
-        def created-          send("_sync_by_#{self.namespace}", event.payload[:model])-        end+        def created(event)+          send("_sync_by_#{self.namespace}", event.payload[:model])+        end
  • 관련 spec 이 있다면 (spec/lib/cupix/pub_sub/subscribers/element_record_synchronizer_spec.rb 등) phase_metric.created 알림 발행 시 ElementRecordMetaSyncWorker.perform_async 가 호출되는지 확인하는 케이스를 추가/보강한다 (현재는 arity 결함이 spec 에도 반영되어 있었을 가능성 — uncertain, spec 파일 존재 확인 필요).

단기 개선 (1주 이내)#

  • Cupix::PubSub::Subscribers::Base#call 이 모든 예외를 rescue StandardError 로 삼키면서 error 로그만 남기는 구조(base.rb:25-27) 때문에 arity 미스매치 같은 프로그래밍 오류가 배포 후에도 조용히 흘러갔다. base 에서 subscriber 메서드 정의 시점에 arity 를 검증하거나 (method(:created).arity 확인), 최소한 CI 에서 각 subscriber 의 public 메서드 시그니처가 (event) 인지 확인하는 lint 를 추가한다.
  • PhaseMetriccreate_event_publishable?false 인데도 이번에 알림이 실제로 발행된 경로를 특정한다. eventable gating 이 우회되는 경우가 있다면 다른 subscriber 에도 동일한 잠재 결함이 있을 수 있다.

장기 개선 (재발 방지)#

  • Pub/Sub subscriber 를 위한 명시적 인터페이스(예: base class 에서 abstract 또는 expect_event(event) 시그니처 강제)를 도입해 새로 추가되는 subscriber 가 컴파일/부트 시점에 시그니처 오류를 잡히도록 한다.
  • Sorbet 타입 시그니처(sig { params(event: ActiveSupport::Notifications::Event).void })를 subscriber 메서드에 강제하는 규칙을 추가하면 정적으로 감지 가능하다.

Monitoring#

  • 알림/메트릭: pub/sub subscriber 실패 카운트를 별도 지표로 노출. class:Cupix::PubSub::Subscribers::*, function:*, message prefix "processing '", "' failed:" 를 기반으로 그룹화.
  • Datadog query 예시 (dashboard timeseries widget 에 그대로 삽입 가능):
text
sum:datadog.estimated_usage.logs.ingested_events{service:cupixworks-api,status:error,@class:Cupix::PubSub::Subscribers::*}.as_count()

배포 후 이번 케이스 재발 여부만 확인하려면 다음 쿼리를 사용한다.

text
service:cupixworks-api status:error @environment:production "processing 'phase_metric.created' failed"

Risk Assessment#

  • Risk level: low (사용자 API 응답에는 영향 없음, subscriber 는 rescue 로 삼켜짐. 다만 SiteInsights element record 동기화가 누락되어 데이터 stale 위험 존재)
  • 예상 복잡도: trivial (한 줄 시그니처 수정 + spec 보강)