ES /docs

[Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:

RCA: [Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed

Overview#

What Happened#

BIM360 OAuth 토큰 갱신 cron (Cupix::Cron::Integration.renew_before_expiration) 이 2026-07-13 13:07 KST 실행 도중 특정 BIM360 integration 레코드의 refresh_token 을 Autodesk Forge 로 재발급 요청하다 HTTP 400 Bad Request 응답을 받고 실패했다. 실패한 integration 은 failed 상태로 전이되며 사용자가 재인증하기 전까지 BIM360 연동이 중단된다. 동일 cron 실행 안에서 다수의 다른 BIM360 integration 은 정상 갱신되었으므로 서비스 전반 장애가 아닌 개별 tenant 의 refresh_token 무효화 이슈다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (code ARG10000)
exception.message BIM360 Authentication failed: (Autodesk developerMessage 필드 비어 있음)
top_frame app/operations/bim360_operation.rb:66-69
runtime Rails on cupixworks-migration-worker (Elastic Beanstalk, service_role: migrationworker)
deploy production-us-west-2-20260713T0407Z0-4f01ffc0-cupixworks
env production, region us-west-2

Affected Teams#

Team / Domain Error Count Impact
BIM360 integration (Autodesk Forge OAuth) 1건 (본 cluster) / 최근 24h 동일 pattern 다수 (integrations 2332, 1160, 1163, 1428, 1234, 1405, 1473, 1482, 1472, 1470 등 10건+) 해당 tenant 의 BIM360 파일/모델 sync 중단, integration statefailed, 사용자 재인증 필요

Timeline#

  1. 2026-07-13 13:07:19 KST — Cron Cupix::Cron::Integration.renew_before_expiration 실행 (매 4시간 :07 스케줄)
  2. 2026-07-13 13:07:19 KSTIntegrationRepository#refresh_tokenBim360Operation.refresh_token 호출, Autodesk Forge 가 HTTP 400 Bad Request 응답
  3. 2026-07-13 13:07:19 KSTRestClient::Exception rescue 진입, BIM360 refresh_token failed: - error: 400 Bad Request 로그 기록 후 Cupix::Errors::Parameter 재발생
  4. 2026-07-13 13:07:19 KSTintegration_repository.rb:135-138 에서 refresh_token_failed_at 갱신 및 failed_state! 로 상태 전이
  5. 2026-07-13 13:07:19 KST — Cron rescue 블록 (integration.rb:10-11) 에서 로그 기록 후 next 로 다음 candidate 계속

Error Log#

Datadog Logs

text
[Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:

Impact#

  • Service: cupixworks-migration-worker
  • 발생 횟수: 1 (본 cluster, fingerprint 367521714360e5fe3ba3e4f08b20d99a)
  • 최초 발생: 2026-07-13 13:07 KST
  • 최근 발생: 2026-07-13 13:07 KST
  • 영향 범위: 개별 BIM360 integration 레코드 (statefailed). 동일 cron 실행 내 다른 BIM360 integration (예: 7295, 7298, 7299, 1579, 1581) 은 정상 갱신되었으므로 BIM360/Autodesk Forge 서비스 자체 장애가 아니라 tenant 별 refresh_token 무효화. Cupix::Cron::Integration.renew_before_expiration 은 매 4시간마다 계속 실행되며 동일 integration 이 failed 상태이면 refresh_token_due_to_expire scope 에서 제외 (not_failed 필터) 되므로 반복 실패는 발생하지 않는다.

Root Cause Summary#

Cupix::Cron::Integration.renew_before_expiration 이 만료 임박 BIM360 integration 들의 refresh_token 을 갱신하려고 Autodesk Forge OAuth v2 refreshtoken 엔드포인트를 호출했는데 특정 integration 의 refresh_token 이 이미 Autodesk 측에서 무효화되어 HTTP 400 Bad Request 응답을 받았다. 원인은 사용자가 Autodesk 계정에서 앱 authorization 을 취소했거나, refresh_token 이 마지막 사용 후 Autodesk 정책상 유효 기간을 초과했거나, 사용자가 비밀번호 변경/계정 상태 변경으로 인해 기존 grant 가 revoke 된 경우다. Cupix 코드는 400 응답을 정상적으로 rescue 하여 integration 을 failed 상태로 전이시키고 로그만 남기는 defensive path 를 이미 갖추고 있으나, Cupix::Logger.error 로 기록되어 error alert 를 유발한다. Autodesk 가 반환한 developerMessage 가 비어 있어 최종 로그 메시지가 BIM360 Authentication failed: 로 정보 없이 남는 것도 부수 이슈다.

Technical Analysis#

Code Path#

  • Entry point: config/schedule.rb:94 — cron 스케줄 every '7 */4 * * *'Cupix::Cron::Integration.renew_before_expiration 실행
  • 실행 흐름:
    1. lib/cupix/cron/integration.rb:3-4 — 만료 임박 candidate 조회
    2. app/models/concerns/statable/integration.rb:12refresh_token_due_to_expire scope 는 not_failed 상태 중 refresh_token_expired_at < 1.day.since 를 반환
    3. lib/cupix/cron/integration.rb:8-9 — 각 candidate 에 대해 IntegrationRepository#refresh_token 호출
    4. app/repositories/integration_repository.rb:128Bim360Operation.refresh_token(@model.refresh_token, @model.region) 호출
    5. app/operations/bim360_operation.rb:60-62 — Autodesk Forge refreshtoken 엔드포인트로 POST
  • Failure point: app/operations/bim360_operation.rb:63-70RestClient::Exception (HTTP 400) rescue 진입
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
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
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
app/operations/bim360_operation.rb:50-81ruby
def self.refresh_token(refresh_token, region)
  data = {
    grant_type: 'refresh_token',
    refresh_token: refresh_token,
    redirect_uri: $OAUTH[:autodesk_forge][:bim360][:redirect_uri]
  }
  header = {
    authorization: "Basic #{$OAUTH[:autodesk_forge][:bim360][:token]}"
  }

  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

  JSON.parse(response)
end

기대 동작 vs 실제 동작: 기대는 Autodesk Forge 가 유효한 refresh_token 에 대해 새 access_token/refresh_token 을 반환하는 것. 실제는 특정 integration 의 refresh_token 이 Autodesk 측에서 무효화되어 HTTP 400 Bad Request 응답. 응답 본문의 developerMessage 필드가 비어 있어 최종 로그 메시지가 BIM360 Authentication failed: (콜론 뒤 공백) 로 정보 없이 남는다.

Log Evidence#

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

text
service:cupixworks-migration-worker status:error @environment:production "[Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:"

Cron 실행 시점을 확인하는 upstream 쿼리:

text
service:cupixworks-migration-worker "BIM360 refresh_token failed"

해당 쿼리에서 확보한 원본 로그 (Autodesk 응답의 상세 상태):

text
2026-07-13 13:07:19 [error] BIM360 refresh_token failed:  - error: 400 Bad Request
2026-07-13 09:07:34 [error] BIM360 refresh_token failed:  - error: 400 Bad Request
2026-07-13 01:07:29 [error] BIM360 refresh_token failed:  - error: 400 Bad Request

관련 downstream log (integration id 포함):

text
2026-07-13 13:07:19 [error] [Integration] failed to refresh token for bim360 integration(2332) - state: failed, error_message: BIM360 Authentication failed:
2026-07-13 13:07:19 [error] [Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:

같은 cron 실행 내에서 정상 갱신된 BIM360 integration (동일 tenant/region, 동일 Autodesk 앱 client credentials 로 성공) — 개별 tenant 문제임을 증명:

text
2026-07-13 13:07:19 [info]  [Integration] Successfully refreshed token for bim360 integration(7299) - state: active,   expired_at: 2026-07-13 05:07:15 UTC, refresh_token_expired_at: 2026-07-27 04:07:16 UTC
2026-07-13 13:07:19 [info]  [Integration] Successfully refreshed token for bim360 integration(7298) - state: inactive, expired_at: 2026-07-13 05:07:14 UTC, refresh_token_expired_at: 2026-07-27 04:07:15 UTC
2026-07-13 13:07:19 [info]  [Integration] Successfully refreshed token for bim360 integration(7295) - state: active,   expired_at: 2026-07-13 05:07:14 UTC, refresh_token_expired_at: 2026-07-27 04:07:15 UTC

Cron 스케줄 매칭: log timestamp 04:07 / 08:07 / 12:07 (UTC) 이 config/schedule.rb:91'7 */4 * * *' 와 정확히 일치.

Status board 컨텍스트: 이 cluster 는 진행 중인 open incident 2026-07-13-svc-cupixworks-migration-worker--unknown-1 에 포함되어 있으며, 같은 서비스에서 3일 전 (2026-07-10 14:13 KST) 동일 root_cause_type 의 incident 가 이미 resolved 됨. 4일 이내 재발.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 특정 tenant 의 BIM360 refresh_token 이 Autodesk 측에서 무효화 (사용자 revoke / 계정 상태 변경 / 장기 미사용 만료) Autodesk 로부터 HTTP 400 Bad Request 응답 (BIM360 refresh_token failed: - error: 400 Bad Request); 같은 cron 실행 내 다른 BIM360 integration (7295, 7298, 7299, 1579, 1581) 은 정상 갱신 → Autodesk 서비스/네트워크/자격증명은 정상; refresh_token grant_type 에서 400 은 Autodesk 문서상 invalid/expired refresh_token 을 의미 Confirmed
H2 BIM360/Autodesk Forge 서비스 장애 (dep-level outage) 동시간대 error 발생 같은 cron 사이클에서 다른 BIM360 integration 이 성공적으로 refresh — 서비스 전체 장애면 모두 실패해야 함; status-board 도 svc:* scope 로 분류 (dep:* 아님) Rejected
H3 Cupix 앱 client credentials ($OAUTH[:autodesk_forge][:bim360][:token]) 만료/오설정 401/403 이 아니라 400 이므로 Basic Auth 자체는 통과했고 요청 파라미터 (refresh_token 값) 이 문제 성공 케이스 존재로 client credentials 는 유효 Rejected
H4 check_refresh_request (Redis setnx lock) 경합으로 ARG10060 (Too many requests) 발생 로그가 ARG10060 이 아닌 BIM360 Authentication failed (ARG10000); integration_repository.rb:133 에서 ARG10060 은 early return 되므로 failed_state! 를 태우지 않음 Rejected
H5 Bim360Operation.refresh_token 에서 token 응답 필드 부족으로 BG10001 발생 400 응답이므로 RestClient::Exception rescue 진입, bim360_operation.rb:63-70ARG10000 경로. BG10001 은 성공 응답 파싱 후에만 도달 (integration_repository.rb:151-153) Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 즉시 코드 변경 불필요. 현재 에러 처리 흐름은 이미 defensive 하다: 실패 시 integration 을 failed 상태로 전이 (app/repositories/integration_repository.rb:135-138), cron 은 rescue/next 로 계속 다음 candidate 처리 (lib/cupix/cron/integration.rb:10-13), refresh_token_due_to_expire scope 가 not_failed 만 선택하므로 반복 실패 없음.
  • 운영/사용자 커뮤니케이션: integration id: 2332 (본 cluster 로그의 대상) 를 소유한 team/facility 에게 BIM360 재인증 요청 알림. 24h 창에서 확인된 다른 실패 integration (1160, 1163, 1234, 1405, 1428, 1470, 1472, 1473, 1482 등) 도 동일 조치.
  • developerMessage 원문 확인: 현재 refresh_token_response_body 컬럼에 Autodesk 응답 본문 전체가 저장되므로 (integration_repository.rb:137) Rails console 에서 Integration.find(2332).refresh_token_response_body 로 실제 Autodesk error code (invalid_grant, AUTH-006 등) 확인 가능. 이를 근거로 사용자 안내 메시지 정제.

단기 개선 (1주 이내)#

  • 로그 레벨 재조정: app/operations/bim360_operation.rb:66Cupix::Logger.errorCupix::Logger.warn 로 낮추는 것을 검토. Autodesk 측 invalid_grant 400 응답은 "예상되는 운영 시나리오" (사용자가 앱 authorization 을 취소한 경우) 이므로 error alert 를 유발할 필요가 없다. 근거: memory 의 "RCA Quality Patterns — Assess error severity during RCA" 항목. Cupix::Cron::Integration#renew_before_expiration 의 catch-all rescue StandardError (lib/cupix/cron/integration.rb:10) 도 blanket error 로 남기지 말고 Cupix::Errors::Parameterwarn 으로, 그 외 StandardErrorerror 로 분리.
  • 로그 메시지 명확화: bim360_operation.rb:66 의 로그가 developerMessage blank 일 때 BIM360 refresh_token failed: - error: 400 Bad Request 로 정보 손실. Autodesk 응답 body 전체 (또는 errorCode/userMessage 등 fallback 필드) 를 로그에 포함하도록 개선.
  • 원인별 세분 처리: Autodesk 응답 JSON 의 errorCode (예: AUTH-006, invalid_grant) 를 파싱해 refresh_token_response_body 에 저장하는 것 외에 별도 컬럼 (예: last_failure_code) 으로 분리해 관리자 UI/알림에서 재인증 안내 문구를 정확히 노출.

장기 개선 (재발 방지)#

  • 사용자 재인증 유도 플로우: integration state == 'failed' 상태가 되었을 때 해당 tenant 사용자에게 in-app notification / 이메일 자동 발송 (Cupix::Cron 이 아닌 별도 job). BIM360 앱 authorization 취소는 사용자 개입 없이 복구 불가능하므로 사용자에게 즉시 알리는 것이 유일한 실사용 복구 경로.
  • 모니터링 신규 지표: 아래 Monitoring 섹션 참조. 개별 실패는 error alert 를 유발하지 않고 실패율/실패 tenant 수 이 급증할 때만 alert 하도록 dashboard 구성.
  • cron rescue 세분화: lib/cupix/cron/integration.rb:10rescue StandardErrorCupix::Errors::Parameter (예상되는 사용자측 원인) 와 StandardError (예상 못한 시스템 원인) 로 분리해 이 두 경로가 별도 로그 레벨/알람을 갖도록 구성. 근거: memory 의 "Scope warn-level downgrades to the specific exception class" 원칙.

Monitoring#

  • BIM360 refresh 실패율 (tenant/시간 단위)
  • 실패한 integration ID 목록을 slack/이메일로 daily digest

Datadog 쿼리 예시 (각 쿼리는 dashboard timeseries widget 에 그대로 삽입 가능):

BIM360 refresh 실패 count (분당):

text
sum:trace.rack.request.hits{service:cupixworks-migration-worker,resource_name:*bim360*,http.status_code:400}.as_count()

Cron 실행 시 실패 로그 수 (분당) — 로그 기반 count:

text
logs("service:cupixworks-migration-worker status:error \"BIM360 Authentication failed\"").index("*").rollup("count").by("host").last("1d")

성공 대비 실패 비율 (분당):

text
logs("service:cupixworks-migration-worker \"Successfully refreshed token for bim360\"").index("*").rollup("count").last("1d")

성공 refresh count 를 별도 timeseries 로 놓고 실패 count 와 함께 표시. Alert threshold 는 "성공 대비 실패율 > 20% (10분 창)" 을 monitor 로 별도 구성 (dashboard 위젯에는 threshold suffix 를 넣지 않음).

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial
  • 이유: 코드 로직상 개별 tenant 의 refresh_token 무효화는 이미 defensive 하게 처리되며 (integration failed_state! 전이 + cron next 로 회복), 서비스 전반에 영향 없음. 실질적 fix 는 (1) 로그 레벨 downgrade (2) 로그 메시지 정보 보강 (3) 사용자 재인증 유도 UX 뿐. 모든 항목은 backward-compatible 이며 breaking change 없음.