ES /docs

Cupix::HttpClient missing OpenSSL::X509::StoreError retry

RCA: RestClient::ServiceUnavailable: 503 Service Unavailable

Overview#

What Happened#

tesla (cupixvista, QA/ECE 배포 cupix-tesla-ece-qa, us-west-2) 가 외부/내부 downstream 엔드포인트로 보낸 outbound HTTP 요청이 503 Service Unavailable 을 반환했고, Cupix::HttpClient 의 내장 재시도(최대 3회, exponential backoff)를 모두 소진한 뒤 RestClient::ServiceUnavailable 로 re-raise 되어 Datadog Error Tracking 에 집계되었다. cupixvista-rest_client 는 실제 앱 서비스가 아니라 rest_client gem 에 붙은 APM instrumentation adapter 이름이다. 3개월(2026-05-01 ~ 2026-08-04) 동안 7,569건이 하나의 fingerprint 로 묶여 있으며, 다양한 downstream 대상의 503 이 한 issue 로 합쳐진 것으로 보인다.

Quick Facts#

Field Value
exception.class RestClient::ServiceUnavailable
exception.message 503 Service Unavailable
top_frame lib/cupix/http_client.rb (get/post/put/delete/patch 의 raise)
runtime Ruby (Rails monolith, tesla)
env QA / ECE (cupix-tesla-ece-qa, us-west-2)

deploy SHA 는 로그에서 확인 불가하여 생략.

Affected Teams#

에러가 Error Tracking 에만 존재하고 매칭되는 Datadog 로그가 없어 특정 downstream/도메인으로 범위를 좁힐 수 없다. Cupix::HttpClient 호출부는 다수의 third-party 연동(Procore, PlanGrid, BIM360, Revizto, Salesforce, Slack, Zapier, Workato, Power BI, Gemini)과 내부 microservice(voxel, siteinsights, issue, thumbnail, notification)에 걸쳐 있다.

Timeline#

  1. 2026-05-01 15:16 KST — 최초 발생 (first_seen). Representative Error 로 pin 된 시점.
  2. 2026-05-01 ~ 2026-08-04 — 동일 fingerprint 로 7,569건 누적 (여러 downstream 503 이 하나로 그룹화).
  3. 2026-08-04 13:53 KST — 최근 발생 (last_seen).
  4. 2026-08-04 — RCA 수행. Datadog 에서 매칭 로그 0건 확인, ET-only issue 로 판정.

Error Log#

Datadog Logs

text
503 Service Unavailable

Impact#

  • Service: cupixvista-rest_client (APM adapter — 실제 앱은 tesla)
  • 발생 횟수: 7569
  • 최초 발생: 2026-05-01 15:16 KST
  • 최근 발생: 2026-08-04 13:53 KST

Root Cause Summary#

tesla 의 outbound HTTP 는 Cupix::HttpClient 로 일원화되어 있고, 이 모듈은 RETRIABLE_STATUS_CODES = [429, 502, 503, 504] 에 대해 최대 3회(MAX_RETRIES = 3) exponential backoff 재시도를 수행한다(lib/cupix/http_client.rb:8-9). 즉 503 이 Error Tracking 까지 올라왔다는 것은 초기 요청 + 3회 재시도 = 총 4회 시도 동안 downstream 이 지속적으로 503 을 반환했음을 의미한다. 이는 tesla 측 코드 결함이 아니라 호출 대상(외부 third-party 또는 내부 microservice)의 일시적/지속적 unavailability 이며, 재시도 로직은 이미 갖춰져 있다. Datadog 로그에 매칭 항목이 전혀 없는 점(-excon/-mysql2 같은 adapter-service ET issue 의 전형적 특성)도 이것이 코드 경로에서 log 를 남기지 않고 APM 에서 예외만 캡처된 external availability 이슈임을 뒷받침한다.

Technical Analysis#

Code Path#

  • Entry point: 다수 호출부 (예: app/operations/procore_operation.rb, app/services/cupix/voxel_service.rb, app/workers/post_slack_message_worker.rb 등 43개 파일이 Cupix::HttpClient.{get,post,put,delete,patch} 호출)
  • 공통 실행 경로: lib/cupix/http_client.rb — 모든 outbound HTTP 가 이 모듈을 통과 (직접 RestClient 사용은 spec/rubocop/cop/style/disallow_direct_http_client_spec.rb 의 cop 으로 금지됨)
  • Failure point: lib/cupix/http_client.rb — 재시도 소진 후 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
  • 기대 동작: downstream 503 발생 시 backoff 후 재시도하여 일시적 불안정을 흡수. 대부분의 transient 503 은 재시도로 성공.
  • 실제 동작: downstream 이 4회 시도 내내 503 을 반환하면 raise 로 원본 RestClient::ServiceUnavailable 이 상위로 전파되어 APM/Error Tracking 에 집계. tesla 코드는 정상 동작(retry 후 fail-fast)이며 예외의 원인은 downstream 에 있다.

Log Evidence#

Datadog 에서 여러 각도로 검색했으나 이 예외와 매칭되는 로그는 0건이다. Cupix::HttpClient 가 재시도 소진 후 별도 로그 없이 raise 하고, APM 이 예외만 Error Tracking 으로 캡처하기 때문이다.

사용한 쿼리:

text
service:cupixvista* "RestClient::ServiceUnavailable"   → Found 0 logs
"RestClient::ServiceUnavailable"                        → Found 0 logs
service:cupixvista* "RestClient"                        → Found 0 logs
service:cupixvista* "ServiceUnavailable"                → Found 0 logs
service:cupixvista-api status:error                     → Found 0 logs

service:cupixvista* 는 로그 자체는 존재하며(아래), 배포 환경이 QA/ECE(cupix-tesla-ece-qa, us-west-2)임을 확인한다. 다만 error-level 항목은 모두 RestClient::ServiceUnavailable 과 무관한 AwsTask#batch_pull! 류였다:

text
service:cupixvista* (status:error OR status:warn)  (now-1d)
json
{
  "timestamp": "2026-08-04 17:30:39",
  "status": "error",
  "message": "batch_pull! error on batch arn:aws:ecs:us-west-2:535002880953:task/cupix-tesla-ece-qa/... - Invalid identifier: cluster identifiers mismatch",
  "class": "AwsTask",
  "function": "batch_pull!"
}

Status board 결과(외부 dependency 인시던트 없음):

json
{
  "scope": "svc:cupixvista-rest_client::unknown",
  "active": null,
  "recent": []
}

Representative Error(503 Service Unavailable)는 first_seen(2026-05-01) sample 로 pin 된 것이며, last_seen(2026-08-04) 주변에서 검색해도 별도 메시지가 없어(로그 부재) 최신 occurrence 도 동일한 무맥락 503 문자열로 확인된다. 즉 stale representative 와 최신 occurrence 간 의미 있는 차이는 관측되지 않는다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 downstream(외부 third-party 또는 내부 microservice)이 지속적으로 503 을 반환하는 external availability 이슈 (transient/outage), tesla 코드 결함 아님 http_client.rb:8-9 는 이미 503 을 3회 재시도; 재시도 소진 후에만 raise. Datadog 매칭 로그 0건 (ET-only). -rest_client 는 APM adapter service. Confirmed
H2 Cupix::HttpClient 의 재시도 로직 누락/버그로 인해 일시적 503 이 흡수되지 못함 http_client.rb:8-24 에 backoff 재시도 로직이 명확히 존재하고 RETRIABLE_STATUS_CODES 에 503 포함 Rejected
H3 특정 downstream(예: voxel/siteinsights) 의 코드 결함이 503 을 유발 43개 호출부 중 일부는 내부 microservice 매칭 로그가 없어 특정 downstream 을 지목할 stack frame/식별자 부재. ET fingerprint 가 다수 대상을 하나로 그룹화 Inconclusive
H4 AwsTask batch_pull! 에러가 동일 root cause 같은 시간대 error-level 로그로 관측됨 예외 클래스/함수(AwsTask#batch_pull!, "cluster identifiers mismatch")가 RestClient 503 과 무관 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 코드 변경 불필요. Cupix::HttpClient 는 이미 429/502/503/504 에 대해 exponential backoff 재시도를 수행하므로(lib/cupix/http_client.rb:8-9) tesla 측에서 추가로 고칠 결함이 없다.

단기 개선 (1주 이내)#

  • 관측성 보강(선택): 재시도 소진 후 raise 직전에 downstream URL/host 를 warn 레벨로 로깅하면, ET fingerprint 하나에 묶인 다양한 503 을 downstream 별로 분리·귀속할 수 있다. 현재는 로그가 전혀 없어 어떤 대상이 실패했는지 사후 파악이 불가하다. (구현 시 URL 의 민감정보/토큰 마스킹 필요.)

장기 개선 (재발 방지)#

  • ET issue 를 downstream host 별 tag(예: @http.downstream_host)로 세분화하여, 특정 third-party/microservice 의 지속적 outage 를 개별 모니터링·알림으로 분리.

Monitoring#

Error Tracking issue 자체는 로그가 없어 로그 기반 그래프가 비어 있을 수 있으나, 재시도 소진 로깅을 도입할 경우 아래로 추적 가능:

text
service:cupixvista* "RestClient::ServiceUnavailable"

APM trace 기반으로 downstream 503 비율을 보려면 trace analytics 에서 resource_name 별 status:503 분포를 확인.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 수정 불필요; 선택적 로깅 보강만 standard)

Noise Verdict#

noise — tesla 는 이미 Cupix::HttpClient 에서 503 을 3회 재시도한 뒤 re-raise 하며, 이 예외는 downstream 의 일시적/지속적 unavailability 로 인한 external availability 이슈일 뿐 코드 결함이 아니다.