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#
- 2026-06-05 04:21:06 KST —
AerialMapRepositoryES timeout 발생 (첫 번째 timeout) - 2026-06-05 04:21:14 KST —
ReviewRepository#records및AerialMapRepositoryES timeout 발생 - 2026-06-05 04:21:16 KST —
RecordRepositoryES timeout 발생 (마지막 timeout) - 2026-06-05 04:21:16 KST 이후 — ES 응답 정상 복구 (이후 timeout 로그 없음)
Error Log#
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에 쿼리를 보낸다:
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 쿼리 실행 지점:
search_results = ::Record.search(query_option.serializable_hash.merge(size: 10000)).records
Elasticsearch 클라이언트 timeout 설정:
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 쿼리:
service:cupixworks-api status:error "Operation timed out"
동일 시간대(04:21:06~04:21:16 KST)에 발생한 timeout 에러들:
{"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 클래스에서 간헐적으로 발생:
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::ConnectionFailedfallback이 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,floorplans6개 메서드에 반복되므로 모두 일괄 수정 필요.
장기 개선 (재발 방지)#
- Elasticsearch 클러스터의 간헐적 timeout 빈도를 모니터링하여, 빈도가 높아질 경우 ES 인프라 스케일링 검토.
- ConnectionPool size(현재 10)와 timeout(현재 10초)의 적정성 재검토 — 트래픽 증가 시 pool 부족 가능.
- Circuit breaker 패턴 도입으로 ES 장애 시 빠른 fallback 전환 고려.
Monitoring#
- ES timeout 빈도 추적:
service:cupixworks-api status:error "Operation timed out after 10002 milliseconds"
- ES 응답 시간 메트릭:
avg:elasticsearch.query.time{service:cupixworks-api}
- Connection pool 대기 시간 모니터링 추가 검토.
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard — rescue 절 확장은 단순하지만 6개 메서드에 걸쳐 일관되게 적용 필요