processing 'facility_permission.full_permission_enabled' failed: private method `service_jwt' called
RCA: processing 'facility_permission.full_permission_enabled' failed: private method `service_jwt' called
Overview#
facility permission 이벤트를 구독하는 UserRecipeGenerator subscriber 가 email recipe 를 생성하려고 Cupix::NotificationService 의 class method service_jwt 를 호출하는데, 이 메서드가 private_class_method 로 선언되어 있어 NoMethodError 로 실패한다. rubocop autocorrect 회귀가 원인이며, 이미 fix branch 가 존재하지만 아직 배포 브랜치에 병합되지 않았다.
What Happened#
2026-08-06 배포 이후 cupixworks-api 에서 facility permission 부여(facility_permission.created, facility_permission.full_permission_enabled) 이벤트를 처리하는 pub/sub subscriber Cupix::PubSub::Subscribers::UserRecipeGenerator 가 NoMethodError: private method 'service_jwt' called for class Cupix::NotificationService 로 실패한다. subscriber 의 rescue StandardError 가 예외를 삼켜 요청 자체는 500 이 아니지만, 신규 멤버용 email recipe 자동 생성이 조용히 누락된다. production 기준 now-14d 창에서 33 건 이상, 전체 50 건 이상이 수 분 단위로 발생하는 고빈도 이슈다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | NoMethodError |
| exception.message | private method 'service_jwt' called for class Cupix::NotificationService |
| top_frame | lib/cupix/notification_service.rb:20 |
| deploy | regression commit 54f4417c2 (TSLA-13082), fix 8bc08ee69 (TSLA-13863) unmerged |
| env | production, dev / ap-southeast-2, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
cupixworks-api (subscriber UserRecipeGenerator) |
50+ (33+ production) in now-14d | facility 권한 부여 시 신규 멤버 email recipe 자동 생성 실패 |
Timeline#
- 2026-06-11 09:31 KST — rubocop 수정 커밋
54f4417c2(TSLA-13082) 가private_class_method :service_jwt, :find_or_create_internal_user추가. - 2026-08-04 17:59 KST — fix 커밋
8bc08ee69(TSLA-13863) 가feature/TSLA-13863브랜치에 생성 (아직 미병합). - 2026-08-06 15:28 KST — 클러스터 first_seen (
first_seen06:28:44 UTC). - 2026-08-06 16:21 KST — 클러스터 last_seen (
last_seen07:21:03 UTC). - 2026-08-06 16:37 KST — Datadog 로그상 계속 발생 중 (RCA 시점).
Error Log#
processing 'facility_permission.full_permission_enabled' failed: private method `service_jwt' called for class Cupix::NotificationService
Impact#
- Service:
cupixworks-api - 발생 횟수: 3 (클러스터 집계값; 실제 Datadog now-14d 50+ 건, production 33+ 건)
- 최초 발생: 2026-08-06 15:28 KST
- 최근 발생: 2026-08-06 16:21 KST
Root Cause Summary#
Cupix::NotificationService.create_user_recipe 는 email-service 호출 시 Authorization: "Bearer #{self.class.service_jwt}" 로 class method service_jwt 를 explicit receiver (self.class.) 로 호출한다. 그러나 커밋 54f4417c2 (TSLA-13082 "fix rubocop Lint/IneffectiveAccessModifier", 2026-06-11) 가 private_class_method :service_jwt, :find_or_create_internal_user 를 추가했다. Ruby 는 private method 를 explicit receiver 로 호출하면 NoMethodError: private method called 를 raise 한다. rubocop autocorrect 가 접근 제어자만 바꾸고 caller 의 호출 형태(self.class.service_jwt)를 함께 고치지 않아 발생한 회귀다. facility 권한 이벤트 subscriber UserRecipeGenerator 가 recipe 생성 중 이 메서드를 호출하다 매번 실패한다.
Technical Analysis#
Code Path#
Entry point: pub/sub subscriber 가 facility permission 이벤트를 수신한다. subscriber 는 config/initializers/subscribers/facility_permission.rb:2 에서 FacilityPermission 네임스페이스에 attach 된다.
Cupix::PubSub::Subscribers::UserRecipeGenerator.attach_to(::FacilityPermission.name.underscore)
full_permission_enabled 이벤트가 _create_recipes_for_facility_permission 를 거쳐 recipe 3종 loop 로 진입한다.
def full_permission_enabled(event)
send("_create_recipes_for_#{self.namespace}", event.payload[:model])
end
# ...
def _create_recipes(opts = {})
%w[
record_preview_ready
record_processing_completed
facility_new_project
].each do |recipe_name|
Cupix::NotificationService.new(user: opts[:user]).create_user_recipe(recipe_name, team_id: opts[:team_id], facility_key: opts[:facility_key])
end
end
Failure point: create_user_recipe 가 self.class.service_jwt 를 호출한다 (origin/master 기준 line 20).
def create_user_recipe(recipe_name, team_id: nil, facility_key: nil)
return if @service_url.nil?
params = email_recipe_params(recipe_name, team_id, facility_key)
response = Cupix::HttpClient.post(
"#{@service_url}/api/recipes/v2",
params.to_json,
{
content_type: :json,
Authorization: "Bearer #{self.class.service_jwt}"
}
)
service_jwt 는 private_class_method 로 선언되어 있어 explicit receiver 호출이 거부된다.
def self.service_jwt
if @service_jwt && @service_jwt_exp && @service_jwt_exp > 30.minutes.from_now.to_i
return @service_jwt
end
# ...
end
# ...
private_class_method :service_jwt, :find_or_create_internal_user
기대 동작: create_user_recipe 가 service_jwt 로 Bearer 토큰을 얻어 email-service 에 recipe 를 생성한다. 실제 동작: self.class.service_jwt 호출이 NoMethodError 로 즉시 실패한다.
이 예외는 subscriber 상위의 Base#call rescue StandardError 가 잡아 error 로그로 기록한다. 로그 메시지 포맷이 정확히 일치한다.
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
회귀를 유발한 커밋의 diff:
++ private_class_method :service_jwt, :find_or_create_internal_userfix branch feature/TSLA-13863 의 fix 커밋 8bc08ee69 은 service_jwt 를 private_class_method 대상에서 제외하고 find_or_create_internal_user 만 private 으로 유지한다.
- private_class_method :service_jwt, :find_or_create_internal_user+ private_class_method :find_or_create_internal_userLog Evidence#
사용한 Datadog 쿼리:
service:cupixworks-api "service_jwt"
now-14d 창에서 50 건 이상, production 한정 33 건 이상. 대표 로그 원문:
{
"timestamp": "2026-08-06 16:37:08",
"status": "error",
"message": "processing 'facility_permission.full_permission_enabled' failed: private method `service_jwt' called for class Cupix::NotificationService",
"class": "Cupix::PubSub::Subscribers::UserRecipeGenerator",
"function": "full_permission_enabled",
"error": {
"msg": "private method `service_jwt' called for class Cupix::NotificationService"
}
}
created 액션도 동일 root cause 로 발생한다.
{
"timestamp": "2026-08-06 16:37:40",
"status": "error",
"message": "processing 'facility_permission.created' failed: private method `service_jwt' called for class Cupix::NotificationService",
"class": "Cupix::PubSub::Subscribers::UserRecipeGenerator",
"function": "created"
}
로그의 class = Cupix::PubSub::Subscribers::UserRecipeGenerator, function = full_permission_enabled / created 가 위 code path 를 정확히 확증한다. 로그 timestamp 가 16:37 KST 까지 이어져 RCA 시점 현재 진행 중이다.
git 상태 확인 (regression 은 배포 브랜치에 있고 fix 는 미병합):
git merge-base --is-ancestor 54f4417c2 origin/master → YES (regression in master)
git merge-base --is-ancestor 54f4417c2 origin/develop → YES (regression in develop)
git merge-base --is-ancestor 8bc08ee69 origin/master → NO (fix not in master)
git merge-base --is-ancestor 8bc08ee69 origin/develop → NO (fix not in develop)
git branch -a --contains 8bc08ee69 → feature/TSLA-13863 only
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | private_class_method :service_jwt 회귀가 self.class.service_jwt explicit-receiver 호출을 깨뜨림 |
notification_service.rb:20 self.class.service_jwt + :243 private_class_method; commit 54f4417c2 가 접근제어자만 추가; Datadog 50+ 로그 메시지 정확 일치; class/function 태그 일치 |
— | Confirmed |
| H2 | SubscriptionGenerator 등 다른 subscriber 가 원인 |
facility_permission 네임스페이스에 여러 subscriber 존재 가능 | 로그 class = UserRecipeGenerator 로 확정; create_user_recipe 만 service_jwt 호출 (다른 recipe 경로는 api_token 사용) |
Rejected |
| H3 | ispring cutover noise (같은 05:xx UTC 창 4b1f8e9b) 와 동일 배포-transient noise | 같은 active 인시던트 svc:cupixworks-api::unknown 에 묶임 |
service_jwt 는 고정 코드 심볼; 회귀 commit 이 master 에 상존; 로그가 16:37 KST 까지 지속 (transient 아님) |
Rejected |
| H4 | 이미 develop 에 fix 반영되어 다음 릴리스만 대기 (release cadence) | fix commit 8bc08ee69 이 존재 |
fix 는 feature/TSLA-13863 에만 있고 master/develop 모두 미병합; 두 배포 브랜치 다 회귀 상태 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
기존 fix branch feature/TSLA-13863 (fix commit 8bc08ee69, TSLA-13863) 을 review 후 develop 및 master 로 병합하고 배포한다. 이 branch 는 service_jwt 를 private_class_method 대상에서 제외해 find_or_create_internal_user 만 private 으로 유지한다 (lib/cupix/notification_service.rb:243). 새 PR 을 만들지 말고 이 기존 fix 를 배포 경로에 올리는 것이 우선이다. 병합 전까지 production 에서 email recipe 자동 생성이 계속 실패한다.
병합 접근 대안 (fix branch 를 쓰지 않는 경우): caller (notification_service.rb:20) 의 호출을 private-safe 형태로 바꾸거나 (service_jwt 를 receiver-less 로 호출) service_jwt 를 public 으로 복구한다. 단, 이미 검증된 fix branch 가 있으므로 그 병합을 권장한다.
단기 개선 (1주 이내)#
rubocop autocorrect 로 접근 제어자를 변경할 때 caller 호출 형태가 함께 수정되었는지 검증하는 절차가 필요하다. Lint/IneffectiveAccessModifier 처럼 시그니처가 바뀌는 cop 은 autocorrect 결과를 테스트로 검증한 뒤 병합해야 한다. UserRecipeGenerator → create_user_recipe → email-service 호출 경로에 대한 subscriber smoke test 를 추가해 이 회귀가 CI 에서 잡히도록 한다.
장기 개선 (재발 방지)#
subscriber 의 Base#call rescue StandardError (base.rb:25-26) 는 NoMethodError / NameError 같은 프로그래밍 에러를 정상적인 처리 실패와 동일하게 삼킨다. 이 때문에 회귀가 요청 실패로 드러나지 않고 조용히 email recipe 누락으로만 나타났다. 프로그래밍 에러 클래스는 별도 심각도로 격상하거나 alert 를 연결해 조용한 실패를 조기에 감지하도록 rescue 구조를 개선한다.
Monitoring#
배포 후 회귀 재발 여부를 추적하는 timeseries 쿼리:
service:cupixworks-api "service_jwt" "private method"
facility permission subscriber 실패 전반 추적:
service:cupixworks-api status:error @class:Cupix::PubSub::Subscribers::UserRecipeGenerator
Risk Assessment#
- Risk level: medium (요청 500 은 아니지만 신규 멤버 email recipe 자동 생성이 조용히 누락되어 알림 미수신 가능)
- 예상 복잡도: trivial (기존 fix branch 병합; 코드 변경은
private_class_method대상 1줄)