ES /docs

Api::V1::FacilitiesController#update (avg 1055ms, max 1055ms)

RCA: FacilitiesController#update Latency (1055ms)

Overview#

What Happened#

2026-05-27 07:11:39 UTC, cupixworks-api 서비스의 Api::V1::FacilitiesController#update 요청이 1055ms 소요되었다. 일반적인 facility update 응답 시간(80-135ms) 대비 약 8배 느린 응답이다. DB 시간은 24ms에 불과했으며, 나머지 ~1028ms는 before_commit 콜백에서 발생한 Google Maps Timezone API 동기 호출과 after_commit에서의 Google Static Map 다운로드에 소비되었다.

Quick Facts#

Field Value
resource_name Api::V1::FacilitiesController#update
top_frame app/models/concerns/geo_queriable/facility.rb:12
env production, us-west-2
duration 1055ms (DB: 24ms, external HTTP: ~1028ms)
facility oqlhj2

Timeline#

  1. 07:09:49 — Facility oqlhj2 생성 (create)
  2. 07:10:39 — 첫 번째 update (110ms, 정상 — name 변경만)
  3. 07:11:17 — 두 번째 update (474ms — siteinsights_version, DB heavy 395ms)
  4. 07:11:39 — 세 번째 update (1055ms — location 변경으로 timezone API + thumbnail 다운로드 발생)
  5. 07:11:39update_timezoneset_location_based_timezone! 콜백 실행 (Google Timezone API 호출)

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::FacilitiesController#update",
  "service": "cupixworks-api",
  "occurrences": 1,
  "avg_ms": 1055,
  "max_ms": 1055,
  "sample_trace_id": "149094867954276496"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-05-27T07:11:38.193Z
  • 최근 발생: 2026-05-27T07:11:38.193Z

단일 사용자(yohan.kim@cupix.com)의 facility 생성 후 location 설정 과정에서 발생. 기능적 실패는 아니며 응답 지연만 해당. 그러나 location이 포함된 모든 facility update 요청에서 동일하게 발생할 수 있는 구조적 문제.

Root Cause Summary#

Facility의 location 필드가 변경될 때 before_commit :update_timezone 콜백이 동기적으로 Google Maps Timezone API를 호출하여 timezone을 조회한다. 이 외부 HTTP 호출이 요청 사이클 내에서 ~1초를 차지했다. 추가로 after_commit :change_thumbnail_job!이 Google Static Map 이미지를 동기 다운로드한 후 self.save를 호출하여 추가 지연을 유발한다. 두 콜백 모두 외부 API 호출을 request cycle 내에서 동기적으로 수행하는 것이 근본 원인이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/facilities_controller.rb:111
  • Repository update: app/repositories/facility_repository.rb:164 (@model.save!)
  • Before commit 콜백: app/models/concerns/geo_queriable/facility.rb:8
  • Timezone lookup: app/models/concerns/has_timezone.rb:10
  • After commit 콜백: app/models/concerns/thumbnailable/facility.rb:62

1. Controller → Repository → save!

app/controllers/api/v1/facilities_controller.rb:111-115ruby
def update
  @model = repository_instance.update(params)

  super
end
app/repositories/facility_repository.rb:160-168ruby
begin
  @model.save!
rescue StandardError => e
  raise Cupix::Errors::Parameter.new(
    code: 'ARG10000',
    reason: "update failed. #{e.message}"
  )
end

2. before_commit 콜백에서 timezone API 동기 호출

save! 호출 시 location이 변경되었으면 before_commit :update_timezone이 트리거된다:

app/models/concerns/geo_queriable/facility.rb:8-17ruby
before_commit :update_timezone, on: :update

private

def update_timezone
  return unless location_changed?

  Cupix::Logger.info("update timezone on facility #{self.key}.", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key, saved_changes: saved_changes })
  self.set_location_based_timezone
end

set_location_based_timezoneupdate!를 포함하며, 내부에서 set_location_based_timezone!를 호출:

app/models/concerns/has_timezone.rb:7-14ruby
def set_location_based_timezone!
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'Location not set') if location.blank?

  timezone = Timezone.lookup(latitude, longitude)
  self.timezone_name = timezone.name
  self.timezone_offset = timezone.utc_offset

  Cupix::Logger.info("Set location based timezone on facility #{self.key}. timezone_offset: #{timezone.utc_offset} timezone_name: #{timezone.name}", class: self.class.name, function: __method__, facility: { id: self.id, key: self.key })
end

Timezone.lookuptimezone gem (v1.0)을 사용하며, Google Maps Timezone API에 HTTP 요청을 보낸다:

config/initializers/timezone.rb:1-5ruby
GOOGLE_API_KEY = ENV['GOOGLE_API_KEY'] || 'AIzaSyACNIOJof_WCGjD8Ol4gNkyHnd27_7EKMI'

Timezone::Lookup.config(:google) do |c|
  c.api_key = GOOGLE_API_KEY
end

이 API 호출은 request transaction 내에서 동기적으로 수행되며, 네트워크 왕복 시간(~500-1000ms)이 그대로 응답 시간에 반영된다.

3. after_commit에서 Google Static Map 동기 다운로드

location 변경 시 after_commit :change_thumbnail_job!도 트리거된다:

app/models/concerns/thumbnailable/facility.rb:62-75ruby
def change_thumbnail_job!
  return if self.google_static_map_url.nil?

  begin
    Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 1} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
    self.thumbnail = download_to_file(self.google_static_map_url)
    Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 2} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
    self.save
    Cupix::Logger.info("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - 3} #{Digest::SHA1.hexdigest(self.thumbnail.to_json)}")
  rescue StandardError => e
    Cupix::Logger.error("[Thumbnail][Facility] (#{id}) {change_thumbnail_job! - failed} : #{e.message}")
    raise e
  end
end

download_to_file이 Google Static Map 이미지를 동기적으로 다운로드하고, self.save가 추가 save cycle(ES indexing 포함)을 유발한다. after_commit은 transaction이 commit된 후 실행되지만 여전히 request cycle 내에서 동기적으로 실행된다.

Log Evidence#

Datadog에서 확인된 로그:

text
service:cupixworks-api @function:update_timezone
Time range: 2026-05-27 06:00 - 08:00 UTC
json
{
  "timestamp": "2026-05-27T07:11:39.946Z",
  "message": "update timezone on facility oqlhj2.",
  "class": "Facility",
  "function": "update_timezone"
}
json
{
  "timestamp": "2026-05-27T07:11:39.946Z",
  "message": "Set location based timezone on facility oqlhj2. timezone_offset: 32400 timezone_name: Asia/Seoul",
  "class": "Facility",
  "function": "set_location_based_timezone!"
}

동일 시간대의 다른 facility update 요청과 비교:

text
service:cupixworks-api "FacilitiesController#update"
Time range: 2026-05-27 06:00 - 08:00 UTC
Timestamp Facility Duration DB Time Notes
07:11:39 oqlhj2 1053ms 24ms timezone + thumbnail (location 변경)
07:11:17 oqlhj2 474ms 395ms siteinsights_version (DB heavy)
07:31:12 4yreuj 343ms 139ms flush_cached_facility_size
07:10:39 oqlhj2 110ms 21ms name만 변경 (정상)
07:35:02 jcvm02 123ms 21ms name만 변경 (정상)

Location 변경이 없는 일반 update는 80-135ms로 정상적이며, location 변경이 포함된 요청에서만 ~1초 지연이 발생함을 확인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Google Timezone API 동기 호출이 지연 원인 before_commit :update_timezone 콜백에서 Timezone.lookup 호출 확인 (geo_queriable/facility.rb:8), DB 시간 24ms로 외부 호출이 대부분의 시간 차지, timezone 로그 동일 타임스탬프 확인 Confirmed
H2 DB 쿼리 N+1 또는 slow query 문제 DB 시간 24ms로 매우 짧음. 동일 facility의 이전 update(110ms)도 정상 Rejected
H3 Elasticsearch 동기 indexing 지연 after_commit :_update_document가 동기적으로 실행됨 (searchable.rb:55) 다른 facility update도 동일한 ES indexing을 거치지만 110-135ms로 정상. ES가 원인이라면 모든 요청에서 지연 발생해야 함 Rejected
H4 Google Static Map 다운로드 추가 지연 after_commit :change_thumbnail_job!에서 download_to_file 동기 호출 (thumbnailable/facility.rb:67), location 변경 시에만 트리거 이 콜백은 after_commit이므로 APM trace duration에 포함되는지 불확실 — trace가 commit 이후에도 측정한다면 기여 Likely contributing

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/geo_queriable/facility.rb:12-16: update_timezone 콜백에서 동기 호출을 제거하고 background worker로 이동
  • 접근 방식: before_commit에서 직접 set_location_based_timezone을 호출하는 대신, after_commit에서 TimezoneUpdateWorker.perform_async(self.id)를 호출하여 비동기로 처리

단기 개선 (1주 이내)#

  • app/models/concerns/thumbnailable/facility.rb:62-75: change_thumbnail_job!이 이미 ThumbnailChangeWorker가 존재함에도 동기적으로 이미지를 다운로드하고 있음. change_thumbnail 메서드(line 17-19)처럼 ThumbnailChangeWorker::Facility.perform_async(self.id)로 대체하여 비동기 처리
  • timezone lookup 결과 캐싱: 동일 좌표에 대한 반복 API 호출 방지를 위해 Redis 캐시 추가 고려

장기 개선 (재발 방지)#

  • before_commit / after_commit 콜백에서 외부 HTTP 호출을 금지하는 코드 컨벤션 수립
  • Location 변경 시 트리거되는 모든 동기 외부 호출을 audit: timezone lookup, static map download, geocoding 등을 모두 background job으로 이관
  • 외부 API 호출에 대한 timeout 설정 확인 (timezone gem의 기본 timeout은 무제한)

Monitoring#

  • Facility update 응답 시간 모니터링:
text
service:cupixworks-api resource_name:"Api::V1::FacilitiesController#update" @duration:>500ms
  • Timezone API 호출 지연 추적:
text
service:cupixworks-api @function:set_location_based_timezone!
  • p95/p99 latency alert 설정: facility update endpoint에 500ms 초과 시 알림

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 발생 빈도가 낮고(location 변경 시에만), 기능적 실패가 아닌 응답 지연이므로 위험도는 낮음. 다만 구조적으로 모든 location 변경 요청에 영향을 주므로 수정이 바람직함.