ES /docs

BIM360 Authentication failed: Connection reset by peer

RCA: BIM360 Authentication failed: Connection reset by peer

Overview#

What Happened#

2026-06-09 09:07:21 KST에 cupixworks-workerCupix::Cron::Integration.renew_before_expiration 크론이 BIM360 integration(5324)의 OAuth refresh token을 갱신하던 중 Autodesk Forge OAuth 엔드포인트와의 TCP 연결이 reset되어 Errno::ECONNRESET("Connection reset by peer")이 발생했다. 이 예외는 Cupix::HttpClient.post에서 재시도되지 않고 Bim360Operation.refresh_tokenrescue StandardError 브랜치까지 전파되었으며, IntegrationRepository#refresh_token이 integration 5324를 failed_state로 영속화했다. 1회 발생.

Quick Facts#

Field Value
exception.class Errno::ECONNRESET (RestClient → Cupix::Errors::BadGateway BG10001로 wrap)
exception.message Connection reset by peer
top_frame app/operations/bim360_operation.rb:72 (Bim360Operation.refresh_token)
caller app/repositories/integration_repository.rb:128
schedule config/schedule.rb:94every '7 */4 * * *'
env production, us-west-2
affected integration bim360 integration id=5324

Affected Teams#

Team / Domain Error Count Impact
BIM360 integration (cupixworks-worker) 1 Integration #5324 1건이 failed 상태로 전이됨. 해당 tenant의 BIM360 연동 작업이 사용자가 재인증할 때까지 중단됨.

Timeline#

  1. 2026-06-09 09:07:21 KSTCupix::Cron::Integration.renew_before_expiration 가 만료 예정 integration들을 처리.
  2. 2026-06-09 09:07:21 KSTBim360Operation.refresh_token 가 Autodesk Forge refresh_url로 POST 호출 중 TCP 연결 reset.
  3. 2026-06-09 09:07:21 KSTBim360Operation.refresh_tokenrescue StandardErrorCupix::Errors::BadGateway 로 raise. IntegrationRepository#refresh_token 이 integration 5324를 failed_state! 로 변경 후 재raise.
  4. 2026-06-09 09:07:21 KSTCupix::Cron::Integration.renew_before_expirationrescue StandardError[Cupix::Cron::Integration] failed to renew integration: Connection reset by peer 로그 후 다음 candidate로 진행.

Error Log#

Datadog Logs

text
BIM360 Authentication failed: Connection reset by peer

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 1
  • 최초 발생: 2026-06-09 09:07:21 KST
  • 최근 발생: 2026-06-09 09:07:21 KST
  • BIM360 integration 5324가 failed 상태로 전이됨. 사용자가 BIM360 재인증을 수행하기 전까지 해당 tenant의 BIM360 연동 워크플로우는 동작하지 않음.

Root Cause Summary#

Autodesk Forge OAuth refresh token 엔드포인트($OAUTH[:autodesk_forge][:site] + refresh_url)와의 TLS/TCP 연결이 원격 종단에 의해 reset되어 Ruby 표준 라이브러리에서 Errno::ECONNRESET("Connection reset by peer")가 raise되었다. Cupix::HttpClient.post는 HTTP 응답 기반 retriable 코드(429/502/503/504)에 대해서만 backoff retry를 수행하므로, TCP 레벨 예외는 retry 없이 즉시 호출자에게 전파된다. Bim360Operation.refresh_token은 이 예외를 그대로 Cupix::Errors::BadGateway로 wrap해 raise하며, IntegrationRepository#refresh_token은 사유와 무관하게 모든 StandardErrorfailed_state!로 영속화하기 때문에, transient한 단일 네트워크 실패만으로 integration이 failed 상태로 영구 전이되어 사용자 재인증이 강제된다.

Technical Analysis#

Code Path#

Entry point — 크론 스케줄에서 4시간마다 실행:

config/schedule.rb:91-97ruby
every '7 */4 * * *' do # 00:07,04:07,08:07 ...
  # runner 'Cupix::Cron::Record.consume_credits'
  runner 'Cupix::Cron::Floorplan.cleanup_creating_floorplans'
  runner 'Cupix::Cron::Integration.renew_before_expiration'
  runner 'Cupix::Cron::Review.flush_stale_reviews'
  runner 'Cupix::Cron::Record.flush_stale_records'
end

크론이 만료 임박 integration들을 순회하며 갱신:

lib/cupix/cron/integration.rb:1-17ruby
module Cupix::Cron
  class Integration < ::Integration
    def self.renew_before_expiration
      candidates = Integration.refresh_token_due_to_expire
      return nil if candidates.blank?

      candidates.each do |model|
        integration_repository = IntegrationRepository.new(model)
        integration_repository.refresh_token
      rescue StandardError => e
        Cupix::Logger.error("[Cupix::Cron::Integration] failed to renew integration: #{e.message}")

        next
      end
    end
  end
end

Repository는 provider 별 operation을 디스패치하고, 모든 StandardErrorfailed_state!로 처리:

app/repositories/integration_repository.rb:123-143ruby
begin
  unless check_refresh_request
    raise Cupix::Errors::Parameter.new(code: 'ARG10060', reason: 'Refresh access token request is too many')
  end

  token = operation_class.refresh_token(@model.refresh_token, @model.region)
  uncheck_refresh_request
rescue StandardError => e
  uncheck_refresh_request

  raise e if e.respond_to?(:code) && e.code == 'ARG10060'

  @model.refresh_token_failed_at = DateTime.now
  @model.refresh_token_expired_at = nil
  @model.refresh_token_response_body = token
  @model.failed_state!

  Cupix::Logger.error("[Integration] failed to refresh token for #{@model.provider} integration(#{@model.id}) - state: #{@model.state}, error_message: #{e.message}")

  raise e
end

Failure point — Bim360Operation.refresh_token의 generic StandardError 분기. RestClient가 ECONNRESET을 그대로 노출하므로 (RestClient::Exception이 아님) 두 번째 rescue로 빠져 BadGateway로 wrap됨:

app/operations/bim360_operation.rb:60-78ruby
begin
  url = "#{$OAUTH[:autodesk_forge][:site]}#{$OAUTH[:autodesk_forge][:refresh_url]}"
  response = Cupix::HttpClient.post(url, data, header)
rescue RestClient::Exception => e
  response = JSON.parse(e.response)

  Cupix::Logger.error("BIM360 refresh_token failed: #{response['developerMessage']} - error: #{e.message}")
  raise Cupix::Errors::Parameter.new(
    code: 'ARG10000',
    reason: "BIM360 Authentication failed: #{response['developerMessage']}"
  )
rescue StandardError => e
  Cupix::Logger.error("BIM360 Authentication failed: #{e.message}", class: self.name, function: __method__)
  raise Cupix::Errors::BadGateway.new(
    code: 'BG10001',
    reason: "BIM360 Authentication failed: #{e.message}",
    message: e.message
  )
end

HTTP wrapper는 HTTP status 기반 retry만 수행하며, transport-level 예외(Errno::ECONNRESET, OpenSSL::SSL::SSLError 등)는 retry 없이 즉시 raise:

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

기대 동작: 일시적 TCP reset과 같은 transient 네트워크 실패는 짧은 backoff와 함께 재시도되어 사용자 영향 없이 자동 복구되어야 함. 실제 동작: TCP-level 예외가 재시도 없이 그대로 전파되고, 동시에 IntegrationRepository가 integration을 즉시 failed 상태로 전이시켜 사용자 재인증을 요구함.

Log Evidence#

Datadog 쿼리 (cluster 파일에 기록된 동일 쿼리):

text
service:cupixworks-worker status:error @environment:production "BIM360 Authentication failed: Connection reset by peer"

이번 사건의 동시각 로그 3건 (모두 09:07:21 KST):

text
2026-06-09 09:07:21  error  service:cupixworks-worker
  message: "BIM360 Authentication failed: Connection reset by peer"
  class:    Bim360Operation
  function: refresh_token
text
2026-06-09 09:07:21  error  service:cupixworks-worker
  message: "[Integration] failed to refresh token for bim360 integration(5324) - state: failed, error_message: Connection reset by peer"
text
2026-06-09 09:07:21  error  service:cupixworks-worker
  message: "[Cupix::Cron::Integration] failed to renew integration: Connection reset by peer"

14일 retention 내 빈도 (service:cupixworks-worker "BIM360 Authentication failed"):

text
2026-06-09 09:07:21  bim360 integration(5324)  message: "...Connection reset by peer"
2026-06-01 09:07:19  bim360 integration(235)   message: "BIM360 Authentication failed:" (developerMessage 누락 — RestClient::Exception 분기, 이번 cluster의 fingerprint와 다름)

세 로그 모두 같은 호출 스택의 다른 레이어(Bim360Operation.refresh_tokenIntegrationRepository#refresh_tokenCupix::Cron::Integration.renew_before_expiration)에서 같은 메시지를 echo한 것으로, 단일 사건임이 확인됨. 14일 동안 동일 fingerprint(ECONNRESET 분기)는 1회만 발생.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Autodesk Forge OAuth 엔드포인트와의 transient TCP reset이 retry되지 않고 그대로 전파됨 로그 메시지가 "Connection reset by peer" (Errno::ECONNRESET). Cupix::HttpClient.post는 HTTP 4xx/5xx만 retry (lib/cupix/http_client.rb:8). Bim360Operation.refresh_token의 rescue StandardError 분기에서 BadGateway로 wrap (bim360_operation.rb:71-77). 14일 동안 동일 fingerprint 1회만 발생 → 일시적 사건. Confirmed
H2 refresh token이 만료되었거나 Autodesk가 거부한 것 (자격 증명 문제) refresh token 만료/거부는 보통 RestClient::Exception(HTTP 401/400)로 반환되며 developerMessage가 채워짐 (bim360_operation.rb:63-69). 이번 로그의 메시지는 OS-level 메시지("Connection reset by peer")이고 class:Bim360Operation function:refresh_token 태그가 있는 두 번째 rescue 분기에서 발생. developerMessage 없음. Rejected
H3 클라이언트 측 잘못된 요청 페이로드/헤더 14일 중 1회만 발생, 다른 BIM360 integration들은 정상 갱신됨 (만약 페이로드 문제면 같은 크론 사이클에서 모든 candidate가 실패해야 함). 같은 시각 다른 candidate에 대한 동일 에러 로그 없음. Rejected
H4 rate limiting (429) rate limit이라면 RestClient::Exception 분기로 들어가 developerMessage와 함께 retry되거나 ARG10000으로 raise됨. 메시지가 transport-level error message이고 RestClient 분기 로그가 아님. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

없음. 단발성 transient 사건이며 즉시 운영 조치는 불필요. 다만 integration 5324는 failed_state! 호출로 인해 failed 상태로 영속화되었으므로, 해당 tenant 사용자에게 BIM360 재인증을 안내하거나 운영자가 단발 transient 실패임을 확인 후 상태를 수동 복구할 수 있다.

단기 개선 (1주 이내)#

  • lib/cupix/http_client.rb:8-46Cupix::HttpClient.post/get/put/delete/patch의 retry 정책을 transport-level 일시적 예외까지 확장 검토. 구체적으로 Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout, RestClient::ServerBrokeConnection, OpenSSL::SSL::SSLError(일시 TLS 실패) 등을 추가 rescue로 감싸 동일한 exponential backoff로 재시도. 변경 시 idempotency가 보장되는 호출(GET, OAuth refresh)에 한정하거나 호출자가 명시적으로 opt-in하도록 한다.
  • app/repositories/integration_repository.rb:130-142 — transient 네트워크 실패와 자격 증명 거부를 구분하지 않고 모두 failed_state!로 전이하는 정책을 재검토. 예: Cupix::Errors::BadGateway(BG10001)는 transient 분류로 두고 상태 전이를 보류한 채 다음 cron에 재시도, Cupix::Errors::Parameter(ARG10000, developerMessage 동반)만 진짜 자격 증명 실패로 간주해 failed_state! 적용.
  • app/operations/bim360_operation.rb:32, 39, 72 — 발생 빈도와 영향(integration 단발 실패)을 감안해 단발 transient ECONNRESET을 error가 아닌 warn으로 다운그레이드하는 옵션 검토 (운영적 노이즈 감소). 단, integration이 실제로 failed_state!로 전이되는 한 error 유지가 정합적이므로, 위 단기 개선(상태 전이 분리)과 함께 적용해야 함.

장기 개선 (재발 방지)#

  • 외부 OAuth provider (Autodesk Forge, Procore, Plangrid, Revizto) 호출 전반에 대해 통합된 transient/permanent 분류 및 retry policy 도입 (e.g. Faraday middleware 또는 retriable gem 표준화).
  • BIM360을 비롯한 외부 OAuth integration의 갱신 실패에 대해 즉시 failed_state!로 보내지 않고 N회 연속 실패 또는 refresh_token_expired_at 도달 시점까지 grace period를 두는 상태 전이 모델 도입 (refresh_attempts_count 같은 별도 카운터).
  • Autodesk Forge 등 외부 dependency에 대한 통합 health 메트릭과 SLO 추적.

Monitoring#

  • 추가 메트릭: 외부 OAuth provider 별 refresh 실패율 / transient(ECONNRESET, timeout) 분리 카운트.
  • Datadog 알림 후보 쿼리:
text
service:cupixworks-worker @class:Bim360Operation @function:refresh_token status:error
text
service:cupixworks-worker "[Integration] failed to refresh token" status:error
  • 임계값 예시: 1시간 내 동일 provider에서 3건 이상 발생 시 경보 (단발 transient는 무시).

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 단발 transient 네트워크 사건이며 affected scope가 단일 integration(5324)에 국한됨. 다만 IntegrationRepository가 transient 실패에서도 failed_state!로 전이시키는 정책은 동일 패턴이 누적되면 사용자 재인증 강제 빈도를 늘릴 수 있어, transport-level retry와 상태 전이 분리는 함께 검토할 가치가 있음.