ES /docs

Api::V1::FloorplansController#complete_creation (avg 14962ms, max 14962ms)

RCA: Api::V1::FloorplansController#complete_creation (avg 14962ms, max 14962ms)

Overview#

What Happened#

2026-07-08 21:30 KST 에 cupixworks-api production (us-west-2) 에서 Api::V1::FloorplansController#complete_creation 요청이 14.96초간 지속되었다. 대상 리소스는 team.domain=exyte의 floorplan id 90746 이며, HTTP 상태 200으로 정상 응답했으나 응답 지연이 임계치를 초과해 latency 클러스터로 감지되었다.

Quick Facts#

Field Value
resource_name Api::V1::FloorplansController#complete_creation
avg_duration_ms 14962
max_duration_ms 14962
top_frame app/repositories/floorplan_repository.rb:239
env production, us-west-2
deploy production-us-west-2-20260708t0843z0-5c918141-cupixworks
tenant cupix
team.domain exyte

Affected Teams#

Team / Domain Error Count Impact
exyte (team.id=783) 1 단일 사용자(id=39791)의 BIM floorplan 업로드 완료 처리가 15초 지연 — 사용자 대기 및 클라이언트 타임아웃 가능

Timeline#

  1. 2026-07-08 21:29:53 KST — Floorplan 90745 complete_creation 완료 (2669ms) — 같은 세션에서 연쇄 요청 시작.
  2. 2026-07-08 21:30:01 KST — Floorplan 90746 check_uploadingupdate 호출 (정상).
  3. 2026-07-08 21:30:03 KST — Floorplan 90746 update_meta_by_key 완료.
  4. 2026-07-08 21:30:16 KST — Floorplan 90746 complete_creation 응답 (200), 총 14715ms 소요 (Datadog request log 기준).
  5. 2026-07-08 21:30:47 KST — Floorplan 90747 complete_creation 완료 (4620ms) — 지연 회복 조짐 없음, 동일 사용자의 후속 요청도 4초 이상 소요.

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::FloorplansController#complete_creation",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 14962,
  "max_ms": 14962,
  "sample_trace_id": "99484605683438107"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-08 21:30 KST
  • 최근 발생: 2026-07-08 21:30 KST

Root Cause Summary#

FloorplansController#complete_creationFloorplanRepository#complete_creationmodel.complete_creating_state! 를 호출하고, state_machinecomplete_creating 이벤트는 리소스 상태에 따라 resource_uploaded 또는 done 으로 전이한다. 이 전이의 after_transition 콜백이 요청 스레드에서 동기적으로 외부 시스템 호출을 여러 개 수행한다: update_thumbnail (SQS send_message), translate_model (SQS send_message), 그리고 bim_floorplan? 인 경우 즉시 done_state 를 발화시켜 notify_floorplan_done (DB create_notification_history + Cupix::Mailer::FacilityMailer.update_on_facility + Analytics.track) 을 실행한다. 문제의 요청은 총 14715ms 중 DB 시간은 2327ms 에 불과하고 나머지 ~12.4초가 콜백 체인 내부의 외부 호출(SQS/mailer/analytics)에 사용된 것으로 관측된다. 즉, HTTP 요청 처리 중 다수의 외부 I/O 를 직렬로 대기하는 구조가 근본 원인이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/floorplans_controller.rb:59
  • Repository dispatch: app/repositories/floorplan_repository.rb:239
  • State machine: app/models/concerns/statable/floorplan.rb:70 (event :complete_creating)
  • Callback 1 (resource_uploaded → thumbnail SQS): app/models/concerns/statable/floorplan.rb:11app/models/concerns/thumbnailable/agent.rb:30
  • Callback 2 (resource_uploaded → floorplan agent SQS): app/models/concerns/translatable/callbacks/floorplan.rb:10app/models/concerns/translatable/floorplan.rb:27
  • Callback 3 (bim → done_state → mailer+analytics): app/models/concerns/translatable/callbacks/floorplan.rb:11, app/models/concerns/notifiable/floorplan.rb:8

컨트롤러는 얇지만, 모델 콜백 체인이 요청 스레드에서 다수의 외부 I/O 를 순차 수행한다:

app/controllers/api/v1/floorplans_controller.rb:59-64ruby
def complete_creation
  @model = repository_instance.complete_creation
  render_api Renderable.new({
    contents: @model
  })
end
app/repositories/floorplan_repository.rb:239-242ruby
def complete_creation
  self.model.complete_creating_state!
  self.model
end
app/models/concerns/statable/floorplan.rb:70-76ruby
event :complete_creating do
  transition creating: :done, if: ->(floorplan) { floorplan.resource_state_uploaded? && floorplan.tile_state_uploaded? }
  transition creating: :created, if: :resource_state_created?
  transition creating: :resource_uploaded, if: :resource_state_uploaded?
  transition creating: :resource_uploading, if: :resource_state_uploading?
  transition creating: :resource_missing, if: :resource_state_missing?
end
app/models/concerns/statable/floorplan.rb:11ruby
after_resource_uploaded_state :update_thumbnail
app/models/concerns/thumbnailable/agent.rb:30-38ruby
def update_thumbnail!(validate: true)
  return if self.respond_to?(:soft_copied?) && self.soft_copied?

  check_thumbnail_updatable if validate
  session = self.user.agent_team_session(self.team)
  client = Cws::ThumbnailAgent::Client.new(model: self, session: session)
  update(thumbnail_update_queued_at: DateTime.now)
  client.invoke!
end
app/models/concerns/translatable/callbacks/floorplan.rb:6-12ruby
included do
  include ::Translatable::Callbacks

  after_create :translate_model, if: :translatable?
  after_resource_uploaded_state :translate_model, if: :translatable?
  after_resource_uploaded_state :done_state, if: :bim_floorplan?
end
app/models/concerns/translatable/floorplan.rb:27-32ruby
def translate_model!(validate: true)
  session = self.user.agent_team_session(self.team)
  client = Cws::FloorplanAgent::Client.new(model: self, session: session)
  update(floorplan_translate_queued_at: DateTime.now)
  client.invoke!
end
app/models/concerns/notifiable/floorplan.rb:8-37ruby
after_done_state :notify_floorplan_done

def notify_floorplan_done
  return unless floorplan_done_notifiable?

  creation_from = self.meta['prop']['bimId'] rescue nil
  reason = creation_from.present? ? BIM360_SHEET_DONE : FLOORPLAN_DONE

  self.create_notification_history(reason)
  Cupix::Mailer::FacilityMailer.update_on_facility(self, reason)
  Analytics.track(
    user_id: user.crn,
    event: 'BE_FLOORPLAN_DONE',
    properties: { ... }
  )
end

기대 동작: 상태 전이 후 부수적인 알림/썸네일/번역 요청은 백그라운드 작업으로 위임되어 HTTP 응답은 sub-second 수준이어야 한다.

실제 동작: 하나의 요청 스레드가 sqs_client.send_message (썸네일), sqs_client.send_message (floorplan-agent), 그리고 (bim_floorplan? 이 false 인 image/pdf 계열은) create_notification_history INSERT, Cupix::Mailer::FacilityMailer.update_on_facility (SES/SMTP), Analytics.track (Segment/Analytics HTTP) 를 순차 호출한다. 개별 호출이 수백ms만 소요돼도 합쳐서 10초를 초과할 수 있다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "complete_creation"
text
service:cupixworks-api "90746"

같은 사용자 세션의 complete_creation 요청 3건이 모두 정상 응답이지만 소요 시간이 크게 차이난다 — 90746 이 14.7초로 유독 길다:

json
{
  "@timestamp": "2026-07-08T12:29:53.884Z",
  "path": "/api/v1/floorplans/90745/complete_creation",
  "http.status_code": 200,
  "duration": 2669.91,
  "db": 1086.33
}
json
{
  "@timestamp": "2026-07-08T12:30:16.988Z",
  "path": "/api/v1/floorplans/90746/complete_creation",
  "http.status_code": 200,
  "duration": 14715.01,
  "db": 2327.48,
  "user": { "id": 39791, "email": "piamaria.nehme@cupix.com" },
  "team": { "id": 783, "domain": "exyte" }
}
json
{
  "@timestamp": "2026-07-08T12:30:47.033Z",
  "path": "/api/v1/floorplans/90747/complete_creation",
  "http.status_code": 200,
  "duration": 4620.4,
  "db": 1642.33
}

Floorplan 90746 요청의 브레이크다운 — 총 14715ms 중 DB 는 2327ms (16%). 나머지 ~12388ms 는 non-DB 구간(외부 I/O). sample_trace_id: 99484605683438107 이 이 요청의 APM trace 이며 (14962ms 은 APM 스팬, 14715ms 는 Rails request 로그 — 프레임워크 미들웨어 계층에서 소량의 차이 발생).

같은 floorplan 의 선행 요청도 정상적으로 200 응답:

text
21:30:01 KST — PUT /api/v1/floorplans/90746/check_uploading  (200)
21:30:01 KST — PUT /api/v1/floorplans/90746                   (200, update)
21:30:03 KST — PUT /api/v1/floorplans/90746/meta/prop         (200, update_meta_by_key)
21:30:16 KST — PUT /api/v1/floorplans/90746/complete_creation (200, 14715ms)
  • 형제 요청(90747, sibling naming SGOPS-S-DS1-00-BLDG-001.rvt) 도 4.6초로 이미 정상 임계치 초과.
  • Status board 상 동일 시간대 활성 인시던트 없음 — 개별 요청의 콜백 체인 소요가 원인.

status board 확인:

text
scope: svc:cupixworks-api::unknown
active: null
recent: (last 7d, 4 resolved incidents on same scope)

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 complete_creation 상태 전이 콜백(SQS thumbnail + SQS floorplan-agent + 이메일/analytics) 이 요청 스레드에서 동기 실행되어 외부 I/O 대기가 누적됨 request 로그의 duration=14715ms 대비 db=2327ms 로 non-DB 구간이 12.4초. code path 상 after_resource_uploaded_state :update_thumbnail, after_resource_uploaded_state :translate_model, after_done_state :notify_floorplan_done 모두 요청 스레드에서 실행됨 (app/models/concerns/statable/floorplan.rb:11, app/models/concerns/translatable/callbacks/floorplan.rb:10-11, app/models/concerns/notifiable/floorplan.rb:8) Confirmed
H2 DB 슬로우 쿼리(느린 SELECT/UPDATE) 가 원인 db=2327.48ms 로 다른 정상 요청(90745: db=1086, 90747: db=1642) 보다 다소 높음 총 소요 14715ms 중 DB 비중은 16%. 나머지 12.4초를 설명하지 못함 Rejected
H3 외부 의존성(AWS/Cognito/S3) 리전 광역 장애 동일 시간대 team=exyte 의 90745(2.6s), 90747(4.6s) 도 임계치 초근접 Status board active: null, 동일 시간대 다른 대량 에러 클러스터 없음. 여러 floorplan 이 아닌 특정 요청만 15초 도달 Rejected
H4 사용자 세션 획득(self.user.agent_team_session(self.team)) 이 Cognito 호출을 두 번(썸네일용/agent용) 유발 update_thumbnail! (thumbnailable/agent.rb:34) 과 translate_model! (translatable/floorplan.rb:28) 이 각각 agent_team_session 호출 — Cognito latency 가 두 번 누적 가능 세션이 캐시되는지 여부는 agent_team_session 구현을 추가 확인 필요 Inconclusive — needs verification

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 200 응답이며 데이터 손실이나 정합성 문제는 없다. 그러나 사용자 UX(클라이언트 타임아웃 위험, 특히 15초는 ALB/CloudFront idle timeout 근처) 개선을 위해 아래 단기 개선 필수.

단기 개선 (1주 이내)#

  • 콜백을 Sidekiq 로 위임: app/models/concerns/statable/floorplan.rb:11after_resource_uploaded_state :update_thumbnailperform_async 계열 워커로 감싸는 얇은 wrapper 로 교체. 팀 컨벤션상 Thumbnailable::Agent#update_thumbnail 이 이미 rescue StandardError; false 이므로 실패 격리는 유지되나, SQS 호출은 워커로 이동해 요청 응답에서 제외.
  • Cupix::Mailer::FacilityMailer.update_on_facilitydeliver_later 로 전환: app/models/concerns/notifiable/floorplan.rb:19. Rails ActiveJob 큐를 이용하여 HTTP 요청 스레드 밖으로 이메일 발송 이동.
  • Analytics.track 을 워커로 이동: app/models/concerns/notifiable/floorplan.rb:21. Segment/analytics 서비스 latency 가 요청 응답에 영향을 주지 않도록 격리.
  • 세션 캐싱: self.user.agent_team_session(self.team) 이 콜백 두 번(썸네일/agent) 호출된다면 요청 스코프에서 memoize. 우선 user.agent_team_session 구현을 확인해 이미 캐싱되어 있는지 검증 필요.

장기 개선 (재발 방지)#

  • 상태 전이 콜백 정책 문서화 — after_*_state 훅에서는 외부 I/O 를 직접 호출하지 않고 Sidekiq/ActiveJob 위임만 허용하도록 규칙화. 리뷰 checklist 에 반영.
  • APM 기반 slow-endpoint 상시 알림 도입 (하단 Monitoring 참조) — 임계치 5초 초과 시 알림.
  • complete_creation 엔드포인트 응답을 202 Accepted + 비동기 상태 폴링 패턴으로 재설계 검토. 완료 판정은 별도 상태 조회로 이관하면 초기 응답을 sub-second 로 낮출 수 있다.

Monitoring#

  • p95 latency 및 초과 발생 카운트를 추적한다. 아래 쿼리는 release dashboard timeseries widget 에 그대로 사용 가능.

95th percentile 응답 시간 (초 단위):

text
p95:trace.rack.request{service:cupixworks-api,resource_name:Api::V1::FloorplansController#complete_creation}

5초 초과 요청 발생률:

text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:Api::V1::FloorplansController#complete_creation,duration:>5s}.as_count()

동일 controller#action 의 에러율(대조군):

text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:Api::V1::FloorplansController#complete_creation}.as_count()
  • 임계 제안: p95 > 3000ms, 또는 5초 초과 카운트가 5분 창에서 3회 이상이면 알림.

Risk Assessment#

  • Risk level: medium — 200 응답이라 실패는 아니나 15초 지연은 클라이언트 타임아웃 및 UX 저하 유발. 동일 사용자의 다른 요청도 4~5초 근접해 확산 조짐.
  • 예상 복잡도: standard — 콜백을 워커/ActiveJob 로 옮기는 국지적 변경. 다만 update_thumbnail 등이 다른 모델(Attachment/Asset)에서도 재사용되므로 회귀 테스트 필수.