ES /docs

[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#

  1. 2026-06-10 09:48:42 KSTSalesforceWebToCaseWorker JID 08c52f7a5647c4d75aa28568 시작 (Floorplan model_id 87830)
  2. 2026-06-10 09:48:44 KST — 위 JID 성공 (send web to case successful, 0.561 sec)
  3. 2026-06-10 09:48:52 KSTSalesforceWebToCaseWorker JID be687e8f81590d6ad02c9709 다시 시작 (동일 Floorplan model_id 87830, 중복 trigger 추정)
  4. 2026-06-10 09:49:52 KST — RestClient read timeout (60.279 sec 후) → 에러 로그 기록 후 worker 정상 종료
  5. 2026-06-10 09:49:52 KST — error-sweeper 가 cluster 생성 (occurrence_count: 1)

Error Log#

Datadog Logs

text
[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 이 발생했다. SalesforceWebToCaseWorkerRestClient::Exception 을 rescue 해서 에러 로그만 남기고 worker 를 정상 종료시키므로 Sidekiq 의 자동 retry 도 발동하지 않는다 (예외가 worker 밖으로 re-raise 되지 않기 때문에 retry: 1 도 적용되지 않음). 또한 Cupix::HttpClient.post 는 retry 대상을 HTTP 상태 코드 [429, 502, 503, 504] 로만 한정하고 있어, 응답이 아예 도착하지 않아 http_codenil 인 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 큐잉.

app/models/concerns/salesforce_integratable/case/case_sendable.rb:7-12ruby
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 호출:

app/workers/salesforce_web_to_case_worker.rb:1-16ruby
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 상수:

config/initializers/salesforce.rb:3ruby
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 대상이 아님:

lib/cupix/http_client.rb:8-46ruby
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:8Cupix::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 스킬):

text
service:cupixworks-worker "SalesforceWebToCaseWorker"
text
service:cupixworks-worker status:error "Timed out reading data from server"

타임아웃 직전·직후 로그 (KST):

text
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 의 default read_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 해 Sidekiq retry: 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 류 에러 발생 추이:

text
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 에서도 동작):

text
service:cupixworks-worker status:error "[SalesforceWebToCaseWorker] fail to send web to case"

Salesforce Web-to-Case 호출 latency p95 추적 (호출 측에서 별도 metric instrumentation 필요 — 현재 코드에는 없음, 단기 개선 항목과 함께 도입):

text
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 보다 한 단계 위