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-worker의 Cupix::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_token의 rescue 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:94 — every '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#
- 2026-06-09 09:07:21 KST —
Cupix::Cron::Integration.renew_before_expiration가 만료 예정 integration들을 처리. - 2026-06-09 09:07:21 KST —
Bim360Operation.refresh_token가 Autodesk Forgerefresh_url로 POST 호출 중 TCP 연결 reset. - 2026-06-09 09:07:21 KST —
Bim360Operation.refresh_token의rescue StandardError가Cupix::Errors::BadGateway로 raise.IntegrationRepository#refresh_token이 integration 5324를failed_state!로 변경 후 재raise. - 2026-06-09 09:07:21 KST —
Cupix::Cron::Integration.renew_before_expiration의rescue StandardError가[Cupix::Cron::Integration] failed to renew integration: Connection reset by peer로그 후 다음 candidate로 진행.
Error Log#
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은 사유와 무관하게 모든 StandardError를 failed_state!로 영속화하기 때문에, transient한 단일 네트워크 실패만으로 integration이 failed 상태로 영구 전이되어 사용자 재인증이 강제된다.
Technical Analysis#
Code Path#
Entry point — 크론 스케줄에서 4시간마다 실행:
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들을 순회하며 갱신:
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을 디스패치하고, 모든 StandardError를 failed_state!로 처리:
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됨:
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:
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 파일에 기록된 동일 쿼리):
service:cupixworks-worker status:error @environment:production "BIM360 Authentication failed: Connection reset by peer"
이번 사건의 동시각 로그 3건 (모두 09:07:21 KST):
2026-06-09 09:07:21 error service:cupixworks-worker
message: "BIM360 Authentication failed: Connection reset by peer"
class: Bim360Operation
function: refresh_token
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"
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"):
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_token → IntegrationRepository#refresh_token → Cupix::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-46—Cupix::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.
Faradaymiddleware 또는retriablegem 표준화). - 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 알림 후보 쿼리:
service:cupixworks-worker @class:Bim360Operation @function:refresh_token status:error
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와 상태 전이 분리는 함께 검토할 가치가 있음.