ES /docs

ReviewRepository ES timeout — missing fallback for slow queries

RCA: Failed to get records - Operation timed out after 10002 milliseconds with 0 bytes received

Overview#

What Happened#

2026-06-05 04:21 KST에 cupixworks-api 서비스의 ReviewRepository#records 메서드에서 Elasticsearch 쿼리가 10초 timeout으로 실패했다. 동일 시간대에 AerialMapRepository, RecordRepository 등 다수 Repository 클래스에서도 동일한 ES timeout이 발생하여, Elasticsearch 클러스터 수준의 일시적 응답 지연이 원인으로 판단된다.

Quick Facts#

Field Value
exception.class Patron::TimeoutError (via Faraday)
exception.message Operation timed out after 10002 milliseconds with 0 bytes received
top_frame app/repositories/concerns/accessible_entities_repository/review.rb:16
env production, us-west-2

Timeline#

  1. 2026-06-05 04:21:06 KSTAerialMapRepository ES timeout 발생 (첫 번째 timeout)
  2. 2026-06-05 04:21:14 KSTReviewRepository#recordsAerialMapRepository ES timeout 발생
  3. 2026-06-05 04:21:16 KSTRecordRepository ES timeout 발생 (마지막 timeout)
  4. 2026-06-05 04:21:16 KST 이후 — ES 응답 정상 복구 (이후 timeout 로그 없음)

Error Log#

Datadog Logs

text
Failed to get records - Operation timed out after 10002 milliseconds with 0 bytes received

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-06-05 04:21 KST
  • 최근 발생: 2026-06-05 04:21 KST

이 에러 발생 시 해당 Review의 records 조회가 실패하여 API 요청이 500 에러를 반환했을 가능성이 높다. 동일 시간대에 3개 이상의 Repository 클래스에서 timeout이 발생했으므로, 약 10초간 ES 의존 API 전체가 영향을 받았을 것으로 추정된다.

Root Cause Summary#

Elasticsearch 클러스터가 일시적으로 응답 불가 상태에 빠져 Patron HTTP 클라이언트의 10초 timeout(config/initializers/elasticsearch.rb:25)이 트리거되었다. ReviewRepository#records 메서드는 내부적으로 accessible_records를 호출하여 ::Record.search()를 수행하는데, 이 ES 쿼리가 10초 내에 응답을 받지 못하면서 Patron::TimeoutError가 발생했다. 에러 핸들러(rescue => e)가 이를 로깅하고 re-raise하여 최종적으로 API 요청 실패로 이어졌다.

코드상으로 Faraday::ConnectionFailed만 fallback(legacy DB 조회)으로 처리하고 있어, timeout 에러는 fallback 없이 그대로 전파된다.

Technical Analysis#

Code Path#

  • Entry point: app/repositories/concerns/accessible_entities_repository/review.rb:10
  • ES 쿼리 실행: app/repositories/concerns/accessible_entities_repository/review.rb:57
  • Failure point: app/repositories/concerns/accessible_entities_repository/review.rb:15-17 (generic rescue)

ReviewRepository#records 호출 시 accessible_records 메서드가 Elasticsearch에 쿼리를 보낸다:

app/repositories/concerns/accessible_entities_repository/review.rb:10-18ruby
def records(from_at: nil, to_at: nil, visibility: Cyclable.visibility[:UNTRASHED])
  accessible_records(from_at: from_at, to_at: to_at, visibility: visibility)
rescue Faraday::ConnectionFailed
  Cupix::Logger.warn('Failed to get records from ES. Try to get records from legacy.', class: self.class.name, function: __method__)
  accessible_records_legacy(from_at: from_at, to_at: to_at, visibility: visibility)
rescue => e
  Cupix::Logger.error("Failed to get records - #{e.message}", class: self.class.name, function: __method__)
  raise e
end

ES 쿼리 실행 지점:

app/repositories/concerns/accessible_entities_repository/review.rb:57ruby
search_results = ::Record.search(query_option.serializable_hash.merge(size: 10000)).records

Elasticsearch 클라이언트 timeout 설정:

config/initializers/elasticsearch.rb:17-27ruby
Elasticsearch::Model.client = ConnectionPool::Wrapper.new(size: 10, timeout: 7) {
  Elasticsearch::Client.new(
    host: ENV.fetch('RAILS_ES_HOST') { 'localhost' },
    port: ENV.fetch('RAILS_ES_PORT') { DEFAULT_RAILS_ES_PORT },
    user: ENV['RAILS_ES_USER'],
    password: ENV['RAILS_ES_PASSWORD'],
    transport_options: {
      request: {
        timeout: 10
      }
    }
  )

기대 동작: ES가 10초 내에 Record 검색 결과를 반환하고, accessible_records가 결과를 리턴함.

실제 동작: ES가 10초 내에 응답하지 못해 Patron HTTP adapter가 Patron::TimeoutError를 발생시킴. 이 에러는 Faraday::ConnectionFailed가 아니므로 legacy fallback으로 분기하지 않고, generic rescue => e에서 잡혀 에러 로그 후 re-raise됨.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api status:error "Operation timed out"

동일 시간대(04:21:06~04:21:16 KST)에 발생한 timeout 에러들:

json
{"timestamp": "2026-06-05 04:21:06", "status": "error", "message": "Operation timed out after 10002 milliseconds with 0 bytes received", "class": "AerialMapRepository"}
{"timestamp": "2026-06-05 04:21:14", "status": "error", "message": "Operation timed out after 10002 milliseconds with 0 bytes received", "class": "AerialMapRepository"}
{"timestamp": "2026-06-05 04:21:14", "status": "error", "message": "Failed to get records - Operation timed out after 10002 milliseconds with 0 bytes received", "class": "ReviewRepository", "function": "records"}
{"timestamp": "2026-06-05 04:21:16", "status": "error", "message": "Operation timed out after 10002 milliseconds with 0 bytes received", "class": "RecordRepository"}

7일간 timeout 에러 추가 조사 — 동일 패턴이 여러 Repository 클래스에서 간헐적으로 발생:

text
service:cupixworks-api status:error "Operation timed out" (7일간)

EditingEntityRepository, VideoRepository, CaptureRepository, FloorplanRepository, ClusterRepository, Pano 등 다양한 모델/Repository에서 동일한 10002ms timeout이 확인됨.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Elasticsearch 클러스터 일시적 응답 지연 (GC, 과부하, 네트워크) 10초 내 3개 이상 Repository 클래스에서 동시 timeout 발생; 이후 즉시 복구됨; 7일간 여러 클래스에서 간헐적 발생 패턴 Confirmed
H2 특정 Review의 대량 데이터로 인한 쿼리 자체 slow query 단일 쿼리만 느려야 하나, 동일 시간에 AerialMapRepository, RecordRepository도 동시 timeout; size: 10000 설정은 모든 쿼리에 공통 동시 다발적 timeout은 단일 쿼리 문제가 아님 Rejected
H3 Connection Pool 고갈 (size: 10, timeout: 7) Pool timeout 시에도 응답 지연 가능 에러 메시지가 "Operation timed out" (HTTP request timeout)이지 connection pool timeout이 아님; 10002ms는 정확히 request timeout: 10 + 약간의 오버헤드 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 현재 단일 발생이며 자동 복구되었으므로 즉각적인 코드 수정 필요 없음.
  • 다만, rescue Faraday::ConnectionFailed fallback이 timeout 에러를 포함하지 않는 설계적 약점이 있음.

단기 개선 (1주 이내)#

  • app/repositories/concerns/accessible_entities_repository/review.rb:12 에서 Faraday::ConnectionFailed뿐만 아니라 Faraday::TimeoutError(또는 timeout 관련 exception)도 legacy fallback으로 처리하도록 rescue 절을 확장.
  • 동일 패턴이 records, levels, captures, annotation_layers, bims, floorplans 6개 메서드에 반복되므로 모두 일괄 수정 필요.

장기 개선 (재발 방지)#

  • Elasticsearch 클러스터의 간헐적 timeout 빈도를 모니터링하여, 빈도가 높아질 경우 ES 인프라 스케일링 검토.
  • ConnectionPool size(현재 10)와 timeout(현재 10초)의 적정성 재검토 — 트래픽 증가 시 pool 부족 가능.
  • Circuit breaker 패턴 도입으로 ES 장애 시 빠른 fallback 전환 고려.

Monitoring#

  • ES timeout 빈도 추적:
text
service:cupixworks-api status:error "Operation timed out after 10002 milliseconds"
  • ES 응답 시간 메트릭:
text
avg:elasticsearch.query.time{service:cupixworks-api}
  • Connection pool 대기 시간 모니터링 추가 검토.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — rescue 절 확장은 단순하지만 6개 메서드에 걸쳐 일관되게 적용 필요