ES /docs

Api::V1::ElementsController#revise (avg 1257ms, max 2828ms)

RCA: ElementsController#revise Latency (avg 1257ms, max 2828ms)

Overview#

What Happened#

2026-05-27 06:05~06:15 UTC 사이에 cupixworks-api 서비스의 Api::V1::ElementsController#revise 엔드포인트에서 116건의 요청이 평균 1257ms, 최대 2828ms의 응답 시간을 기록했다. 모든 요청은 HTTP 200으로 정상 응답했으나, 각 요청이 100개의 element를 개별 save 호출로 처리하면서 N+1 write 패턴으로 인한 latency가 발생했다.

Quick Facts#

Field Value
resource_name Api::V1::ElementsController#revise
top_frame app/repositories/concerns/siteinsights_eventable_repository/element.rb:41
env production, us-west-2
avg_duration 1257ms
max_duration 2828ms

Timeline#

  1. 2026-05-27T06:05:57Z — 최초 slow trace 감지 (>500ms threshold)
  2. 2026-05-27T06:05~06:15Z — 116건의 revise 요청 연속 발생 (BIM revision 업데이트 batch operation)
  3. 2026-05-27T06:15:17Z — 마지막 slow trace 기록

Error Log#

Datadog Logs

text
{
  "resource_name": "Api::V1::ElementsController#revise",
  "service": "cupixworks-api",
  "occurrences": 116,
  "avg_ms": 1257,
  "max_ms": 2828,
  "sample_trace_id": "702067061714033657"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 116
  • 최초 발생: 2026-05-27T06:05:57.249Z
  • 최근 발생: 2026-05-27T06:15:16.999Z

Root Cause Summary#

revise 메서드가 요청당 최대 100개의 element를 순차적으로 element.save(validate: false) 호출하며 각 save마다 DB write + after_commit 콜백(PubSub publish, DataWareHouse partial JSON 전송)이 개별 실행된다. 100개 element에 대해 100번의 개별 DB UPDATE + 100번의 after_commit 콜백 체인이 직렬로 실행되어 전체 요청 시간이 1~3초에 달한다. Elasticsearch bulk indexing은 마지막에 한 번만 수행되지만, DB save와 콜백 비용이 지배적이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/concerns/siteinsights_eventable_controller.rb:16
  • Repository dispatch: app/repositories/concerns/siteinsights_eventable_repository/element.rb:7
  • DB query (elements load): element.rb:21-22
  • Hot loop (N+1 save): element.rb:29-61
  • Elasticsearch bulk: element.rb:64-66
app/controllers/concerns/siteinsights_eventable_controller.rb:16-19ruby
def revise
  _revised_ids = repository_instance.revise(params)

  render_json 200, _revised_ids
end
app/repositories/concerns/siteinsights_eventable_repository/element.rb:29-41ruby
bulk_item = elements.map do |element|
  item = id_item_hash[element.id] || ext_id_item_hash[element.bim_external_id]
  next if item.nil?

  # ... logging for each changed field ...

  update_element_attributes(element, item)
  element.skip_index_document!
  element.event_subject = 'ElementBimRevisionUpdated'
  element.save(validate: false)   # <-- 개별 DB write, after_commit 트리거

  # ... build doc hash for ES bulk ...
  doc
end.compact

element.save(validate: false) 호출 시 다음 after_commit 콜백이 실행된다:

app/models/concerns/pub_sub/publisher.rb:37-44ruby
after_save do |model|
  model.pub_sub_notifications_manager.prepare_notifications(namespace, model)
end

after_commit do |model|
  model.pub_sub_notifications_manager.publish_notifications(namespace)
  model.pub_sub_notifications_manager.reset_notifications(namespace)
end
app/models/concerns/data_ware_house/partial_json.rb:6-7ruby
after_commit :save_partial_json_to_file_as_created, on: :create
after_commit :save_partial_json_to_file_as_updated, on: :update, if: :not_new_record?

skip_index_document! 호출로 Elasticsearch 개별 indexing(Searchable after_commit)은 건너뛰지만, PubSub publishDataWareHouse partial JSON 콜백은 여전히 실행된다.

최종적으로 Elasticsearch bulk indexing은 효율적으로 처리된다:

app/repositories/concerns/siteinsights_eventable_repository/element.rb:64-66ruby
if bulk_item.present?
  self.class.current_class.__elasticsearch__.client.bulk(index: self.class.current_class.index_name, body: bulk_item.flat_map { |item| [{ update: { _id: item[:id], data: { doc: item.except(:id) } } }] })
end

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-api "elements are considered to be revised"

대부분의 요청이 100개 element를 처리함을 확인:

json
{
  "timestamp": "2026-05-27 15:15:17",
  "status": "info",
  "message": "100 elements are considered to be revised. element_ids: [16885061, 16885062, ..., 16885160]",
  "class": "Element",
  "function": "revise"
}
json
{
  "timestamp": "2026-05-27 15:15:19",
  "status": "info",
  "message": "40 elements are considered to be revised. element_ids: [16885261, ..., 16885300]",
  "class": "Element",
  "function": "revise"
}

요청 패턴: 2초 간격으로 연속 요청이 발생. 단일 BIM revision 업데이트 작업에서 수천 개의 element를 100개씩 나누어 revise API를 호출하는 클라이언트 패턴으로 확인됨.

컨트롤러 응답 로그 (모두 HTTP 200):

text
service:cupixworks-api "ElementsController" "revise"
json
{
  "timestamp": "2026-05-27 15:15:20",
  "status": "info",
  "message": "[200] PUT /api/v1/elements/revise (Api::V1::ElementsController#revise)"
}

에러/경고 로그 없음:

text
service:cupixworks-api status:error "ElementsController"  → 0 results
service:cupixworks-api status:warn "ElementsController"   → 0 results

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 N+1 개별 save + after_commit 콜백 비용 누적 코드에서 100회 element.save 반복 (element.rb:41), PubSub/DataWareHouse after_commit 콜백 존재 (publisher.rb:41, partial_json.rb:7). 100 elements 처리 시 avg 1257ms는 element당 ~12ms로 DB write + callback overhead와 일치 Confirmed
H2 Elasticsearch bulk indexing 병목 ES bulk는 마지막에 한 번만 수행 (element.rb:64-66) skip_index_document! 호출로 개별 ES indexing은 skip됨. ES bulk는 단일 요청이므로 100개 doc update가 수십ms 내에 완료됨 Rejected
H3 DB 쿼리(element 조회) 시 slow query where(bim_id:, id:, cycle_state:) 쿼리는 인덱스를 탈 것으로 예상 조회 자체는 1회만 실행되고, latency의 대부분은 루프 내 save에서 발생. 100개 id IN 쿼리는 수ms 수준 Rejected
H4 transaction 미사용으로 인한 개별 commit 오버헤드 revise 메서드에 transaction 블록 없음 — 각 save가 독립 transaction으로 commit transaction으로 감싸면 after_commit이 지연되어 일부 개선 가능하나, 근본 원인은 100회 반복 save 자체 Supporting (H1 보완)

Fix Recommendation#

즉시 조치 (Critical)#

  • app/repositories/concerns/siteinsights_eventable_repository/element.rb:29-61 의 개별 save 루프를 update_all 또는 upsert_all로 대체
  • 콜백 실행이 필요한 경우, ActiveRecord::Base.transaction 블록으로 감싸서 commit 횟수를 1회로 줄이기
  • DataWareHouse/PubSub 이벤트는 bulk 완료 후 한 번에 발행하는 방식으로 변경

단기 개선 (1주 이내)#

  • update_all로 DB 업데이트를 단일 SQL로 처리하고, after_commit 콜백은 bulk 처리 후 별도로 트리거
  • import 또는 insert_all/upsert_all (Rails 6+) 활용하여 batch write 수행
  • 현재 skip_index_document!처럼 DataWareHouse/PubSub 콜백도 skip 후, bulk 완료 시점에 일괄 발행하는 패턴 적용

장기 개선 (재발 방지)#

  • BIM revision 업데이트와 같은 대량 element 수정은 Sidekiq worker로 비동기 처리하여 API 응답 시간에서 분리
  • 클라이언트가 1000개 제한까지 한 번에 보내도 응답 시간이 선형 증가하지 않도록 bulk write 패턴을 repository 레벨에서 표준화

Monitoring#

  • APM에서 resource_name:Api::V1::ElementsController#revise p95 latency 알림 추가
  • Datadog 쿼리:
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api::v1::elementscontroller_revise} > 1000

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — 개별 save를 bulk write로 전환하면서 after_commit 콜백 의존 관계를 확인해야 함. 기능 정확성 검증(PubSub event 발행, DataWareHouse sync)이 필요.