ES /docs

Api::V1::Admin::EditingsController#add_reviewer (avg 248065ms, max 248065ms)

RCA: Api::V1::Admin::EditingsController#add_reviewer latency outlier (248s)

Overview#

What Happened#

2026-06-15 20:27 KST cupixworks-apiApi::V1::Admin::EditingsController#add_reviewer 엔드포인트에서 단일 요청이 약 248초 동안 실행된 latency outlier가 감지되었다 (APM 트레이스 9175273460483011690). 동일 시간대의 다른 add_reviewer 요청은 65–180ms 수준으로 정상이었고, 이 한 건만 평소 p99 대비 약 1,000배 이상 느렸다.

Quick Facts#

Field Value
resource_name Api::V1::Admin::EditingsController#add_reviewer
sample_trace_id 9175273460483011690
avg_duration_ms 248065
max_duration_ms 248065
occurrences 1
env production, us-west-2
tenant cupix
version production-us-west-2-20260613t...-9443a6d8-cupixworks

Affected Teams#

Team / Domain Error Count Impact
admin (team_id=133, Retool 운영툴) 1 운영자가 reviewer 추가 시 응답을 받지 못함 (ALB idle timeout 또는 클라이언트 타임아웃 도달 가능). 동일 윈도에서 같은 사용자가 다른 editing(id=1166083 등)에 대한 add_reviewer는 정상 처리됨.

Timeline#

  1. 2026-06-15 20:27 KST — APM trace 9175273460483011690 종료, 총 duration 248,065ms 기록 (cluster first_seen/last_seen).
  2. 2026-06-15 20:28 KST — 동일 사용자(claire.lee@cupix.com, team_id=133)가 editing 1166083에 대해 add_reviewer 정상 호출 (65.83ms, db 19.21ms). 시스템은 직후 정상 복구되었다.
  3. 2026-06-15 20:29~20:33 KST — 같은 윈도 내 다른 add_reviewer/Priority score calculated 로그 정상 발생. 동일 outlier 재현 없음.
  4. 2026-06-15 (수집 시점) — error-sweeper collector 가 @duration:>threshold 쿼리로 슬로우 스팬 수집, latency 클러스터 생성.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::Admin::EditingsController#add_reviewer",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 248065,
  "max_ms": 248065,
  "sample_trace_id": "9175273460483011690"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-06-15 20:27 KST
  • 최근 발생: 2026-06-15 20:27 KST

영향 범위는 한 번의 admin Retool 호출 1건. 동일 사용자/엔드포인트의 후속 요청은 모두 정상이었으므로 사용자 차원의 지속적 장애는 아니다. 다만 add_reviewer 응답이 동기적으로 EditingPriorityScorer를 호출하므로 (아래 Code Path 참조), 재발 시 admin 운영툴이 멈추는 UX 이슈를 다시 만들 수 있다.

Root Cause Summary#

Api::V1::Admin::EditingsController#add_reviewer 는 reviewer 생성 직후 EditingPriorityScorer.new(editing_id, editor_id).call 을 동기적으로 호출하고, scorer는 editing_entities (includes(:entity)), team.entity_parameters 등 여러 association 을 추가 쿼리한다. 평상시에는 65–180ms 수준이지만, 이번 1건에 한해 약 248초가 소요되었다. 확정된 root cause는 식별되지 않았다 (uncertain — needs verification): 해당 trace id 에 해당하는 request completion log 가 Datadog 에 존재하지 않는데, 정상 종료 시 항상 기록되는 [NNN] PUT /api/v1/admin/editings/:id/add_reviewer 로그가 없다는 점은 Rails worker (Puma) 가 ALB/Puma worker timeout 또는 SIGKILL 로 응답을 마치지 못하고 종료되었음을 시사한다. 가장 유력한 가설은 EditingPriorityScorer 가 호출하는 association 쿼리(editing_entities 또는 team.entity_parameters) 중 하나가 lock contention/네트워크 hiccup/이상 데이터 분포로 인해 비정상적으로 오래 블록되었고, 이로 인해 동기 경로 전체가 정체되었다는 것이다. 다만 동시간대 DB 에러나 lock timeout 로그는 관찰되지 않아 확증할 수는 없다.

Technical Analysis#

Code Path#

Entry point: app/controllers/api/v1/admin/editings_controller.rb:1 (route PUT /api/v1/admin/editings/:id/add_reviewerReviewableController#add_reviewer).

Controller action 은 단순히 repository 의 add_reviewer 를 호출하고 결과를 직렬화한다:

app/controllers/concerns/reviewable_controller.rb:4-10ruby
def add_reviewer
  repository_instance.add_reviewer(params)
  render_api Renderable.new({
    contents: @model,
    serializer_option: @serializer_option
  })
end

Repository 에서 user lookup → reviewer create → 동기적으로 scorer 호출:

app/repositories/concerns/reviewable_repository.rb:4-17ruby
def add_reviewer(params)
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'user_id is required') if params[:user_id].nil?

  user = User.find_by(team: current_user.team, id: params[:user_id])

  raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Reviewer not found') if user.blank?

  existing_reviewer = self.model.reviewers.find_by(user: user)
  return existing_reviewer if existing_reviewer

  self.model.reviewers.create!(user: user).tap do
    EditingPriorityScorer.new(editing_id: self.model.id, editor_id: user.id).call
  end
end

Scorer 는 생성자에서 Editing.find + User.find 후, call 에서 점수를 계산하고 update! 로 저장한다. 점수 계산 단계는 여러 association 을 추가 쿼리한다:

app/services/editing_priority_scorer.rb:48-81ruby
def initialize(editing_id:, editor_id: nil)
  @editing = ::Editing.find(editing_id)
  @editor = editor_id ? ::User.find(editor_id) : nil
end

def call
  return nil unless @editing.editing_type == 'normal'

  score = calculate
  return nil if score.nil?

  @editing.update!(priority_score: score)
  score
end
app/services/editing_priority_scorer.rb:146-166ruby
def pano_capture?
  captures = @editing.editing_entities
                     .where(entity_type: 'Capture')
                     .includes(:entity)
                     .map(&:entity)
                     .compact

  capture_type_ids = captures.map(&:capture_type_id).compact.uniq
  return false if capture_type_ids.empty?

  ::CaptureType.where(id: capture_type_ids, method: %w[singleshot multishot]).exists?
end

def pointcloud_entity?
  @editing.editing_entities.where(entity_type: 'Pointcloud').exists?
end

def paid_a_lot_score
  boost_param = @editing.team&.entity_parameters&.find { |ep| ep.name == 'editing_priority_boost' }
  boost_param&.value == 'true' ? PAID_A_LOT_SCORE : 0
end

기대 동작: 전체 경로가 짧은 SQL 몇 개 + editing.update! 로 100ms 이내 완료되어야 한다. 실제 동작: 이번 trace 한 건에서 248초가 소요됨. 어디서 시간이 소진되었는지는 스팬 상세를 보지 않으면 단정할 수 없다(uncertain — needs verification via APM trace span breakdown).

Failure point (most likely): scorer 내부의 동기 association 쿼리 또는 이어지는 @editing.update! 한 곳에서 외부 요인(lock/네트워크/이상 데이터)에 의해 블록되었을 것으로 추정.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "EditingsController" "add_reviewer"
service:cupixworks-api "9175273460483011690"
service:cupixworks-api "1166083"
service:cupixworks-api "Priority score"
service:cupixworks-api status:warn
service:cupixworks-api status:error
service:cupixworks-worker status:error

(시간 범위: 2026-06-15T11:20:00Z ~ 2026-06-15T11:35:00Z UTC, 그리고 now-14d 범위)

핵심 관찰 1 — 정상 add_reviewer (직후 호출) 의 duration:

json
{
  "@timestamp": "2026-06-15T11:28:10.347Z",
  "action": "add_reviewer",
  "controller": "Api::V1::Admin::EditingsController",
  "params": { "id": "1166083" },
  "user": { "email": "claire.lee@cupix.com", "id": 22893, "team": { "id": 133 } },
  "user_agent": "Retool/2.0 (+https://docs.tryretool.com/docs/apis)",
  "duration": 65.83,
  "view": 0.06,
  "db": 19.21,
  "http": { "status_code": 200, "method": "PUT" },
  "request_id": "1afd9150-e1d3-4020-bc97-6b0fb3bb0f73"
}

핵심 관찰 2 — 14일 범위에서 본 add_reviewer 요청 로그의 duration 분포 (top 20, ms 단위):

text
840.57, 182.81, 170.13, 122.87, 114.66, 112.03,
89.79, 87.24, 84.42, 84.19, 84.07, 83.32, 82.84,
81.52, 80.59, 78.21, 77.56, 77.10, 76.74, 74.88

→ 정상 p99 는 ~200ms 이내. 248,065ms 는 약 1,200x 이상 outlier.

핵심 관찰 3 — trace id 9175273460483011690 으로 직접 검색한 결과 0건:

text
Searching: service:cupixworks-api "9175273460483011690"
Time range: now-2h to now
Found 0 logs:

핵심 관찰 4 — 동시간대 (11:20–11:35 UTC) cupixworks-api status:error 0건. status:warnPointcloud NotFound - attributes_in_database 등 무관한 메시지. EditingPriorityScorer 가 정상 종료한 케이스의 "Priority score calculated" 로그는 11:26:24, 11:28:32, 11:29:00, 11:31:05 ... 등 정상 분포 — 이 한 건만 누락.

→ trace 의 request completion 로그가 없음 + p99 대비 압도적 outlier + 동시간대 다른 호출은 정상 = 일시적 환경 stall (worker timeout 으로 종료) 가능성을 시사. 단, DB lock/timeout 의 직접 증거는 로그에 없음 (uncertain — needs verification with APM span breakdown 또는 RDS performance insights).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 EditingPriorityScorer 동기 호출이 add_reviewer 응답시간을 항상 늘려 SLO 를 위협한다 scorer 가 controller→repository 경로에서 동기 실행 (reviewable_repository.rb:14-16), editing_entities/team.entity_parameters 등 N+1 가능성 있는 쿼리 수행 14일 범위 정상 add_reviewer 의 db 시간 17–20ms, total 60–180ms — 평균적으로는 빠름. 단일 outlier 만 존재 Rejected (만성 문제 아님; 단, 동기 호출 자체는 위험 인자 — H4 참조)
H2 외부 요인(특정 RDS lock/네트워크 hiccup)으로 scorer 내부의 association 쿼리 또는 editing.update! 가 비정상적으로 블록됨 trace 의 request completion log 부재 → worker stall 정황. 동시간대 다른 동일 엔드포인트 호출은 정상 → 코드 결함이 아닌 환경 요인 시사 DB lock timeout/connection 에러 로그 부재. APM span breakdown 미확인 Inconclusive — needs verification
H3 특정 editing 의 editing_entities 가 비정상적으로 많아 scorer 의 includes(:entity) 쿼리가 폭발 scorer 코드 (editing_priority_scorer.rb:146-156) 가 editing_entities 전체를 메모리로 로드 후 map(&:entity) 수행 — 데이터 분포에 따라 비례적으로 느려질 수 있음 트레이스에 묶인 editing_id 를 확인할 수 있는 request log 가 없어 entity count 검증 불가 Inconclusive — needs verification (trace span 으로 editing_id 확인 후 Kibana 로 entity count 조회 필요)
H4 코드 결함 (race condition/deadlock) 이 add_reviewer 본문에서 재현 가능 동일 사용자/팀이 동시간대 동일 엔드포인트를 정상 호출, 재현 없음 (occurrence_count=1) Rejected
H5 클라이언트(Retool) 측 retry/재시도가 만든 합산 시간이 trace 에 반영됨 APM trace duration 은 단일 span의 wall-clock — 클라이언트 retry 와 무관 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 추가 코드 변경 권장하지 않음 (occurrence_count=1, root cause 미확정). 대신 다음을 수행하여 재발 시 즉시 진단 가능하도록 한다:
    1. 운영자(요청자)는 Datadog APM 의 trace 9175273460483011690 span breakdown 을 열어, 어느 SQL/외부 호출에서 시간이 소진됐는지 확인. (RDS Performance Insights 에서 동일 시점 lock/long-running query 도 함께 확인)
    2. 해당 trace 의 editing_id 를 식별하면 Kibana 로 editing_entities 카운트를 조회해 H3 검증.

단기 개선 (1주 이내)#

  • EditingPriorityScorer.call 의 비동기화 검토 (app/repositories/concerns/reviewable_repository.rb:14-16). admin endpoint 응답 경로에서 분리하여 ActiveJob/Sidekiq worker 로 enqueue 하면, scorer 가 어떤 이유로 느려지더라도 사용자 응답에 영향이 없다. 단, 호출자 측에서 priority_score 업데이트의 즉시성 요구 여부를 product 와 합의 필요.
  • Puma worker timeout 알림 강화: 60s 이상 실행되는 Rails 요청이 발생하면 별도 알림이 가도록 monitor 추가 (현재는 이번처럼 슬로우 스팬 수집기에서 사후 감지됨).

장기 개선 (재발 방지)#

  • EditingPriorityScorer#pano_capture?editing_entities.includes(:entity).map(&:entity) 패턴은 entity 수에 비례해 느려진다. 필요한 컬럼만 join 으로 가져오거나(joins(:entity).pluck(...)), editing_entities.where(entity_type: 'Capture').joins(entity: :capture_type).where(capture_types: { method: %w[singleshot multishot] }).exists? 형태로 단일 SQL 로 전환하여 데이터 분포에 robust 하게 만든다.
  • admin 동기 경로에 의존하는 부수 작업(scorer, 통계, 인덱싱 등)은 일반적으로 background job 화하는 정책을 확립한다.

Monitoring#

다음 Datadog 쿼리를 release/health 대시보드 timeseries 위젯에 추가한다.

add_reviewer 평균/최대 응답시간 추이:

text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::admin::editingscontroller#add_reviewer}
text
max:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::admin::editingscontroller#add_reviewer}

add_reviewer 호출 수:

text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::admin::editingscontroller#add_reviewer}.as_count()

5초 이상 걸린 cupixworks-api 요청 수 (worker stall 조기 감지):

text
sum:trace.rack.request.duration.by.service.5s{service:cupixworks-api}.as_count()

(위 메트릭 이름이 환경에 없는 경우 trace.rack.request.duration{service:cupixworks-api} 의 percentile 위젯 + threshold 마커로 대체)

Risk Assessment#

  • Risk level: low (단일 outlier, occurrence_count=1, 사용자 영향 미미, 코드 결함 미확인)
  • 예상 복잡도: trivial (즉시 코드 수정 필요 없음). 단기 개선(scorer 비동기화)은 standard 수준.