ES /docs

Api::V1::Admin::EditingsController#update (avg 14639ms, max 14639ms)

RCA: Api::V1::Admin::EditingsController#update — 14.6s latency

Overview#

What Happened#

2026-06-27 04:39 KST(UTC 19:39)에 production cupixworks-api (us-west-2)에서 PUT /api/v1/admin/editings/1192462 요청이 14,621.98ms 동안 실행되었다. 응답은 HTTP 200으로 정상 종료됐고, 요청은 Retool 어드민 도구가 editing의 statedone으로 변경한 케이스다. 동일 cluster fingerprint의 발생 횟수는 1회. 동시간대(19:01–20:17 UTC) svc:cupixworks-api::unknown 인시던트가 진행 중이었다 (2026-06-26-svc-cupixworks-api--unknown-4).

Quick Facts#

Field Value
resource_name Api::V1::Admin::EditingsController#update
http.method / status PUT / 200
http.path /api/v1/admin/editings/1192462
duration 14621.98 ms
db 1801.45 ms
view 0.09 ms
user_agent Retool/2.0
user eve.kim@cupix.com (id 37822, team 133)
request_id ec55d518-9d55-4c9b-8cbc-267972102a8e
host ip-10-1-19-190.us-west-2.compute.internal
deploy production-us-west-2-20260626T1924Z0-bfdc5ebd-cupixworks
env / region production / us-west-2
editing.id 1192462 (editing_type: refinement)
capture.id 722909
facility.id 13474

Affected Teams#

Team / Domain Error Count Impact
admin (team 133, Retool) 1 어드민 사용자가 editing 완료 처리 시 14.6초 대기

Timeline#

  1. 2026-06-27 04:39:19 KST (19:39:19.96 UTC)PUT /api/v1/admin/editings/1192462 요청 시작 (duration 14621.98ms 기준 역산).
  2. 2026-06-27 04:39:24 KST (19:39:24.532 UTC) — Editing 1192462: state readydone 트랜지션 시작 (요청 시작 후 약 4.6초 경과).
  3. 2026-06-27 04:39:28 KST (19:39:28.549 UTC)FORCE_APPLY_ENTITIES_STATUSES after_transition: Capture 722909 bulk_operation! 호출.
  4. 2026-06-27 04:39:30 KST (19:39:30.567 UTC) — Capture 상태 editing_readyediting_done, refinement_state readyqueued.
  5. 2026-06-27 04:39:30 KST (19:39:30.570 UTC)CaptureInvoker#create_refinement, CreateCaptureRefinementJob 1156543 생성.
  6. 2026-06-27 04:39:32 KST (19:39:32.571 UTC) — Facility 13474 reset_parent_cached_entity_updates 실행.
  7. 2026-06-27 04:39:34 KST (19:39:34.576 UTC)CreateCaptureRefinementJob#invoke_function (AWS Lambda Invoke) 호출.
  8. 2026-06-27 04:39:34 KST (19:39:34.577 UTC) — Lambda status_code(202) 응답.
  9. 2026-06-27 04:39:34 KST (19:39:34.586 UTC) — HTTP 200 응답 반환.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::Admin::EditingsController#update",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 14639,
  "max_ms": 14639,
  "sample_trace_id": "1305505309062459369"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-06-27 04:39 KST
  • 최근 발생: 2026-06-27 04:39 KST
  • 사용자 영향: Retool 어드민 사용자 1명이 editing 완료 시 14.6초 대기. 응답 자체는 성공(200). 동일 사용자/엔드포인트의 다른 호출은 0.2–2.1s 범위로 정상.

Root Cause Summary#

Admin이 editing의 statedone으로 변경하는 단일 PUT /api/v1/admin/editings/:id 요청 안에서, refinement 타입 editing의 done 트랜지션이 연쇄 동기 callback 체인을 트리거한다. 체인은 (1) Editing 모든 editing_entities에 대한 done_state! 호출, (2) Capture 상태 editing_done 트랜지션 및 bulk_operation!, (3) Refinement 상태 queued 트랜지션, (4) CaptureInvoker#create_refinementCreateCaptureRefinementJob 생성, (5) 동기 AWS Lambda Invoke API 호출(전송은 invocation_type: Event지만 HTTP 호출 자체는 동기), (6) Facility reset_parent_cached_entity_updates 등 비-DB 작업을 포함한다. 로그 타임스탬프 기준 단계별 2–4초 gap이 누적되어 총 14.6s가 소요됐고, DB time은 1.8s에 불과해 latency의 대부분(12.8s)이 callback chain 내 외부 호출/카운트 갱신/락 대기에서 발생했다. 동시간대 cupixworks-api 서비스 전반에 latency 인시던트(2026-06-26-svc-cupixworks-api--unknown-4)가 진행 중이어서 외부 의존성 응답 지연이 callback chain의 누적 비용을 증폭시킨 것으로 보인다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/editings_controller.rb:31 (#update)
  • Repository: app/repositories/admin/editing_repository.rb:13-25@model.save! 호출
  • State machine: app/models/concerns/statable/editing.rb:71-205done event의 다중 after_transition
  • Capture state callback: app/models/concerns/refinementable.rb:55-57queued 트랜지션에서 run_capture_refinement 동기 호출
  • Lambda invocation: app/jobs/create_capture_refinement_job.rb:36-75executeinvoke_function

Controller는 단순 위임이다:

app/controllers/api/v1/admin/editings_controller.rb:31-35ruby
def update
  @model = repository_instance.update(params)

  super
end

Repository도 save!만 호출하기 때문에 모든 비용은 모델 callback 체인에서 발생한다:

app/repositories/admin/editing_repository.rb:13-25ruby
def update(params = {})
  super

  set_parameters(params)

  begin
    @model.save!
  rescue StandardError => e
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
  end

  @model
end

done 트랜지션의 after_transition은 4개가 등록되어 있고 모두 동기 실행된다:

app/models/concerns/statable/editing.rb:141-174ruby
after_transition from: any, to: FORCE_APPLY_ENTITIES_STATUSES do |model, transition|
  # Force to update editing entities state
  model.editing_entities.untrashed.each do |entity|
    # ...
    entity.public_send("#{transition.to_name}_state!")
  end
end

after_transition from: any, to: %i[done skipped] do |model, transition|
  if model.editing_type == 'refinement'
    capture = model.editing_entities.where(entity_type: 'Capture').includes(:entity).first&.entity
    capture.log_trace_event('preview_finished') if capture.present? && capture.respond_to?(:log_trace_event)
  end
end

after_transition from: any, to: :done do |model, transition|
  model.start_sitetrack
end

entity의 done_state!는 Capture의 refinement_state queued 트랜지션을 트리거하고, 이 트랜지션의 before_transitionCaptureInvoker를 통해 AWS Lambda를 호출한다:

app/models/concerns/refinementable.rb:55-57ruby
before_transition any => :queued do |model, transition|
  model.run_capture_refinement(model)
end
app/models/concerns/refinementable.rb:128-131ruby
def run_capture_refinement(model)
  capture_invoker = CaptureInvoker.new(model: model, current_user: model.user, current_team: model.team)
  capture_invoker.create_refinement
end

Lambda 호출은 invocation_type: 'Event' (asynchronous on Lambda side) 이지만 Ruby 측 HTTP 호출 자체는 동기다:

app/jobs/create_capture_refinement_job.rb:36-75ruby
def execute
  response = invoke_function
  Cupix::Logger.info("Refinement transform lambda status_code(#{response.try(:status_code)}) on #{self.class.name} #{id}", class: self.class.name, function: __method__)
rescue => e
  Cupix::Logger.warn("Refinement transform lambda failed, fallback to SQS on #{self.class.name} #{id}: #{e.message}", class: self.class.name, function: __method__)
  send_message
end

def invoke_function
  # ...
  response = client.invoke({
    function_name: refinement_service_name,
    invocation_type: 'Event',
    payload: payload.to_json
  })
end

기대 동작: refinement editing 완료 처리는 사용자 작업 흐름의 일부로 1초 내외에 응답해야 하고, Lambda invoke와 SQS 전송 같은 비-사용자-체감 외부 작업은 비동기(Sidekiq worker)로 분리되어야 한다.

실제 동작: 단일 HTTP 요청 안에서 (entity 업데이트 × N) + (Capture state 전이) + (refinement state 전이 + Lambda invoke) + (Facility cached-entity 갱신) + (start_sitetrack 가드) 가 모두 직렬 실행되어 step별 latency가 누적된다. 본 사례에서 DB(1.8s)는 정상 범위였으나, callback 단계 간 gap이 2–4s씩 발생했다.

Log Evidence#

사용한 Datadog 쿼리 — request_id로 trace 추출:

text
service:cupixworks-api @si_trace_id:ec55d518-9d55-4c9b-8cbc-267972102a8e

핵심 trace (오름차순):

text
2026-06-26T19:39:24.532Z  state has transitioned from ready to done on Editing 1192462
2026-06-26T19:39:28.549Z  update - id: 722909 - 722909                            (Capture#bulk_operation!)
2026-06-26T19:39:30.567Z  [Capture] state changed from editing_ready to editing_done
2026-06-26T19:39:30.567Z  refinement_state has transitioned from ready to queued on Capture 722909
2026-06-26T19:39:30.570Z  Refinement job is created for capture 722909. job_id: 1156543
2026-06-26T19:39:30.570Z  [Capture] create_refinement
2026-06-26T19:39:32.571Z  reset Facility (ID: 13474) cached entity updates
2026-06-26T19:39:32.574Z  [Capture] preview_finished
2026-06-26T19:39:32.574Z  Called for Editing 1192462 (editing_type: refinement)   (start_sitetrack)
2026-06-26T19:39:32.574Z  editing_type is not 'normal' for Editing 1192462
2026-06-26T19:39:32.574Z  Facility is not siteinsights for Editing 1192462
2026-06-26T19:39:32.574Z  StartSitetrackWorker not started for Editing 1192462 due to unmet conditions.
2026-06-26T19:39:34.576Z  Invoking refinement transform lambda for capture 722909
2026-06-26T19:39:34.577Z  Refinement transform lambda status_code(202) on CreateCaptureRefinementJob 1156543

Request 요약(원본 attributes):

json
{
  "@timestamp": "2026-06-26T19:39:34.586Z",
  "http": { "method": "PUT", "url_details": { "path": "/api/v1/admin/editings/1192462" }, "status_code": 200 },
  "duration": 14621.98,
  "db": 1801.45,
  "view": 0.09,
  "controller": "Api::V1::Admin::EditingsController",
  "action": "update",
  "params": { "state": "done", "id": "1192462", "fields": ["id", "state"] },
  "user_agent": "Retool/2.0",
  "user": { "id": 37822, "email": "eve.kim@cupix.com", "team": { "id": 133 } },
  "tenant": "cupix"
}

단계별 gap (callback chain 누적 비용):

Step Δ (s) 설명
요청 시작 → editing.state 트랜지션 시작 ~4.6 controller before_action + factory/repository 진입 (DB 1.8s 중 일부 + 락 대기 가능)
트랜지션 시작 → Capture bulk_operation ~4.0 FORCE_APPLY_ENTITIES_STATUSES after_transition (entity 별 update + Capture 부분)
Capture update → refinement queued ~2.0 Capture state machine editing_done 처리
refinement queued → Facility cache reset ~2.0 Capture#save + reset_parent_cached_entity_updates
Facility cache reset → Lambda invoke ~2.0 _perform_sitetrack_start 가드 + Job 생성/lambda payload 준비
Lambda invoke → response ~0.01 Lambda Event 호출

동시간대 latency 인시던트 (status-board):

json
{
  "id": "2026-06-26-svc-cupixworks-api--unknown-4",
  "scope": "svc:cupixworks-api::unknown",
  "title": "cupixworks-api service degraded",
  "status": "open",
  "started_at": "2026-06-26T19:01:37.996Z",
  "last_event_at": "2026-06-26T20:17:28.494Z"
}

같은 20분 윈도(19:30–19:50 UTC)에서 @duration:>5000인 요청이 30건 이상 관찰됨 (PanosController#bulk, CapturesController#update 등 다양). 즉 본 cluster의 14.6s는 특정 endpoint 단독 회귀가 아니라 서비스 전반 latency 스파이크 와중에 callback chain이 증폭된 결과로 해석된다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 done 트랜지션의 동기 callback chain (entity 업데이트 + Capture state 전이 + AWS Lambda invoke + Facility cache reset) 이 누적되어 14.6s 소요 Trace 14개 로그가 19:39:24.532 → 19:39:34.577에 걸쳐 5개 단계 분포; DB time 1.8s, view 0.09s, 나머지 12.8s가 callback 영역; 코드 경로 statable/editing.rb:141-174, refinementable.rb:55-57, create_capture_refinement_job.rb:36-75 일치 Confirmed
H2 Active service-wide latency incident (2026-06-26-svc-cupixworks-api--unknown-4) 의 외부 의존성 지연이 step별 gap을 키움 status-board에 19:01–20:17 UTC 인시던트 open; 19:30–19:50 UTC에 @duration:>5000 요청 30+건 (PanosController#bulk, CapturesController#update 등 다른 endpoint) 본 cluster는 단일 endpoint·단일 trace; 인시던트와의 직접 인과는 단정 불가 Confirmed (contributing)
H3 DB lock contention 또는 slow query db: 1801.45 ms (전체의 12%) DB time이 전체의 12%에 불과; 동시간대 정상 요청들은 db time 유사하면서 1s 내 완료 Rejected
H4 start_sitetrack 또는 SitetrackFactory 의 분기에서 느린 처리 start_sitetrack 분기 후보 (record.captures 조인) 로그 editing_type is not 'normal' / Facility is not siteinsights 로 즉시 unmet conditions 처리됨; _perform_sitetrack_start는 호출되지 않음 (statable/editing.rb:243-271) Rejected
H5 AWS Lambda Invoke API 자체 지연 Invoking refinement transform lambda 19:39:34.576 → status_code(202) 19:39:34.577 (~1ms) Lambda 호출 자체는 빠름; 직전 단계까지의 누적이 원인 Rejected
H6 Retool 클라이언트나 인프라 (ELB/네트워크) 측 지연 duration은 Rails 서버 내부 measure Rails 로그 자체에 14.6s 기록; 클라이언트 영향이 아님 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 사용자 경험상 14s 단일 요청은 명백히 비정상이지만, 본 cluster는 단일 발생이고 응답은 성공이다. 즉시 코드 변경 없이, 동시간대의 service-wide latency 인시던트 (2026-06-26-svc-cupixworks-api--unknown-4) 해소를 우선 확인할 것 — 인시던트 페이지 링크: <DOCS_SITE_URL>/status/2026-06-26-svc-cupixworks-api--unknown-4.
  • 재발 모니터링: Api::V1::Admin::EditingsController#update p95/p99 alert (아래 Monitoring 참조).

단기 개선 (1주 이내)#

  • app/models/concerns/refinementable.rb:55-57 before_transition any => :queuedrun_capture_refinement를 비동기 worker로 이관. 현재 CreateCaptureRefinementJob#run (app/jobs/create_capture_refinement_job.rb:28-34) 은 동기 execute (AWS Lambda Invoke) 를 수행하므로, Sidekiq worker로 enqueue만 하고 실제 invoke는 worker에서 실행하는 구조로 전환. Lambda 호출 자체는 1ms로 빠르지만, payload 준비/session 조회 등 부수 작업이 요청 경로에 남아있어 분리가 합리적이다.
  • app/models/concerns/statable/editing.rb:141-153 FORCE_APPLY_ENTITIES_STATUSESediting_entities.untrashed.each 루프를 entity 수에 비례한 비용으로 분석 — entity 수가 많은 editing에서는 응답 시간이 entity 수에 선형 증가. 필요 시 bulk update 또는 비동기 분리 검토.
  • app/models/concerns/statable/editing.rb:172-174 after_transition ... model.start_sitetrack 는 가드 분기 (editing_type != 'normal' 등) 가 모두 동기 로깅을 수반한다. refinement 케이스는 곧바로 빠져나오므로 비용은 작지만, 향후 가드 추가 시 비동기화 고려.

장기 개선 (재발 방지)#

  • State machine after_transition 으로 외부 시스템(Lambda, SQS, ES 인덱싱) 을 직접 호출하는 패턴 일반화 — 모든 외부 호출은 callback에서 worker enqueue로 통일하는 가이드라인 수립 및 점진적 적용.
  • Admin 엔드포인트 SLO 정의 (예: p95 ≤ 2s, p99 ≤ 5s) 와 위반 시 자동 alert.
  • service-wide latency 인시던트(svc:cupixworks-api::unknown) 의 근본 원인 추적 (별도 RCA) — 본 cluster는 그 인시던트의 한 단면일 가능성이 높다.

Monitoring#

  • p95/p99 latency timeseries (resource 기준):
text
avg:trace.rack.request.duration.by.resource_service.95p{service:cupixworks-api,resource_name:api::v1::admin::editingscontroller#update}
  • 5초 초과 update 발생 빈도:
text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api::v1::admin::editingscontroller#update,@duration:>5000}.as_count()
  • refinement Lambda 호출 시간 (worker 측 이관 후):
text
avg:trace.aws.lambda.invoke.duration{function_name:cupix-refinement-transform-production}
  • 서비스 전반 5xx + 느린 요청:
text
sum:trace.rack.request.errors{service:cupixworks-api}.as_count()

Risk Assessment#

  • Risk level: low (occurrence_count=1, status 200, 직접 사용자 영향 1명, ongoing 광역 latency 인시던트의 한 단면)
  • 예상 복잡도: standard (즉시 코드 변경 없음 + 단기 개선은 run_capture_refinement 비동기화 1건이 핵심)