ES /docs

Api::V1::ReviewsController#bulk_share (avg 8675ms, max 8675ms)

RCA: Api::V1::ReviewsController#bulk_share Latency (8675ms)

Overview#

What Happened#

2026-05-27 05:25 UTC에 cupixworks-api 서비스의 PUT /api/v1/reviews/share 엔드포인트에서 단일 요청이 8,675ms 소요되었다. 이 요청은 3개의 Review를 새 사용자에게 공유하는 과정에서, 신규 사용자를 AWS Cognito에 동기적으로 생성하고, notification recipe를 순차적으로 생성하며, permission cascade를 처리하느라 응답 시간이 크게 지연되었다.

Quick Facts#

Field Value
resource_name Api::V1::ReviewsController#bulk_share
top_frame app/controllers/concerns/share_controller.rb:62
duration 8675ms (DB: 432ms, app: ~8243ms)
env production, us-west-2
deploy production-us-west-2-20260527t0524z0-650f3601-cupixworks

Timeline#

  1. 05:25:04ZPUT /api/v1/reviews/share 요청 수신 (3 reviews, 1 new email)
  2. 05:25:07Z — 신규 사용자 Cognito 생성 완료 (~2.9s)
  3. 05:25:07Z — Segment event 전송, PubSub 이벤트 발행
  4. 05:25:10~11Z — FacilityPermission 생성 → PubSub UserRecipeGenerator 트리거 → notification recipe 3건 순차 생성 (~5.5s)
  5. 05:25:13Z — Permission flush, event publish, 응답 반환 (HTTP 200)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::ReviewsController#bulk_share",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 8675,
  "max_ms": 8675,
  "sample_trace_id": "1189013322334403148"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-27T05:25:04.751Z
  • 최근 발생: 2026-05-27T05:25:04.751Z
  • 사용자 영향: 단일 사용자(user_id: 46182, nishiyama.satoshi@takenaka.co.jp)가 3개 Review를 공유하는 동안 ~8.7초 대기. 기능적 실패 없음 (HTTP 200).

Root Cause Summary#

bulk_share 액션이 신규 이메일로 공유할 때, HTTP 요청 내에서 (1) AWS Cognito 사용자 생성, (2) FacilityPermission 생성 시 PubSub를 통해 UserRecipeGenerator가 notification recipe 3건을 순차적 외부 HTTP 호출로 생성, (3) 복수 Review에 대한 permission cascade + Elasticsearch 동기 인덱싱이 모두 동기적으로 실행된다. 이 중 notification recipe 순차 생성(~5.5초)과 Cognito 사용자 생성(~2.9초)이 전체 8.7초 지연의 대부분을 차지한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/share_controller.rb:62bulk_share 액션
app/controllers/concerns/share_controller.rb:62-77ruby
def bulk_share
  validate_project_permissions_feature!
  check_share_requests!(params)

  members = params[:items].map do |item|
    @model = repository_instance.show(item[:model_id])

    repository_instance.share(item.slice(*SHARE_REQUEST_PARAMS), item[:permission])
  end.flatten.compact

  render_api Renderable.new({
    contents: members.uniq,
    is_collection: true,
    serializer: MemberSerializer
  })
end

각 item에 대해 repository_instance.show(11-JOIN SQL)와 share를 반복 호출한다.

  • Share 실행 흐름: app/repositories/concerns/sharable_repository.rb:131
app/repositories/concerns/sharable_repository.rb:143-164ruby
if params[:emails].present?
  # ...
  if invite_candidate_user_emails.present?
    invited_users = InvitationOperation.invite_people(
      { user_emails: invite_candidate_user_emails, skip_permission_check: true },
      self.current_user, @current_team, @model
    )
  end
end

(@users + @groups + already_joined_or_invited_users + invited_users).each do |user_or_group|
  @model.add_permission! user_or_group, permission
  shared_users_and_groups << user_or_group
end

새 이메일이 포함되면 InvitationOperation.invite_people이 동기적으로 사용자를 생성한다.

  • Cognito 사용자 생성 (동기): lib/cupix/aws/cognito/user.rb:10 — User model의 after_create 콜백
lib/cupix/aws/cognito/user.rb:54-72ruby
def create_cognito_user
  if Cupix::Aws::Cognito.user_exists?(self.email)
    cognito_user = Cupix::Aws::Cognito.get_user_by_email(self.email)
    self.update!(cognito_user_id: cognito_user.attributes.find { |attr| attr.name == 'sub' }.value)
  else
    Cupix::Logger.info("Creating user in Cognito: #{self.email}", ...)
    cognito_user_creation_response = Cupix::Aws::Cognito.create_user!(self)
    self.update!(cognito_user_id: cognito_user_creation_response.user.attributes.find { |attr| attr.name == 'sub' }.value)
  end
end

AWS Cognito API 호출 2회 (user_exists? + create_user!)가 동기적으로 HTTP 요청 내에서 실행된다.

  • Notification recipe 생성 (동기, 최대 병목): FacilityPermission 생성 → PubSub → UserRecipeGenerator
lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-41ruby
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

3개의 notification recipe를 each로 순차적 외부 HTTP 호출(DynamoDB 기반 notification-service)하여 ~5.5초 소요.

  • Permission cascade (동기): app/models/concerns/permissionable.rb:60
app/models/concerns/permissionable.rb:60-68ruby
def add_permission!(user_or_group, permission)
  _permission = permissions.find_or_initialize_by(accessor: user_or_group)
  _permission.permission = permission
  _permission.save!

  Permissionable.flush_cached_permissions(user_or_group)
  Permissionable.flush_cached_permissions_by_user(user_or_group, self)

  _permission
end

save!가 ReviewPermission → before_create :create_facility_permissionbefore_create :create_workspace_permissionbefore_create :create_team_permission cascade를 트리거하고, 각 after_commit에서 Elasticsearch 동기 인덱싱이 실행된다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api @http.url_details.path:"/api/v1/reviews/share"
text
service:cupixworks-api status:info @class:Cognito
text
service:cupixworks-api status:info @class:Cupix::NotificationService

요청 완료 로그:

json
{
  "message": "[200] PUT /api/v1/reviews/share (Api::V1::ReviewsController#bulk_share)",
  "duration": 8671.15,
  "db": 432.56,
  "view": 0.14,
  "user": "nishiyama.satoshi@takenaka.co.jp",
  "user_id": 46182,
  "team": "umedafm",
  "request_id": "54b0029a-b6fd-40fa-804c-5fbee9a1ea53"
}

Cognito 사용자 생성 로그 (05:25:07.689Z):

text
Creating user in Cognito: h_kazuhiro_okedoi@eng.sanki.co.jp
User created in Cognito: h_kazuhiro_okedoi@eng.sanki.co.jp

Notification recipe 순차 생성 로그 (05:25:10~11Z):

text
recipe created: record_preview_ready (created_at: 05:25:10.964Z)
recipe created: record_processing_completed (created_at: 05:25:11.318Z)
recipe created: facility_new_project (created_at: 05:25:11.578Z)
subscription of h_kazuhiro_okedoi@eng.sanki.co.jp on Facility 5v5ar2 created

Permission flush 시 warn 로그:

text
[warn] ReviewPermission#_update_document: NotFound - attributes_in_database

시간 분석:

Phase 시간 소요
요청 시작 → Cognito 생성 완료 05:25:04.7 → 05:25:07.7 ~2.9s
Cognito 완료 → Recipe 생성 시작 05:25:07.7 → 05:25:10.9 ~3.2s (permission cascade + ES indexing)
Recipe 순차 생성 05:25:10.9 → 05:25:11.6 ~0.7s (recipe HTTP calls)
Recipe → PubSub 완료 → 응답 05:25:11.6 → 05:25:13.7 ~2.1s (flush + events)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 동기적 Cognito 사용자 생성 + notification recipe 순차 호출이 주요 병목 Cognito 생성에 ~2.9s, recipe 생성이 PubSub 경유로 ~5.5s 소요 (로그 타임스탬프 확인). DB 시간은 432ms로 전체의 5%에 불과 Confirmed
H2 N+1 DB 쿼리 (review show 반복) 가 병목 params[:items].map에서 각 item마다 11-JOIN show 쿼리 실행 (share_controller.rb:67) DB 총 시간이 432ms로 비교적 작음. 이번 케이스는 items가 3개라 DB 영향은 제한적 Rejected (이번 케이스에서는 주요 원인 아님)
H3 Elasticsearch 동기 인덱싱이 병목 ReviewPermission, FacilityPermission 등의 after_commit에서 ES 동기 호출 실행 (searchable.rb:12). Permission cascade로 최대 12건의 ES 호출 로그에서 ES 관련 에러/지연 로그 미확인. 전체 ~8.2s 중 recipe 생성이 명확히 5.5s 차지 Inconclusive (기여 요소일 수 있으나 주요 원인은 아님)

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/pub_sub/subscribers/user_recipe_generator.rb:34-41: notification recipe 생성을 비동기 worker로 이동. 현재 PubSub 이벤트 핸들러 내에서 동기적으로 3회 외부 HTTP 호출을 하고 있어, 이를 Sidekiq worker로 위임하면 ~5.5초를 즉시 절감할 수 있다.
  • lib/cupix/aws/cognito/user.rb:10: after_create :create_cognito_user 콜백을 비동기 worker로 변환. Cognito 사용자 생성은 즉시 필요하지 않으며 (초대 메일이 이미 비동기), worker에서 처리 가능하다.

단기 개선 (1주 이내)#

  • UserRecipeGenerator._create_recipes에서 3건의 recipe를 병렬(Concurrent 또는 Thread)로 생성하도록 변경. 순차 호출 대비 ~3배 속도 향상 가능.
  • share_controller.rb:66-70: params[:items]에 대한 반복을 최적화 — 동일 사용자에 대한 permission cascade가 중복 실행되지 않도록 items를 그룹핑하여 사용자당 1회만 초대/권한 부여 처리.

장기 개선 (재발 방지)#

  • bulk_share 전체 흐름에서 외부 서비스 호출(Cognito, NotificationService, EventService Kinesis, Elasticsearch)을 모두 비동기로 분리하는 아키텍처 리팩토링.
  • Permission cascade의 after_commit Elasticsearch 인덱싱을 bulk indexing worker로 교체.
  • bulk_share 요청의 SLO를 설정하고 (예: P95 < 2000ms), 초과 시 알림.

Monitoring#

  • 추가할 메트릭: bulk_share 요청 duration의 P95/P99 추적
  • Datadog 쿼리 예시:
text
service:cupixworks-api resource_name:"Api::V1::ReviewsController#bulk_share" @duration:>5000000000
  • NotificationService recipe 생성 latency 모니터링:
text
service:cupixworks-api @class:Cupix::NotificationService @function:create_user_recipe
  • Cognito user creation latency 추적:
text
service:cupixworks-api @class:Cognito @function:create_cognito_user

Risk Assessment#

  • Risk level: low (기능 실패 없이 느린 응답만 발생, 발생 빈도 낮음)
  • 예상 복잡도: standard (비동기화 패턴은 기존 Sidekiq 인프라 활용 가능하나, PubSub subscriber 구조 변경 필요)