ES /docs

failed to get element records - error: Timed out connecting to server

RCA: failed to get element records - error: Timed out connecting to server

Overview#

What Happened#

2026-07-08 21:58 KST, cupixworks-api production (us-west-2) 에서 GET /api/v1/siteinsights/element_records 요청 하나가 502 로 실패했다. 원인은 하위 Siteinsights microservice (AWS API Gateway) 로의 TCP 연결이 60초 동안 열리지 않아 RestClient::Exceptions::OpenTimeout 이 발생한 것. 14일 retention 안에서 동일 exception 은 총 3건 (2개 엔드포인트) 뿐이라 systemic 이슈가 아니라 transient network/upstream 이벤트로 판단된다.

Quick Facts#

Field Value
exception.class Cupix::Errors::System (원인: RestClient::Exceptions::OpenTimeout)
exception.message failed to get element records - error: Timed out connecting to server
top_frame app/services/cupix/siteinsights_service.rb:118
code SYS20000
deploy production-us-west-2-20260708T0836Z0-5c918141-cupixworks
env production, region us-west-2
duration 60027.9 ms (Net::HTTP default open_timeout = 60s)

Affected Teams#

Team / Domain Error Count Impact
clark-vdc (team_id 87) 1 단일 사용자(sebastian.boyle@clarkconstruction.com)의 facility_key=3fq7tj element_records 페이지네이션 요청 1건이 502 응답. UI 상 재시도 후 정상 응답 가능성이 높다.

Timeline#

  1. 2026-07-08 17:36 KST — 최신 배포(production-us-west-2-20260708T0836Z0-5c918141-cupixworks) 반영.
  2. 2026-07-08 21:58:57 KSTApi::V1::ElementRecordsController#index 가 Siteinsights #{service_url}/element_records 호출을 시작. TCP connect 실패로 60,027.9 ms 후 RestClient::Exceptions::OpenTimeout 발생.
  3. 2026-07-08 21:58:57 KSTCupix::Errors::System (SYS20000) 로 rescue 되어 502 응답. 후속 재시도 로그 없음(즉시 해소된 것으로 보임).

Error Log#

Datadog Logs

text
failed to get element records - error: Timed out connecting to server

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 1
  • 최초 발생: 2026-07-08 21:58 KST
  • 최근 발생: 2026-07-08 21:58 KST

Root Cause Summary#

Cupix::SiteinsightsService.get_element_records!Cupix::HttpClient.get 을 통해 프로덕션 Siteinsights API Gateway (https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api/element_records) 에 GET 요청을 보냈으나, TCP 연결이 60초 (Net::HTTP open_timeout 기본값) 동안 성립되지 않아 RestClient::Exceptions::OpenTimeout 이 던져졌다. 이는 downstream 서비스의 응답 지연이 아니라 connect 단계 실패이며, transient 네트워크/DNS/API Gateway edge 이슈로 추정된다. Cupix::HttpClient 의 재시도 로직은 HTTP status code 기반이라 (RETRIABLE_STATUS_CODES = [429, 502, 503, 504]), status code 가 없는 connect timeout 은 e.http_code == nil 이 되어 재시도가 트리거되지 않고 즉시 최상위로 전파된 것이 실패 폭을 키운 부수 요인이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/element_records_controller.rb:4-9
  • Failure point: app/services/cupix/siteinsights_service.rb:118 (RestClient GET)
  • Rescue / 재포장: app/services/cupix/siteinsights_service.rb:121-124
  • 502 변환: app/controllers/api/v1/element_records_controller.rb:2, 37-39
  • 재시도 필터: lib/cupix/http_client.rb:12-24 — status code 만으로 판정

Controller 는 Cupix::Errors::System 을 catch 하여 502 로 매핑한다.

app/controllers/api/v1/element_records_controller.rb:1-9ruby
class Api::V1::ElementRecordsController < Api::V1::ApiController
  rescue_from Cupix::Errors::System, with: :redirect_to_502_error

  def index
    si_query_option = Cupix::QueryOption::Siteinsights.new(get_query_option, params)
    element_records = Cupix::SiteinsightsService.get_element_records!(si_query_option, fields: @fields, current_user: @current_user, current_team: @current_team)

    render_json 200, element_records
  end

실제 HTTP 호출과 예외 재포장 지점.

app/services/cupix/siteinsights_service.rb:100-129ruby
def get_element_records!(query_option, fields: '', current_user: nil, current_team: nil)
  raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'facility_key is required') if query_option.facility_key.blank?

  begin
    params = {
      facility_key: query_option.facility_key
    }
    params[:vendor_ids] = query_option.vendor_ids.join(',') if query_option.vendor_ids.present?
    # ... 기타 params 병합 ...
    params[:fields] = fields if fields.present?

    response = Cupix::HttpClient.get("#{Cupix::Siteinsights.service_url}/element_records?#{params.to_param}")

    JSON.parse(response.body)['result']
  rescue RestClient::Exception => e
    Cupix::Logger.error("failed to get element records - error: #{e.message}", class: self.name, function: __method__, facility_key: query_option.facility_key, params: params)

    raise Cupix::Errors::System.new(code: 'SYS20000', reason: "failed to get element records - error: #{e.message}")

재시도 로직은 HTTP status code 기반이라 connect timeout 을 잡지 못한다. e.http_codeRestClient::Exceptions::OpenTimeout 에서 nil 이므로 RETRIABLE_STATUS_CODES.include?(nil) == false 로 즉시 raise 로 진행한다.

lib/cupix/http_client.rb:8-24ruby
RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
MAX_RETRIES = 3

sig { params(url: String, headers: T::Hash[T.untyped, T.untyped], retries: Integer).returns(RestClient::Response) }
def self.get(url, headers = {}, retries: MAX_RETRIES)
  attempt = 0
  begin
    RestClient.get(url, headers)
  rescue RestClient::Exception => e
    if RETRIABLE_STATUS_CODES.include?(e.http_code) && attempt < retries
      attempt += 1
      sleep((2**(attempt - 1)) + rand(0.0..0.5))
      retry
    end
    raise
  end
end

Production us-west-2 는 Cupix::Siteinsights.service_url 이 기본 case (region_code = usw2) 로 떨어져 https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api 를 호출한다.

lib/cupix/siteinsights.rb:16-30ruby
when 'production'
  case Cupix::Tesla.region_code
  when 'euce1'
    'https://4img1siwbf.execute-api.eu-central-1.amazonaws.com/api'
  when 'apse2'
    'https://5ufrbwf306.execute-api.ap-southeast-2.amazonaws.com/api'
  when 'apne1'
    'https://gi8rwxyr8j.execute-api.ap-northeast-1.amazonaws.com/api'
  when 'apse1'
    'https://30h7tj8fjg.execute-api.ap-southeast-1.amazonaws.com/api'
  when 'cace1'
    'https://px30557psl.execute-api.ca-central-1.amazonaws.com/api'
  else
    'https://6hdi0xzqqk.execute-api.us-west-2.amazonaws.com/api'
  end

Log Evidence#

Datadog query 1 — 대표 에러 재현.

text
service:cupixworks-api "failed to get element records"

핵심 필드 (원문 JSON 발췌, 2026-07-08 21:58:57 KST).

json
{
  "message": "failed to get element records - error: Timed out connecting to server",
  "class": "Cupix::SiteinsightsService",
  "function": "get_element_records!",
  "level": "error",
  "request_id": "23874b65-02e6-49e8-9002-b83eaf0f6f9f",
  "facility_key": "3fq7tj",
  "tenant": "cupix",
  "environment": "production",
  "region": "us-west-2",
  "version": "production-us-west-2-20260708T0836Z0-5c918141-cupixworks",
  "params": {
    "per_page": 300,
    "page": 6,
    "level_ids": "72845",
    "category_ids": "1029840,1029845,1029854,1029857,1029859",
    "completed_until": "2026-07-06T18:03:04.000Z",
    "facility_key": "3fq7tj"
  }
}

연동된 request 로그 (같은 request_id).

json
{
  "message": "[502] GET /api/v1/siteinsights/element_records (Api::V1::ElementRecordsController#index)",
  "http": { "status_code": 502, "method": "GET", "url_details": { "path": "/api/v1/siteinsights/element_records" } },
  "duration": 60027.9,
  "db": 5.41,
  "team": { "domain": "clark-vdc", "id": 87 },
  "user": { "id": 50088, "email": "sebastian.boyle@clarkconstruction.com" },
  "error": {
    "reason": "failed to get element records - error: Timed out connecting to server",
    "code": "SYS20000",
    "class": "Cupix::Errors::System"
  },
  "request_id": "23874b65-02e6-49e8-9002-b83eaf0f6f9f"
}

duration: 60027.9 ms 는 Ruby Net::HTTPopen_timeout 기본값 60초와 정확히 일치 — 즉 응답 지연이 아니라 TCP connect 자체가 성립하지 못한 상태에서 timeout 된 것이다.

Datadog query 2 — 14일 window 확장 검색 (범위 넓힘).

text
service:cupixworks-api "Timed out connecting to server"

결과 3건, 모두 다른 시각/엔드포인트:

Timestamp (KST) Endpoint Class
2026-07-08 21:58 GET /api/v1/siteinsights/element_records Cupix::SiteinsightsService
2026-07-08 21:58 [502] request 로그 (동일 요청) Cupix::Errors::System
2026-07-07 20:13 POST /api/v1/bims/forge_access_token RestClient::Exceptions::OpenTimeout

Datadog query 3 — downstream(siteinsights) 서비스 자체 에러 로그 검증.

text
"siteinsights" status:error

2026-07-08T12:55Z ~ 13:05Z 구간에서 결과 0건. Downstream Lambda/API Gateway 측의 에러 로그가 관측되지 않는다는 것은, 요청이 애초에 도달하지 못했을 (TCP connect 실패) 가능성을 뒷받침한다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 하위 Siteinsights API Gateway/Lambda 로의 TCP connect 가 transient 하게 실패하여 60초 open_timeout duration: 60027.9ms 가 Net::HTTP open_timeout 기본값과 일치, 예외 클래스가 RestClient::Exceptions::OpenTimeout (read timeout 아님), downstream service 에러 로그 0건 Confirmed
H2 Downstream Siteinsights 서비스 자체 장애 (upstream outage) 동일 시간대에 다른 팀/facility 의 실패가 있으면 outage 가능 14일 window 에서 element_records 관련 실패는 단 1건, downstream 로그도 error 없음. status-board 도 dep:* scope 아닌 svc:cupixworks-api::unknown 으로 분류. Rejected
H3 Cupix::HttpClient 의 재시도 부족이 근본 원인 RETRIABLE_STATUS_CODES 가 status code 만 검사, OpenTimeoute.http_code == nil 이라 재시도 스킵 connect timeout 자체가 근본 트리거이며, 재시도는 완화책. 이 자체가 root cause 는 아님. Rejected (contributing factor)
H4 배포 (5c918141, 2026-07-08 08:36Z) 로 인한 회귀 배포 후 ~4시간 뒤 발생 배포 후 발생 건수 1건 뿐이며, 배포 전(2026-07-07) 에도 유사 OpenTimeout(forge_access_token) 발생. 배포와 무관한 패턴. Rejected
H5 페이지네이션(per_page=300, page=6) 이 원인 (파라미터 유발 부하) 파라미터가 큰 편 connect timeout 은 서버 처리 부하가 아니라 TCP handshake 실패 — 파라미터가 영향을 주는 계층 아님. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. 단일 transient 이벤트이고, 사용자 재시도로 회복되었다. 별도 hotfix 나 롤백 필요 없음.

단기 개선 (1주 이내)#

  • lib/cupix/http_client.rb 재시도 로직을 확장하여 connect 계열 예외(RestClient::Exceptions::OpenTimeout, RestClient::ServerBrokeConnection) 도 재시도 대상에 포함. rescue 절에서 case e when RestClient::Exceptions::OpenTimeout, ... then retry 형태로 처리하거나, status code 검사 앞단에 별도 조건 추가.
    • 재시도 회수 제한(MAX_RETRIES = 3) 과 exponential backoff (2**(attempt-1) + rand(0..0.5)) 은 그대로 재활용.
    • 재시도 시 idempotent 하지 않은 verb(post/put/patch/delete) 는 connect 실패인 경우에도 요청이 서버에 도달하지 않았음이 보장되므로 안전하다. 단, retry 대상 verb 정책은 리뷰어 협의 필요.
  • Cupix::HttpClient 호출부 전반에 대해 명시적 open_timeout / read_timeout 지정(RestClient::Request.execute(..., open_timeout: 5, timeout: 30)) 을 검토. 현재 60초 open_timeout 은 사용자 UX 관점에서 지나치게 길다.

장기 개선 (재발 방지)#

  • Cupix::SiteinsightsService 및 다른 downstream HTTP client wrapper 들이 실질적으로 동일한 retry/timeout 패턴을 다루므로, 재시도·타임아웃 정책을 Cupix::HttpClient 한 곳에서 declarative 하게 정의하도록 통합. 예: HttpClient.get(url, retry_on: %i[open_timeout server_5xx], open_timeout: 5, read_timeout: 30).
  • Downstream 서비스별 SLO 대시보드에 outbound connect 실패율/응답 시간 histogram 추가. 현 배포 버전에는 outbound RestClient 호출 자체를 계측하는 명시적 메트릭이 확인되지 않음.

Monitoring#

Datadog release dashboard timeseries 위젯용 쿼리 (모두 sum: prefix, .as_count() 로 count 를 그리도록 구성).

Outbound connect timeout 전반 트렌드 (모든 downstream) — 초기값이 낮으므로 개선 후 지속적으로 0에 수렴하는지 확인.

text
sum:trace.rack.request.errors{service:cupixworks-api,error_type:RestClient::Exceptions::OpenTimeout,env:production}.as_count()

Siteinsights element_records 502 응답 카운트.

text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api/v1/element_records_controller#index,http.status_code:502,env:production}.as_count()

로그 기반 대체 쿼리 (trace metric 미구성 시).

text
logs("service:cupixworks-api \"failed to get element records - error: Timed out connecting to server\"").index("*").rollup("count").by("region").last("1h") > 5

위 로그 쿼리는 monitor 문법이므로 dashboard timeseries 에 넣을 경우 아래처럼 count_by_status 계열 metric 으로 치환할 것.

text
sum:cupix.logs.errors{service:cupixworks-api,@class:Cupix::SiteinsightsService,@function:get_element_records!,env:production}.as_count()

알림 임계값 제안: 1분 rolling window 에서 동일 exception 이 3회 이상이면 경보 (systemic outage 신호로 간주).

Risk Assessment#

  • Risk level: low. 14일 retention 내에서 단 1건, 사용자 재시도로 회복 가능한 502. 사용자 impact 는 한 사용자·한 페이지네이션 요청.
  • 예상 복잡도: trivial. 코드 수정 자체는 필요 없음. 단기 개선(retry 확장)만 진행한다면 lib/cupix/http_client.rb 5개 메서드에 동일 패턴 적용 (standard 수준).