ES /docs

Api::V1::WorkspacesController#share (avg 10639ms, max 10639ms)

RCA: Api::V1::WorkspacesController#share latency (10639ms)

Overview#

What Happened#

PUT /api/v1/workspaces/5853/share 요청 하나가 10639 ms 걸렸다. HTTP 200 으로 정상 성공했으나, 새 이메일 5명을 초대하면서 각 초대자마다 Cognito 사용자 계정을 request thread 안에서 순차 생성했다. Cognito API 호출 26회(약 4.8초) 와 초대자별 권한 부여 SQL 354건(약 4.2초 wall)이 합쳐져 지연이 발생했다. 코드 결함이 아니라 초대 인원에 비례하는 synchronous fan-out 성능 문제다.

Quick Facts#

Field Value
cluster_type latency (exception 아님)
resource_name Api::V1::WorkspacesController#share
http.route /api/v1/workspaces/:id/share
http.method / status_code PUT / 200
avg / max duration 10639 ms / 10639 ms
top_frame app/repositories/concerns/sharable_repository.rb:155 (InvitationOperation.invite_people)
deploy production-us-west-2-20260806T1506Z0-9b66f324-cupixworks
host ip-10-1-16-80.us-west-2.compute.internal
env production, us-west-2
trace_id / request_id 3186060758106362080 / 895db26e-f784-41e5-875e-d050f18e23fe

Affected Teams#

Team / Domain Error Count Impact
cupix (tenant southlandind.com) 1 trace (5 invitees) 워크스페이스 공유 시 초대 이메일 수에 비례한 응답 지연. 성공(200)하나 사용자가 10초 대기

Timeline#

  1. 2026-08-07 02:51:44 KSTPUT /api/v1/workspaces/5853/share 요청 시작 (request_id 895db26e...)
  2. 2026-08-07 02:51:45 KST — 첫 초대자 tpickrell@southlandind.com Cognito 생성 시작
  3. 2026-08-07 02:51:47 ~ 02:51:53 KST — 초대자 reyna.hughes / dbernstein / mhallenbeck / aking 순차 Cognito 생성 (약 2초 간격)
  4. 2026-08-07 02:51:55 KST — 마지막 초대자 aking@southlandind.com Cognito 생성 완료
  5. 2026-08-07 02:51:55 KST — 요청 종료, 총 10639 ms 소요, HTTP 200

Error Log#

이 클러스터는 exception 이 아니라 latency span 이다. 대표 정보는 span 이다.

Datadog Logs

json
{
  "resource_name": "Api::V1::WorkspacesController#share",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 10639,
  "max_ms": 10639,
  "sample_trace_id": "3186060758106362080"
}

Impact#

  • Service: cupixworks-api
  • Team: cupix (tenant southlandind.com)
  • 발생 횟수: 1
  • 최초 발생: 2026-08-07 02:51 KST
  • 최근 발생: 2026-08-07 02:51 KST

Root Cause Summary#

WorkspacesController#shareSharableRepository#share 를 호출하고, 이 메서드는 아직 가입하지 않은 이메일마다 InvitationOperation.invite_people 로 사용자를 생성한다. User 모델의 after_create :create_cognito_user callback 이 초대자 한 명당 Cognito list_users + admin_create_user + admin_set_user_password 를 request thread 안에서 동기적으로 실행한다. 이 트레이스는 새 이메일 5명을 초대했고, Cognito 호출 26회(약 4.8초)와 초대자별 permission 부여 SQL 354건(약 4.2초 wall)이 순차 누적되어 응답이 10639 ms 로 늘어났다. 초대 인원에 선형 비례하는 synchronous fan-out 이 근본 원인이며, 요청 자체는 200 으로 성공한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/share_controller.rb:32 (ShareController#share, WorkspacesController 가 include)
  • Failure(지연) point: app/repositories/concerns/sharable_repository.rb:155app/operations/invitation_operation.rb:60lib/cupix/aws/cognito/user.rb:60

컨트롤러는 repository_instance.share 로 위임한다.

app/controllers/concerns/share_controller.rb:32-44ruby
def share
  validate_project_permissions_feature!

  members = repository_instance.share(
    params.slice(*SHARE_REQUEST_PARAMS),
    params[:permission]
  )

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

SharableRepository#share 는 아직 가입/초대되지 않은 이메일을 골라 InvitationOperation.invite_people 를 호출한다. 이 호출은 background worker 가 아니라 request thread 안에서 실행된다.

app/repositories/concerns/sharable_repository.rb:152-156ruby
invite_candidate_user_emails = email_array - already_joined_or_invited_users.pluck(:email)

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

invite_people 는 이메일 배열을 map 으로 순회하며 초대자마다 UserFactory#create! 로 사용자를 생성한다. 반복 안에 초대 메일 발송(send_invitation_mail)도 들어 있다.

app/operations/invitation_operation.rb:59-93ruby
invitees = emails.map do |email|
  begin
    invitee = User.find_by(team: current_team, email: email)

    if invitee.present?
      # ...
      invitee.save!
    else
      user_factory = UserFactory.new(current_user: current_user, current_team: current_team, skip_permission_check: params[:skip_permission_check])

      invitee = user_factory.create!({
        team: current_team,
        email: email
      })
    end
  # ...
  send_invitation_mail({ user_email: email, redirect_url: redirect_url }, current_user, current_team, invitee)

  invitee
end

User 가 저장되면 after_create :create_cognito_user callback 이 동기적으로 Cognito 를 호출한다. user_exists?(= list_users) 를 먼저 확인하고, 없으면 create_user! 를 부른다.

lib/cupix/aws/cognito/user.rb:8-11,60-67ruby
included do
  attr_accessor :skip_cognito_user_creation

  after_create :create_cognito_user, unless: :skip_cognito_user_creation
  after_update :update_cognito_user
end
# ...
def create_cognito_user
  if Cupix::Aws::Cognito.user_exists?(self.email)
    # ...
  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: ...)
  end

create_user! 는 내부에서 다시 user_exists?(list_users), admin_create_user, admin_set_user_password 를 연달아 호출한다. 초대자 한 명당 Cognito round-trip 이 3~4회 발생한다.

lib/cupix/aws/cognito.rb:42-88ruby
def create_user!(user)
  # ...
  raise Cupix::Errors::Argument.new(code: 'AUTH20012', reason: "User #{user.email} already exists on Cognito") if user_exists?(user.email)

  response = client.admin_create_user(
    username: user.email,
    message_action: 'SUPPRESS',
    temporary_password: random_password,
    # ...
  )

  client.admin_set_user_password(
    user_pool_id: $AWS.fetch(:cognito).fetch(:user_pool_id),
    username: user.email,
    password: random_password,
    permanent: true
  )

  response
end

기대 동작: 워크스페이스 공유는 즉시 응답하고, 신규 사용자 provisioning 은 background 로 처리되어야 한다. 실제 동작: 초대자 N명이면 Cognito 호출이 약 3~4N회 순차 발생하고, 이후 share 는 초대자 전원에 대해 add_permission! + event 생성 + serializer 렌더까지 request thread 에서 마저 수행한다. 결과적으로 응답 시간이 초대 인원에 선형 비례한다.

Log Evidence#

트레이스 span 분해 (query: trace_id:3186060758106362080). 총 1111 span. component 별 duration 합계:

text
rack               n=    1 sum=  10639.3ms   (root, PUT /api/v1/workspaces/5853/share, status 200)
action_pack        n=    1 sum=  10622.2ms
aws                n=   26 sum=   4758.3ms
net/http           n=   26 sum=   4672.6ms   (aws-sdk-ruby over net/http)
active_record      n=  354 sum=   4197.9ms
mysql2             n=  238 sum=   1879.9ms
elasticsearch      n=   27 sum=    274.3ms
faraday            n=   27 sum=    259.1ms
redis              n=  139 sum=    147.3ms
active_support     n=  233 sum=    135.3ms
sidekiq            n=   39 sum=      0.5ms

26개 aws span 은 전부 Cognito(cognito-idp.us-west-2.amazonaws.com) 호출이며, 초대자 5명에 대응하는 list_users / admin_create_user / admin_set_user_password triplet 이 반복된다.

text
dur=  382.4ms aws.operation=admin_create_user      peer=cognito-idp.us-west-2.amazonaws.com
dur=  284.5ms aws.operation=admin_set_user_password peer=cognito-idp.us-west-2.amazonaws.com
dur=  105.4ms aws.operation=list_users             peer=cognito-idp.us-west-2.amazonaws.com
dur=   85.5ms aws.operation=list_users             peer=cognito-idp.us-west-2.amazonaws.com
dur=  398.8ms aws.operation=admin_create_user      peer=cognito-idp.us-west-2.amazonaws.com
...
dur=  442.2ms aws.operation=admin_create_user      peer=cognito-idp.us-west-2.amazonaws.com
dur=  327.4ms aws.operation=admin_set_user_password peer=cognito-idp.us-west-2.amazonaws.com
dur=   35.0ms aws.operation=assume_role            peer=sts.us-west-2.amazonaws.com

같은 request 안에서 초대자 5명이 약 2초 간격으로 순차 생성되었다 (query: service:cupixworks-api "Creating user in Cognito", 모두 si_trace_id=895db26e-f784-41e5-875e-d050f18e23fe = 이 트레이스의 request-id).

text
2026-08-07 02:51:45 KST  Creating user in Cognito: tpickrell@southlandind.com
2026-08-07 02:51:47 KST  Creating user in Cognito: reyna.hughes@southlandind.com
2026-08-07 02:51:49 KST  Creating user in Cognito: dbernstein@southlandind.com
2026-08-07 02:51:51 KST  Creating user in Cognito: mhallenbeck@southlandind.com
2026-08-07 02:51:53 KST  Creating user in Cognito: aking@southlandind.com

14일 latency 분포 (query: service:cupixworks-api resource_name:"Api::V1::WorkspacesController#share"). baseline 은 빠르고 극단 outlier 만 10초대다. 초대자가 없거나 적으면 p50 수준으로 끝난다.

text
request span count(14d): 14
p50=118 p90=10622 p95=10639 p99=10639 max=10639 ms
count >2000ms: 2   >5000ms: 2   >10000ms: 2
status codes: {"200": all sampled}

status board 조회 결과 이 클러스터의 scope svc:cupixworks-api::unknown 에 active incident 는 없다 (active: null). 같은 scope 의 최근 resolved incident 2026-08-06-svc-cupixworks-api--unknown-1 (05:42~07:21 UTC) 는 cluster_ids 에 a85d08fa 를 포함하지 않고 발생 시각도 다르므로 무관하다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 초대자별 synchronous Cognito provisioning + 권한부여 fan-out 이 지연 원인 aws 26 span 전부 Cognito(create/set_password/list_users) 합 4758 ms; active_record 354 span 4198 ms; "Creating user in Cognito" 로그 5건이 같은 request_id 로 2초 간격; 코드 sharable_repository.rb:155invite_people map → after_create :create_cognito_user 없음 Confirmed
H2 host/Puma worker 리소스 경합으로 인한 untraced stall (131084d3 유형) rack root 10639 ms instrumented children 합(aws 4758 + AR 4198 + ES 274 + redis 147 = 약 9.4초)이 root 를 대부분 설명 → untraced gap 아님 Rejected
H3 Cognito/AWS 다운스트림 장애로 인한 개별 호출 timeout aws 호출이 다수 개별 aws span 최대 442 ms 로 정상 범위, timeout/에러 없음(전부 200). 지연은 호출 지연이 아니라 호출 횟수(순차 26회) Rejected
H4 코드 결함(예: 무한 루프, N+1 회귀) AR 354 / mysql2 238 span 요청 200 성공, 초대자 5명에 선형 비례, baseline p50=118 ms 정상. 설계상 synchronous fan-out 이지 회귀 버그 아님 Rejected
H5 외부 dependency incident 의 일부 status board active: null, 최근 resolved incident 는 시각/cluster_ids 불일치 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

즉시 배포가 필요한 코드 결함은 없다. 요청은 200 으로 성공하며 데이터 손상이나 사용자 대면 에러가 아니다. 초대 인원이 많은 워크스페이스 공유에서만 응답이 느려지는 성능 이슈이므로 단기 개선으로 처리한다.

단기 개선 (1주 이내)#

  • 신규 사용자 초대(및 Cognito provisioning)를 request thread 밖으로 이동한다. app/repositories/concerns/sharable_repository.rb:154-156InvitationOperation.invite_people 동기 호출을 background worker 로 옮기는 방향을 검토한다. 이미 같은 메서드의 초대 메일 발송은 ShareEntityMailerWorker.perform_async(sharable_repository.rb:186) 로 비동기 처리하고 있어, 초대 자체도 동일 패턴을 따르는 것이 일관적이다.
  • 초대자 provisioning 이 반드시 동기여야 한다면, lib/cupix/aws/cognito.rb:42-88create_user! 안에서 user_exists?(list_users) 가 create_cognito_user(lib/cupix/aws/cognito/user.rb:60) 의 사전 확인과 중복 호출되는 부분을 정리해 초대자당 Cognito round-trip 수를 줄인다.
  • share 응답 경로(sharable_repository.rb:159-193)의 초대자별 add_permission! 와 serializer 렌더에서 발생하는 354건 AR 쿼리를 초대자 batch 처리로 축소하는 방향을 검토한다.

장기 개선 (재발 방지)#

  • 공유/초대 API 를 "즉시 응답 + 비동기 처리" 모델로 통일한다. 초대 결과는 별도 조회 또는 알림으로 노출하고, 요청 자체는 초대 인원과 무관하게 상수 시간에 반환하도록 한다.
  • APM 에 초대 인원(배치 크기)을 span tag 로 남겨 fan-out 규모와 latency 상관을 추적할 수 있게 한다.

Monitoring#

#share 엔드포인트에서 느린 요청(2초 초과)의 발생 추이를 추적한다.

text
service:cupixworks-api @resource_name:"Api::V1::WorkspacesController#share" @duration:>2000000000

Cognito 사용자 생성 로그 건수 추이 (초대 fan-out 규모의 proxy).

text
service:cupixworks-api "Creating user in Cognito"

APM 기준 #share 요청 latency 를 metric 으로 추적한다.

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::workspacescontroller#share}

Risk Assessment#

  • Risk level: low (요청은 성공하며 성능 저하만 발생, 데이터 무결성 영향 없음)
  • 예상 복잡도: standard (초대 provisioning 을 비동기로 옮기는 작업은 초대 결과 반환 계약 변경을 수반하므로 프런트엔드 조율 필요)