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#
- 2026-05-01 15:16 KST — 최초 발생 (first_seen). Representative Error 로 pin 된 시점.
- 2026-05-01 ~ 2026-08-04 — 동일 fingerprint 로 7,569건 누적 (여러 downstream 503 이 하나로 그룹화).
- 2026-08-04 13:53 KST — 최근 발생 (last_seen).
- 2026-08-04 — RCA 수행. Datadog 에서 매칭 로그 0건 확인, ET-only issue 로 판정.
Error Log#
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
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 으로 캡처하기 때문이다.
사용한 쿼리:
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! 류였다:
service:cupixvista* (status:error OR status:warn) (now-1d)
{
"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 인시던트 없음):
{
"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 자체는 로그가 없어 로그 기반 그래프가 비어 있을 수 있으나, 재시도 소진 로깅을 도입할 경우 아래로 추적 가능:
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 이슈일 뿐 코드 결함이 아니다.