ES /docs

GroupedUser counter_culture_fix_counts full table scan — missing scope

RCA: Api::V1::SignupsController#signup_on_team Latency (avg 1244ms)

Overview#

What Happened#

2026-05-26 05:15~06:10 UTC 사이에 cupixworks-api 서비스의 POST /api/v1/signups/team 엔드포인트가 평균 1244ms, 최대 1521ms의 응답 시간을 기록했다. 4개 리전(ca-central-1, ap-northeast-1, us-west-2, ap-southeast-1)에서 8건 발생. 모든 요청은 HTTP 200으로 정상 응답했으나 latency threshold(500ms)를 초과했다.

Quick Facts#

Field Value
resource_name Api::V1::SignupsController#signup_on_team
top_frame app/operations/signup_operation.rb:43
avg_duration 1244ms
max_duration 1521ms
env production (ca-central-1, ap-northeast-1, us-west-2, ap-southeast-1)

Timeline#

  1. 2026-05-26T05:15:33Z — 최초 latency threshold 초과 감지
  2. 2026-05-26T06:10:54Z — 마지막 slow trace 기록 (8건 누적)
  3. 2026-05-26T10:24:02Z — APM 메트릭에서 평균 816ms로 확인 (개선 추세)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::SignupsController#signup_on_team",
  "service": "cupixworks-api",
  "occurrences": 8,
  "avg_ms": 1244,
  "max_ms": 1521,
  "sample_trace_id": "3691244330617136317"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 8
  • 최초 발생: 2026-05-26T05:15:33.012Z
  • 최근 발생: 2026-05-26T06:10:54.831Z
  • 사용자 영향: 팀 가입 시 1.2~1.5초 대기. 기능적 오류는 없으나 UX 저하.

Root Cause Summary#

signup_on_team 요청 처리 과정에서 3가지 성능 병목이 복합적으로 작용한다: (1) user.active_state! 상태 전환 시 after_transition 콜백에서 GroupedUser.counter_culture_fix_counts스코프 없이 호출하여 전체 grouped_users 테이블을 스캔하고, (2) Cupix::Aws::Cognito.change_password가 매 호출마다 STS assume_role + Cognito API를 순차적으로 실행하며 (credential 캐싱 없음), (3) Group 멤버십 삽입이 N+1 패턴으로 각 group.users << user마다 after_create 콜백(reindex_group, flush_cached_permitted_items)을 트리거한다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/signups_controller.rb:24
  • SignupOperation.team_signup(params): app/operations/signup_operation.rb:43
  • user.active_state!: app/operations/signup_operation.rb:81 → state machine transition
  • after_transition callback: app/models/concerns/statable/user.rb:42-44after_state_changed
  • GroupedUser.counter_culture_fix_counts (unscoped): app/models/concerns/statable/user.rb:48
  • Group membership loop: app/operations/signup_operation.rb:85-94
  • Cognito.change_password: app/operations/signup_operation.rb:96lib/cupix/aws/cognito.rb:162
  • Cognito.client (STS assume_role): lib/cupix/aws/cognito.rb:9-27
  • UserOperation.get_token: app/controllers/api/v1/signups_controller.rb:27

병목 1: counter_culture_fix_counts (전체 테이블 스캔)

app/models/concerns/statable/user.rb:42-49ruby
after_transition from: any, to: any do |user, transition|
  user.after_state_changed(transition)
end

def after_state_changed(transition)
  GroupedUser.counter_culture_fix_counts
end

counter_culture_fix_countsonly: 또는 where: 파라미터 없이 호출된다. 이는 전체 grouped_users 테이블의 카운터를 재계산한다. 비교: 동일 코드베이스의 editing_factory.rb:68에서는 스코프를 지정한다:

app/factories/editing_factory.rb:68ruby
::EditingEntity.counter_culture_fix_counts only: :editing, where: { editings: { id: editing_ids } }

병목 2: Cognito API 호출 (credential 미캐싱)

lib/cupix/aws/cognito.rb:9-27ruby
def client(region: nil)
  region ||= Cupix::Tesla.region

  sts_client = ::Aws::STS::Client.new(region: region)
  assumed_role = sts_client.assume_role(
    role_arn: $AWS.fetch(:cognito).fetch(:role),
    role_session_name: 'CognitoSession'
  )
  credentials = assumed_role[:credentials]

  ::Aws::CognitoIdentityProvider::Client.new(
    region: region,
    credentials: ::Aws::Credentials.new(
      credentials[:access_key_id],
      credentials[:secret_access_key],
      credentials[:session_token]
    )
  )
end

매 호출마다 STS assume_role → Cognito client 생성을 반복한다. change_password (cognito.rb:162)에서 client를 호출하므로 매 signup 요청마다 STS round-trip이 발생한다.

병목 3: N+1 Group 멤버십 삽입

app/operations/signup_operation.rb:85-94ruby
Group.left_outer_joins(:email_domains).where(
  team: team,
  email_domains: { domain: ['*', user_email_domain] },
  group_type_code: 'normal'
).or(Group.left_outer_joins(:email_domains).where(
       team: team,
       group_type_code: 'users'
     )).distinct.each do |group|
  group.users << user unless group.users.exists?(user.id)
end

group.users << userGroupedUser 생성을 트리거하고, after_create :reindex_groupafter_commit :flush_cached_permitted_items 콜백이 실행된다.

Log Evidence#

Datadog에서 해당 시간대의 요청을 확인. 모두 HTTP 200 정상 응답이나 latency threshold 초과:

text
service:cupixworks-api "SignupsController" "signup_on_team"
Time range: 2026-05-26T04:15:00Z to 2026-05-26T07:00:00Z
text
[200] POST /api/v1/signups/team (Api::V1::SignupsController#signup_on_team)

에러 로그 없음 (status:error 검색 결과 0건). 순수 latency 이슈.

APM 메트릭 (2026-05-26 17:24 KST 기준):

json
{
  "query": "avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::signupscontroller_signup_on_team}",
  "value": 0.816258,
  "unit": "seconds"
}

평균 816ms로 여전히 높은 수준이나 인시던트 시점의 1244ms 대비 개선 추세.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 GroupedUser.counter_culture_fix_counts 전체 테이블 스캔이 주요 병목 스코프 없는 호출 확인 (statable/user.rb:48), 다른 코드에서는 only:/where: 사용 (editing_factory.rb:68) 정확한 쿼리 시간 측정 불가 (APM span 미확인) Confirmed
H2 Cognito STS assume_role 매 호출 반복이 latency 증가 원인 client 메서드가 캐싱 없이 매번 STS 호출 (cognito.rb:9-27), cross-region 호출 시 추가 latency Cognito 에러 로그 0건 (기능적 문제는 아님) Confirmed
H3 외부 서비스 장애로 인한 일시적 latency spike 4개 리전 동시 발생, 인시던트 시점에 집중 에러 로그 0건, 이후에도 816ms 유지, 코드 구조상 상시 발생 가능 Rejected
H4 DB 커넥션 풀 고갈/lock contention 여러 리전 동시 발생 다른 엔드포인트 latency 이슈 없음, 발생 빈도 8건으로 burst 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/statable/user.rb:48GroupedUser.counter_culture_fix_counts를 해당 유저가 속한 그룹으로 스코프 제한. 현재 전체 테이블 스캔을 유저 관련 그룹으로 한정.

단기 개선 (1주 이내)#

  • lib/cupix/aws/cognito.rb:9-27Cognito.client 메서드에 credential 캐싱 추가. STS assume_role 결과를 TTL(예: 50분, 토큰 만료 1시간 기준)과 함께 캐싱하여 매 요청마다 STS round-trip을 제거.
  • app/operations/signup_operation.rb:85-94 — Group 멤버십 삽입을 bulk insert로 변경하거나, 콜백을 일시 suppress 후 일괄 처리하는 방식 검토.

장기 개선 (재발 방지)#

  • after_transition 콜백에서 expensive operation(counter_culture_fix_counts)을 비동기 worker로 위임. 실시간 정확도가 불필요한 counter는 background job으로 처리.
  • Cognito client를 싱글톤 또는 connection pool 패턴으로 관리하여 credential 재사용 보장.

Monitoring#

  • avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::signupscontroller_signup_on_team} > 500ms 알림 설정
  • GroupedUser.counter_culture_fix_counts 실행 시간 계측 (custom instrumentation)
  • Cognito API 호출 횟수/latency 추적:
text
avg:trace.aws.request.duration{service:cupixworks-api,aws_service:cognitoidentityprovider}

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — counter_culture_fix_counts 스코프 추가는 straightforward하나 counter 정확성 검증 필요. Cognito client 캐싱은 thread-safety 고려 필요.