[SalesforceWebToCaseWorker] fail to send web to case. reason: 'RestClient error' case_type: 'floorpl
RCA: SalesforceWebToCaseWorker — RestClient ReadTimeout to Salesforce Web-to-Case endpoint
Overview#
What Happened#
2026-06-10 09:49:52 KST, cupixworks-worker (Sidekiq) 의 SalesforceWebToCaseWorker 가 Salesforce Web-to-Case endpoint(https://webto.salesforce.com/servlet/servlet.WebToCase) 로 floorplan_update_case POST 요청을 보내던 중 읽기 타임아웃(60초 초과) 으로 실패했다. RestClient 의 read timeout (Net::ReadTimeout) 이 발생하여 RestClient::Exceptions::ReadTimeout 으로 wrapping 되었고, 14일 retention 기간 내 단 1건만 발생한 transient 이벤트로 확인됐다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | RestClient::Exceptions::ReadTimeout (wraps Net::ReadTimeout) |
| exception.message | Timed out reading data from server |
| top_frame | app/workers/salesforce_web_to_case_worker.rb:8 (Cupix::HttpClient.post) |
| downstream | https://webto.salesforce.com/servlet/servlet.WebToCase |
| env | production, us-west-2 |
| tenant | cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| Salesforce integration / Floorplan | 1 | floorplan_update_case 1건이 Salesforce Case 로 전송되지 않음 (Floorplan model_id 87830). 단, 같은 model_id 87830 에 대해 9초 전 (09:48:44 KST) 동일한 case 전송이 성공한 기록이 있어 사용자/영업 임팩트는 거의 없음 (중복 trigger 로 보이는 두 번째 시도가 실패) |
Timeline#
- 2026-06-10 09:48:42 KST —
SalesforceWebToCaseWorkerJID08c52f7a5647c4d75aa28568시작 (Floorplan model_id 87830) - 2026-06-10 09:48:44 KST — 위 JID 성공 (
send web to case successful, 0.561 sec) - 2026-06-10 09:48:52 KST —
SalesforceWebToCaseWorkerJIDbe687e8f81590d6ad02c9709다시 시작 (동일 Floorplan model_id 87830, 중복 trigger 추정) - 2026-06-10 09:49:52 KST — RestClient read timeout (60.279 sec 후) → 에러 로그 기록 후 worker 정상 종료
- 2026-06-10 09:49:52 KST — error-sweeper 가 cluster 생성 (occurrence_count: 1)
Error Log#
[SalesforceWebToCaseWorker] fail to send web to case. reason: 'RestClient error' case_type: 'floorplan_update_case' sf_resource_id: 'company' model: 'Floorplan' message: Timed out reading data from server
Impact#
- Service:
cupixworks-worker - 발생 횟수: 1
- 최초 발생: 2026-06-10 09:49:52 KST
- 최근 발생: 2026-06-10 09:49:52 KST
Root Cause Summary#
Salesforce 의 Web-to-Case endpoint (https://webto.salesforce.com/servlet/servlet.WebToCase) 가 단일 POST 요청에 대해 60초 이내에 응답하지 않아 RestClient 의 read timeout 이 발생했다. SalesforceWebToCaseWorker 는 RestClient::Exception 을 rescue 해서 에러 로그만 남기고 worker 를 정상 종료시키므로 Sidekiq 의 자동 retry 도 발동하지 않는다 (예외가 worker 밖으로 re-raise 되지 않기 때문에 retry: 1 도 적용되지 않음). 또한 Cupix::HttpClient.post 는 retry 대상을 HTTP 상태 코드 [429, 502, 503, 504] 로만 한정하고 있어, 응답이 아예 도착하지 않아 http_code 가 nil 인 timeout 케이스에는 자동 retry 가 작동하지 않는다. Salesforce 측 일시적 지연으로 발생한 transient 외부 의존성 장애이며, 14일 retention 기간 내 단 1건만 관찰되어 systematic 결함이 아니다.
Technical Analysis#
Code Path#
Entry point: 도메인 모델이 send_web_to_case 또는 Cupix::Salesforce::Case::CaseSender.send_web_to_case_in_worker 를 호출 → SalesforceWebToCaseWorker.perform_async 큐잉.
def send_web_to_case(data, case_type)
return if Rails.env.test?
data.merge!({ orgid: SALESFORCE_ORG_ID })
SalesforceWebToCaseWorker.perform_async(JSON.generate(data), case_type, self.class.name, self.id)
end
Worker 진입점에서 외부 HTTP POST 호출:
class SalesforceWebToCaseWorker
include Sidekiq::Worker
sidekiq_options queue: :salesforce, retry: 1
def perform(case_form, case_type, model_type, model_id)
Cupix::Logger.info("[SalesforceWebToCaseWorker] start creating new salesforce case. case_type: '#{case_type}' sf_resource_id: '#{case_form['company']}' model: '#{model_type}' model_id: '#{model_id}'")
data = JSON.parse(case_form)
Cupix::HttpClient.post(WEB_TO_CASE_POST_URL, data, { content_type: 'application/x-www-form-urlencoded' })
rescue RestClient::Exception => e
Cupix::Logger.error("[SalesforceWebToCaseWorker] fail to send web to case. reason: 'RestClient error' case_type: '#{case_type}' sf_resource_id: '#{case_form['company']}' model: '#{model_type}' message: #{e.message}")
rescue StandardError => e
Cupix::Logger.error("[SalesforceWebToCaseWorker] fail to send web to case. reason: 'Standard error 'case_type: '#{case_type}' sf_resource_id: '#{case_form['company']}' model: '#{model_type}' message: #{e.message}")
else
Cupix::Logger.info("[SalesforceWebToCaseWorker] send web to case successful. case_type: '#{case_type}' sf_resource_id: '#{case_form['company']}' model: '#{model_type}' model_id: '#{model_id}'")
end
end
WEB_TO_CASE_POST_URL 상수:
WEB_TO_CASE_POST_URL = 'https://webto.salesforce.com/servlet/servlet.WebToCase?encoding=UTF-8'.freeze
Cupix::HttpClient.post 의 retry 정책 — timeout(http_code nil) 은 retry 대상이 아님:
RETRIABLE_STATUS_CODES = [429, 502, 503, 504].freeze
MAX_RETRIES = 3
# ...
def self.post(url, payload, headers = {}, retries: MAX_RETRIES)
attempt = 0
begin
RestClient.post(url, payload, 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
Failure point: app/workers/salesforce_web_to_case_worker.rb:8 의 Cupix::HttpClient.post 호출이 60초 read timeout 에 걸려 RestClient::Exceptions::ReadTimeout 을 raise. http_code 는 nil 이므로 Cupix::HttpClient.post 의 retry 분기 미적용 → 예외 re-raise → worker 의 rescue RestClient::Exception 에 잡혀 에러 로그만 출력하고 worker 는 정상 종료(예외 swallow). 그 결과 Sidekiq 의 retry: 1 도 적용되지 않는다.
기대 동작 vs 실제 동작:
- 기대: Web-to-Case 가 일시적 timeout 일 때 자동 retry 후 성공 → Salesforce 에 case 생성
- 실제: timeout 발생 → retry 없이 worker 가 swallow 하고 종료 → Salesforce 에 case 미생성, 알림/추적 누락
Log Evidence#
Datadog 쿼리 (searching-datadog-logs 스킬):
service:cupixworks-worker "SalesforceWebToCaseWorker"
service:cupixworks-worker status:error "Timed out reading data from server"
타임아웃 직전·직후 로그 (KST):
2026-06-10 09:48:42 info SalesforceWebToCaseWorker JID-08c52f7a5647c4d75aa28568: done: 0.561 sec
2026-06-10 09:48:44 info [SalesforceWebToCaseWorker] send web to case successful. case_type: 'floorplan_update_case' sf_resource_id: 'company' model: 'Floorplan' model_id: '87830'
2026-06-10 09:48:52 info [SalesforceWebToCaseWorker] start creating new salesforce case. case_type: 'floorplan_update_case' sf_resource_id: 'company' model: 'Floorplan' model_id: '87830'
2026-06-10 09:49:52 error [SalesforceWebToCaseWorker] fail to send web to case. reason: 'RestClient error' case_type: 'floorplan_update_case' sf_resource_id: 'company' model: 'Floorplan' message: Timed out reading data from server
2026-06-10 09:49:52 info SalesforceWebToCaseWorker JID-be687e8f81590d6ad02c9709: done: 60.279 sec
핵심 관찰:
- 실패한 JID
be687e8f81590d6ad02c9709의 실행 시간이 정확히 60.279 초 — RestClient 의 defaultread_timeout(60초) 와 일치한다. - 같은 Floorplan model_id 87830 에 대해 9초 전(09:48:44 KST) 별도 JID 가 정상 성공 (
send web to case successful) → 동일 모델에 두 번 case 가 trigger 된 것으로 보인다 (성공 1건 + 타임아웃 1건). - 14일 retention 내
service:cupixworks-worker status:error "Timed out reading data from server"검색 결과 단 1건. 동일 워커의 다른 정상 호출 50+ 건은 모두 0.5–0.8 초에 완료 → systematic 성능 저하 아님. done: 60.279 sec로그가 정상 출력된 것은 worker 가 예외를 rescue 후 정상 return 했다는 것 → Sidekiq retry 미발동 확정.
Database/State Verification#
- 동일 Floorplan model_id 87830 에 대해 같은 시각대 정상 성공 사례 존재 (위 09:48:44 KST 로그). Salesforce 측 Case 레코드는 첫 번째 시도에서 이미 생성되었을 가능성이 높음. (Salesforce 측 데이터 직접 확인은 불가 — uncertain, needs verification)
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Salesforce Web-to-Case endpoint 가 일시적으로 응답 지연 → RestClient default 60s read timeout 초과 | 실패 JID 의 실행 시간이 정확히 60.279s (default read_timeout 과 일치); 14일 내 1건만 발생; 같은 워커의 다른 호출은 0.5–0.8s 에 정상 완료 | — | Confirmed |
| H2 | Cupix::HttpClient.post 의 retry 가 정상 동작했다면 회복 가능했지만 timeout 은 retry 대상이 아님 (코드 결함이 직접적 root cause) |
RETRIABLE_STATUS_CODES = [429, 502, 503, 504] 만 retry; timeout 시 e.http_code 는 nil 이므로 분기 false |
timeout 자체가 transient 이므로 retry 가 있어도 같은 timeout 이 또 날 수 있음 — 코드 결함이 단독 root cause 는 아님 | Contributing factor (root cause 아님) |
| H3 | Sidekiq retry: 1 가 발동했어야 했음 |
sidekiq_options 에 retry: 1 설정 존재 | worker 가 rescue RestClient::Exception => e 로 예외를 swallow 하고 정상 return → Sidekiq 입장에서 job 성공으로 보여 retry 미발동. done: 60.279 sec 로그가 이를 확정. |
Rejected (설계상 retry 불가) |
| H4 | 중복 case 생성 trigger 로 인해 동일 Floorplan 87830 에 두 번 perform_async 호출됨 | 09:48:42 / 09:48:52 두 차례 start 로그, 같은 model_id 87830, 첫 번째는 성공 두 번째는 timeout |
두 번째가 timeout 으로 실패한 것은 중복 trigger 자체가 원인이 아니라 Salesforce 측 지연 — 단, 중복 trigger 가 없었다면 이 cluster 자체가 발생 안 했을 가능성 | Inconclusive (관련 컨텍스트지만 timeout 의 원인 아님) |
| H5 | systematic 코드 회귀 (네트워크 lib 변경, 인증 만료 등) | — | 14일 내 1건; 직전·직후 같은 워커가 정상 동작; 같은 호스트로 다른 case_type 도 정상 성공 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 없음. 14일 내 1건의 transient 외부 의존성(Salesforce) 타임아웃이며, 동일 Floorplan 에 대해 중복 trigger 중 하나가 실패했고 다른 하나는 성공했다. 즉시 사용자 영향이 명확하지 않으므로 즉시 롤백/핫픽스는 불필요.
- 운영 관점: Salesforce 측 Case 가 실제로 생성됐는지(첫 번째 성공 시도로) 한 번 확인하면 안전.
단기 개선 (1주 이내)#
lib/cupix/http_client.rb의 retry 정책을 timeout(RestClient::Exceptions::Timeout, 즉ReadTimeout/OpenTimeout)도 retry 대상에 포함하도록 확장 검토. 현재RETRIABLE_STATUS_CODES.include?(e.http_code)분기는e.http_code가 nil 인 timeout 을 통과시키지 못함. 단, timeout retry 는 idempotent 하지 않은 endpoint 에서는 중복 처리 위험이 있으므로 Web-to-Case 와 같은 case-creation endpoint 에 적용 시 주의 필요 — 호출 측에서 dedup key 를 보내거나 Salesforce 측에서 dedup 로직이 있는지 검증 후 적용.SalesforceWebToCaseWorker의 예외 핸들링을 검토: 현재 모든RestClient::Exception을 swallow 해서 Sidekiq retry 가 발동하지 않는다. 적어도Timeout계열 예외는 re-raise 해 Sidekiqretry: 1정책이 동작하게 하면 transient 장애에 자연스럽게 대응 가능. 다만 idempotency 검토는 동일하게 필요.- 에러 로그에
model_id가 빠져 있어 어떤 레코드의 case 가 실패했는지 추적이 어려움 — error 로그 라인에model_id추가를 권장 (현재 info 라인에는 있음). 이 cluster 에서도 model_id 를 알기 위해 인접 info 로그를 역추적해야 했다.
장기 개선 (재발 방지)#
- Web-to-Case 같은 fire-and-forget 외부 통합에 대해 outbox / dead-letter pattern 도입 검토: Sidekiq job 이 최종 실패하면 별도 테이블/큐에 기록하고 운영자가 backfill 가능하도록.
- Salesforce 통합 호출에 대한 metric (latency, error rate, retry count) 노출 후 dashboard/monitor 등록.
- 동일 모델에 대한 중복 case_send trigger (H4 참고) 의 원인을 별도 task 로 조사 —
send_web_to_case호출자가 여러 콜백/트랜잭션 경로에서 중복 호출되는지 확인.
Monitoring#
추가할 메트릭/알림 (Datadog timeseries widget 용 — writing-datadog-monitoring-queries 스킬 가이드 따라 작성):
SalesforceWebToCaseWorker 의 timeout 류 에러 발생 추이:
sum:trace.sidekiq.job.errors{service:cupixworks-worker,resource_name:salesforceweb_to_case_worker}.as_count()
(메트릭 이름은 환경에 따라 상이할 수 있어 uncertain — 실제 적용 전 Datadog 메트릭 explorer 에서 확인 필요)
대안: 로그 기반 카운트 (Datadog log analytics widget — timeseries widget 에서도 동작):
service:cupixworks-worker status:error "[SalesforceWebToCaseWorker] fail to send web to case"
Salesforce Web-to-Case 호출 latency p95 추적 (호출 측에서 별도 metric instrumentation 필요 — 현재 코드에는 없음, 단기 개선 항목과 함께 도입):
p95:trace.rest_client.request.duration{host:webto.salesforce.com,service:cupixworks-worker}
(trace.rest_client.request.duration 메트릭 존재 여부 uncertain — needs verification. 없다면 dd-trace 의 RestClient integration 활성화 필요)
알림 정책 권장:
[SalesforceWebToCaseWorker] fail to send web to case가 5분 윈도우에 3건 이상 발생 시 warn 알림 (현재처럼 14일 1건은 noise 이므로 즉각 알림은 과함).
Risk Assessment#
- Risk level: low — 14일 1건 transient 외부 의존성 타임아웃, 동일 모델은 다른 시도로 성공했을 가능성 높음, systematic 결함 아님
- 예상 복잡도: standard — 단기 개선(HttpClient timeout retry 정책, worker 의 timeout re-raise) 은 idempotency 검증을 동반해야 하므로 trivial 보다 한 단계 위