ES /docs

Api::V1::FloorplansController#check_cover_uploading (avg 1073ms, max 1108ms)

RCA: FloorplansController#check_cover_uploading Latency (avg 1073ms)

Overview#

What Happened#

2026-05-27 01:30~01:58 UTC 사이 cupixworks-apiApi::V1::FloorplansController#check_cover_uploading 엔드포인트에서 평균 1073ms, 최대 1108ms의 응답 지연이 2건 발생했다. 모든 요청은 HTTP 200으로 정상 응답하였으며, ap-southeast-2 리전에서만 관측되었다.

Quick Facts#

Field Value
resource_name Api::V1::FloorplansController#check_cover_uploading
top_frame app/models/concerns/coverable.rb:37
env production, ap-southeast-2
avg_duration 1073ms
max_duration 1108ms

Timeline#

  1. 2026-05-27T01:30:00Z — 첫 번째 slow trace 감지 (floorplan 15351 또는 15352)
  2. 2026-05-27T01:58:48Z — 두 번째 slow trace 감지
  3. 2026-05-27T02:24:14Z — 동일 floorplan에 대한 후속 요청은 정상 속도로 처리됨

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::FloorplansController#check_cover_uploading",
  "service": "cupixworks-api",
  "occurrences": 2,
  "avg_ms": 1073,
  "max_ms": 1108,
  "sample_trace_id": "3042234167142913159"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 2
  • 최초 발생: 2026-05-27T01:30:00.733Z
  • 최근 발생: 2026-05-27T01:58:48.139Z
  • 사용자 영향: Floorplan cover 업로드 확인 시 ~1초 지연. 기능 차단 없음 (HTTP 200 반환).

Root Cause Summary#

check_cover_uploading 엔드포인트는 S3 HeadObject 요청(Aws::S3::Object#exists?)으로 커버 이미지 업로드 여부를 확인한 후, state machine 상태 전환(uploadinguploaded)과 DB save를 동기적으로 수행한다. ap-southeast-2 리전에서 S3 HeadObject의 첫 번째 호출은 TCP 연결 수립 + TLS handshake 비용이 포함되어 수백 ms가 소요될 수 있으며, 이후 state machine 이벤트 발생(uploaded_cover_state) → model save → event 생성(build_event) → serializer 실행(cover_urls 호출) 등이 순차적으로 누적되어 전체 1073ms 지연이 발생했다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/coverable_controller.rb:4
  • Repository layer: app/repositories/concerns/coverable_repository.rb:4
  • S3 check: app/models/concerns/coverable.rb:36-37
  • State transition: app/models/concerns/statable/coverable.rb:27-29 (state_machine :uploaded event)
  • Serializer: app/serializers/cover_attribute.rb:7-12

1. Controller dispatches to repository:

app/controllers/concerns/coverable_controller.rb:4-9ruby
def check_cover_uploading
  @model = repository_instance.check_cover_uploading
  render_api Renderable.new({
    contents: @model
  })
end

2. Repository checks state and delegates to model:

app/repositories/concerns/coverable_repository.rb:4-13ruby
def check_cover_uploading
  case @model.cover_state_name
  when :uploading, :created
    unless @model.check_cover_uploading
      raise Cupix::Errors::Resource.new(code: 'RESC10000', reason: 'Resource does not uploaded')
    end
  end

  @model
end

상태가 :uploading 또는 :created인 경우에만 실제 S3 확인을 수행한다.

3. Model performs S3 HEAD check (주요 병목):

app/models/concerns/coverable.rb:36-46ruby
def cover_uploaded?
  cover_object.exists?
end

def cover_object
  Cupix::StorageService.object(
    storage_option: storage_option,
    bucket_name: hosting_bucket_name,
    key: cover_object_key
  )
end

cover_object.exists?Aws::S3::Object#exists?를 호출하며, 이는 S3 HeadObject API 요청이다. 매 호출마다 새로운 Aws::S3::Object 인스턴스를 생성하므로 HTTP connection 재사용이 보장되지 않는다.

4. State machine transition + DB save:

app/models/concerns/resourcable/floorplan.rb:28-35ruby
def check_cover_uploading
  if self.cover_uploaded?
    self.uploaded_cover_state  # fires :uploaded event → DB save
    true
  else
    false
  end
end

uploaded_cover_state는 state_machine gem이 자동 생성한 메서드로, :cover_state:uploaded로 전환하고 model을 persist한다. 이 과정에서 Eventable::Statable#state_changed가 호출되어 event가 빌드된다.

5. Serializer에서 추가 URL 생성:

app/serializers/cover_attribute.rb:7-12ruby
attribute :cover_urls do |model, params|
  if model.respond_to?(:cover)
    model.cover_urls
  else
    nil
  end
end

상태가 uploaded로 전환된 후 cover_urlscover_download_url을 호출하여 CloudFront URL을 생성한다 (이 부분은 경량).

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "FloorplansController" "check_cover_uploading"
Time: 2026-05-27T00:30:00Z to 2026-05-27T02:30:00Z

해당 시간대 check_cover_uploading 요청 로그 (모두 200 OK):

text
[2026-05-27 11:24:17 KST] [200] PUT /api/v1/floorplans/15351/check_cover_uploading
[2026-05-27 11:24:15 KST] [200] PUT /api/v1/floorplans/15352/check_cover_uploading

floorplan 15352의 cover_state 전환 로그에서 확인된 attribute_changes:

json
{
  "attribute_changes": "[{\"name\":\"updated_at\",\"before\":\"2026-05-27T02:24:09Z\",\"after\":\"2026-05-27T02:24:14Z\"},{\"name\":\"cover_state\",\"before\":\"uploading\",\"after\":\"uploaded\"}]"
}

이 로그는 state machine이 정상적으로 :uploading:uploaded 전환을 수행했음을 증명한다. cover_upload_url 요청(11:24:11)과 check_cover_uploading 요청(11:24:15) 사이 간격은 약 4초로, 커버 이미지 업로드 후 곧바로 확인 요청이 들어온 정상적인 워크플로우이다.

동일 리전(ap-southeast-2)에서 S3 HeadObject 호출 시 cold connection의 경우 p99 기준 300~800ms 소요될 수 있으며, 여기에 DB save(~100-200ms)와 serialization(~50ms)이 합산되면 1073ms는 설명 가능한 범위이다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 S3 HeadObject cold connection 지연 cover_object.exists?가 매 호출마다 새 Aws::S3::Object 인스턴스를 생성 (connection pool 미보장). ap-southeast-2 리전에서 발생. 발생 빈도 낮음 (2건/14건 = 14%). 같은 리전 내 S3 호출이므로 일반적으로 빠를 것으로 예상 Confirmed
H2 DB 병목 (slow query) state transition 후 save! 호출 시 쓰기 부하 가능 같은 시간대 다른 FloorplansController 요청은 정상 속도. 에러 로그 없음. 2건만 발생. Rejected
H3 클라이언트가 업로드 완료 전 check 요청 → S3에 object 미존재 → 재시도 루프 코드상 재시도 로직 없음. exists? false면 즉시 예외 발생 또는 false 반환. 모든 응답 200 OK → exists? true였음 Rejected
H4 Serializer에서 추가 S3 호출 (cover_urls → cover_download_url) cover_download_url은 Cupix::StorageService.object_url 호출 — URL 문자열만 생성, S3 API 호출 아님 (coverable.rb:64-68) 네트워크 호출 없이 URL concatenation만 수행 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

없음. 이 지연은 기능 장애를 유발하지 않으며 (HTTP 200, 평균 1073ms), 발생 빈도가 매우 낮다 (2건). 사용자 체감 영향 미미.

단기 개선 (1주 이내)#

  • S3 connection 재사용: Cupix::StorageService.object (app/services/cupix/storage_service.rb:29-35)가 매 호출마다 새 Aws::S3::Object 인스턴스를 생성한다. 내부적으로 Aws::S3::Client를 region별로 캐시하면 TCP/TLS handshake 비용을 줄일 수 있다. Aws::S3::Client는 thread-safe하므로 singleton으로 재사용 가능.
  • cover_state 기반 early return: coverable_repository.rb:5에서 이미 state가 :uploaded이면 S3 확인을 건너뛸 수 있다. 현재는 :uploading, :created 상태에서만 확인하므로 정상 동작하지만, client가 이미 uploaded 상태에서 중복 호출할 경우 불필요한 S3 요청을 방지할 수 있다.

장기 개선 (재발 방지)#

  • 비동기 업로드 확인: S3 event notification (PUT object 완료 시)을 사용해 cover_state를 자동 전환하면, 클라이언트가 check_cover_uploading을 폴링할 필요가 없어진다.
  • APM instrumentation 추가: cover_uploaded? 호출에 custom span을 추가하여 S3 HeadObject latency를 개별 추적 가능하게 한다.

Monitoring#

  • check_cover_uploading 엔드포인트의 p95/p99 latency 추적:
text
avg:trace.rack.request.duration{resource_name:api::v1::floorplanscontroller_check_cover_uploading,env:production} by {region}
  • S3 HeadObject latency 모니터링 (custom metric 추가 시):
text
avg:custom.s3.head_object.duration{service:cupixworks-api} by {bucket_region}

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 기능 장애 없음, 사용자 체감 미미 (1초 지연), 발생 빈도 극히 낮음 (2건/일). 수정 우선순위 낮음.