ES /docs

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

RCA: WorkspacesController#share Latency (1924ms)

Overview#

What Happened#

2026-05-27 10:38 UTC, eu-central-1 리전에서 Api::V1::WorkspacesController#share 엔드포인트가 1924ms 소요되었다. Workspace 540에 2명의 신규 사용자를 초대하는 과정에서, 동기적으로 수행되는 AWS Cognito 사용자 생성 API 호출이 전체 응답 시간의 대부분(~1735ms)을 차지했다.

Quick Facts#

Field Value
resource_name Api::V1::WorkspacesController#share
duration 1924ms (DB: 187ms, External: ~1735ms)
top_frame app/repositories/concerns/sharable_repository.rb:155
env production, eu-central-1
deploy production-eu-central-1-20260527T0505Z0-650f3601-cupixworks

Affected Teams#

Team / Domain Error Count Impact
danfoss (Team ID 48) 1 사용자가 workspace share 시 ~2초 대기

Timeline#

  1. 10:38:10.155Z — PUT /api/v1/workspaces/540/share 요청 시작
  2. 10:38:10.602Z — 첫 번째 Cognito 사용자 생성 호출 (pauline.fichaux@danfoss.com)
  3. 10:38:12.603Z — 두 번째 Cognito 사용자 생성 호출 (m.bron@danfoss.com) + 첫 번째 완료
  4. 10:38:15.213Z — 요청 완료 (200 OK, 총 1922.44ms)

Error Log#

Datadog Logs

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

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-27T10:38:10.155Z
  • 최근 발생: 2026-05-27T10:38:10.155Z
  • 영향: Workspace 공유 시 사용자가 ~2초간 응답 대기. 200 OK로 기능적 실패는 없으나 UX 저하.

Root Cause Summary#

WorkspacesController#share 액션이 신규 사용자를 초대할 때, InvitationOperation.invite_people()UserFactory.create!()User#after_create 콜백을 통해 AWS Cognito API(create_user!, user_exists?)를 동기적으로 호출한다. 2명의 신규 사용자 생성 시 Cognito API가 각각 ~1초씩 소요되어 총 ~2초의 지연이 발생했다. DB 시간(187ms)과 view/serialization(6ms)은 전체의 10%에 불과하며, 나머지 ~1735ms는 모두 외부 Cognito API 호출에 소비되었다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/share_controller.rb:32
app/controllers/concerns/share_controller.rb:32-45ruby
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
  • Main logic: app/repositories/concerns/sharable_repository.rb:131

share 메서드에서 params[:emails]가 존재하면 InvitationOperation.invite_people()를 동기 호출한다:

app/repositories/concerns/sharable_repository.rb:143-156ruby
if params[:emails].present?
  email_array =
    if params[:emails].is_a?(Array)
      params[:emails]
    else
      params[:emails].split(',').collect(&:strip) rescue []
    end

  already_joined_or_invited_users = UserRepository.where(email: email_array, team: @current_team, state: %i[active invited])
  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
end
  • Invitation -> User creation: app/operations/invitation_operation.rb:61-82

각 이메일에 대해 순차적으로 UserFactory.new.create!() 호출:

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

    if invitee.present?
      unless invitee.cycle_state_created?
        invitee.cycle_state = 'created'
        invitee.cycle_state_updated_at = DateTime.now
      end
      invitee.trashed_at = nil
      invitee.purged_at = nil
      invitee.state = 'created'
      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
  end
end
  • Failure point (latency source): lib/cupix/aws/cognito/user.rb:54-72

User 모델의 after_create 콜백이 Cognito API를 동기 호출:

lib/cupix/aws/cognito/user.rb:10-11ruby
after_create :create_cognito_user, unless: :skip_cognito_user_creation
lib/cupix/aws/cognito/user.rb:54-72ruby
def create_cognito_user
  if Cupix::Aws::Cognito.user_exists?(self.email)
    Cupix::Logger.info("User already exists in Cognito: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')
    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}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')
    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
rescue StandardError => e
  Cupix::Logger.error("Failed to create cognito user: #{self.email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito', error: e.message)
else
  Cupix::Logger.info("User created in Cognito: #{email}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')
end

기대 동작: share 요청이 200-500ms 이내에 완료되어야 한다. 실제 동작: 신규 사용자 N명 초대 시, Cognito API 호출이 순차적으로 N*~1초 소요되어 응답 시간이 선형으로 증가한다.

  • 추가 지연: app/models/concerns/permissionable.rb:60-66

각 사용자/그룹에 대해 add_permission!이 루프 내에서 호출되며, 매번 flush_cached_permissions + flush_cached_permissions_by_user를 실행:

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

Log Evidence#

Datadog 쿼리:

text
service:cupixworks-api @request_id:"fb6de8b0-0db3-4fc7-9cbd-8e605476195a"

요청의 전체 trace (22 로그 엔트리):

text
10:38:10.602Z [INFO] Cognito.create_cognito_user - "Creating user in Cognito: pauline.fichaux@danfoss.com"
10:38:12.603Z [INFO] Cognito.create_cognito_user - "Creating user in Cognito: m.bron@danfoss.com"
10:38:12.603Z [INFO] Cognito.create_cognito_user - "User created in Cognito: pauline.fichaux@danfoss.com"
10:38:12.603Z [INFO] Cognito.create_cognito_user - "User created in Cognito: m.bron@danfoss.com"
10:38:12.603Z [INFO] User.segment_user_event - "Segment event sent for User 5010" (pauline.fichaux@danfoss.com)
10:38:12.603Z [INFO] User.segment_user_event - "Segment event sent for User 5011" (m.bron@danfoss.com)
10:38:12.603Z [INFO] Cupix::EventService.publish_event - "Published event - failed_record_count: 0 / 1" (User 5010)
10:38:12.603Z [INFO] Cupix::EventService.publish_event - "Published event - failed_record_count: 0 / 1" (User 5011)
10:38:12.603Z [INFO] User.flush_cached_permission - "Deleting cached_permission on user 5010"
10:38:12.603Z [INFO] User.flush_cached_permission - "Deleting cached_permission on user 5011"
10:38:12.603Z [WARN] WorkspacePermission._update_document - "NotFound - attributes_in_database"
10:38:12.603Z [WARN] WorkspacePermission._update_document - "NotFound - attributes_in_database"
10:38:12.604Z [INFO] Module.flush_cached_permissions - "Flush cached permissions for User 5010"
10:38:12.604Z [INFO] Module.flush_cached_permissions - "Flush cached permissions for User 5011"
10:38:12.604Z [INFO] Module.flush_cached_permissions_by_user - "Flush cached permissions By User for User 5010 on Workspace 540"
10:38:12.604Z [INFO] Module.flush_cached_permissions_by_user - "Flush cached permissions By User for User 5011 on Workspace 540"
10:38:15.213Z [INFO] [200] PUT /api/v1/workspaces/540/share (1922.44ms, db: 187.35ms, view: 0.07ms)

패턴 확인 — 다른 리전에서도 동일 패턴 발생:

text
service:cupixworks-api @action:share @controller:"Api::V1::WorkspacesController" @duration:>1000
text
2026-05-26T15:20:49.879Z [us-west-2] PUT /api/v1/workspaces/5298/share - 2140.84ms (db: 395.02ms)
  Team: trane, User: richard.viglione@trane.com

시간 분석:

  • 총 소요 시간: 1922.44ms
  • DB 시간: 187.35ms (9.7%)
  • View/Serialization: 6.07ms (0.3%)
  • 외부 API 호출 (Cognito + Segment + EventService): ~1729ms (89.9%)

첫 Cognito 호출 시작(10:38:10.602Z)에서 완료(10:38:12.603Z)까지 ~2001ms 소요. 이는 Cognito.user_exists? + Cognito.create_user! 두 번의 AWS API 왕복을 포함한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 동기적 Cognito API 호출이 주요 지연 원인 첫 Cognito 로그(10:38:10.602Z)~완료(10:38:12.603Z) = 2초. DB는 187ms로 전체의 10%만 차지. 다른 리전(us-west-2)에서도 동일 패턴(2140ms) 확인 Confirmed
H2 DB 쿼리 성능 저하 (N+1, missing index) DB 시간 187ms로 일부 기여 전체 1922ms 중 10%에 불과. 주요 bottleneck이 아님 Rejected
H3 Elasticsearch document sync 실패가 지연 원인 WorkspacePermission._update_document NotFound 경고 2건 발생 경고는 10:38:12.603Z에 발생하며 비동기 document update 실패일 뿐, 요청 완료는 10:38:15.213Z로 이 시점 이후에도 추가 작업 진행. 주요 원인이 아님 Rejected
H4 Permission cache flush가 지연 원인 4명 사용자에 대해 flush 호출 다수 확인 flush 호출은 모두 10:38:12.603-12.604Z에 완료. Worker enqueue는 비동기이므로 주요 지연 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: lib/cupix/aws/cognito/user.rb:10
  • 방향: after_create :create_cognito_user 콜백을 비동기 worker로 전환. User 생성 시 Cognito 계정 생성을 CreateCognitoUserWorker.perform_async(user.id)로 백그라운드 처리하여 HTTP 요청 경로에서 분리.
  • 대안: share 요청 경로에서만 skip_cognito_user_creation!을 설정하고, 별도 worker에서 Cognito 생성을 후처리.

단기 개선 (1주 이내)#

  • InvitationOperation.invite_people()에서 다수 사용자를 초대할 때 emails.map 루프를 병렬화하거나, 최소한 Cognito 호출만 비동기로 분리.
  • add_permission! 루프(sharable_repository.rb:159-164)를 batch insert로 변경하여 N번의 개별 DB 트랜잭션 + cache flush 를 1회로 축소.
  • Eventable::Events::Share.create_event()의 Segment/EventService 호출을 비동기 worker로 위임.

장기 개선 (재발 방지)#

  • Workspace share 액션의 전체 플로우를 비동기 패턴으로 재설계: 즉시 202 Accepted 응답 후, 백그라운드에서 초대/Cognito 생성/이벤트 발행을 처리하고, 완료 시 WebSocket 또는 polling으로 결과 전달.
  • 외부 API 호출(Cognito, Segment, Midas)에 timeout 및 circuit breaker 적용.

Monitoring#

  • Share 엔드포인트 P95/P99 duration 메트릭 추적:
text
avg(trace.rack.request.duration){service:cupixworks-api, resource_name:Api::V1::WorkspacesController#share} by {region}
  • Cognito API 호출 duration 별도 메트릭 추가:
text
service:cupixworks-api "Creating user in Cognito" | stats avg(@duration) by @usr.email
  • 1초 이상 소요되는 share 요청에 대한 알림:
text
service:cupixworks-api @action:share @duration:>1000

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 기능적 실패(500 에러)가 아닌 UX 지연 이슈. 발생 빈도는 신규 사용자 초대 시에만 해당하며 기존 사용자 공유는 영향 없음. 그러나 초대 인원 수에 비례하여 지연이 선형 증가하므로, 대량 초대(10명 이상) 시 timeout 위험 존재.