[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 state → failed, 사용자 재인증 필요 |
Timeline#
- 2026-07-13 13:07:19 KST — Cron
Cupix::Cron::Integration.renew_before_expiration실행 (매 4시간 :07 스케줄) - 2026-07-13 13:07:19 KST —
IntegrationRepository#refresh_token이Bim360Operation.refresh_token호출, Autodesk Forge 가 HTTP 400 Bad Request 응답 - 2026-07-13 13:07:19 KST —
RestClient::Exceptionrescue 진입,BIM360 refresh_token failed: - error: 400 Bad Request로그 기록 후Cupix::Errors::Parameter재발생 - 2026-07-13 13:07:19 KST —
integration_repository.rb:135-138에서refresh_token_failed_at갱신 및failed_state!로 상태 전이 - 2026-07-13 13:07:19 KST — Cron rescue 블록 (
integration.rb:10-11) 에서 로그 기록 후next로 다음 candidate 계속
Error Log#
[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 레코드 (
state→failed). 동일 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_expirescope 에서 제외 (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실행 - 실행 흐름:
lib/cupix/cron/integration.rb:3-4— 만료 임박 candidate 조회app/models/concerns/statable/integration.rb:12—refresh_token_due_to_expirescope 는not_failed상태 중refresh_token_expired_at < 1.day.since를 반환lib/cupix/cron/integration.rb:8-9— 각 candidate 에 대해IntegrationRepository#refresh_token호출app/repositories/integration_repository.rb:128—Bim360Operation.refresh_token(@model.refresh_token, @model.region)호출app/operations/bim360_operation.rb:60-62— Autodesk Forgerefreshtoken엔드포인트로 POST
- Failure point:
app/operations/bim360_operation.rb:63-70—RestClient::Exception(HTTP 400) rescue 진입
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
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
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
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 파일에 기록된 것):
service:cupixworks-migration-worker status:error @environment:production "[Cupix::Cron::Integration] failed to renew integration: BIM360 Authentication failed:"
Cron 실행 시점을 확인하는 upstream 쿼리:
service:cupixworks-migration-worker "BIM360 refresh_token failed"
해당 쿼리에서 확보한 원본 로그 (Autodesk 응답의 상세 상태):
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 포함):
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 문제임을 증명:
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-70 의 ARG10000 경로. 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_expirescope 가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:66의Cupix::Logger.error를Cupix::Logger.warn로 낮추는 것을 검토. Autodesk 측invalid_grant400 응답은 "예상되는 운영 시나리오" (사용자가 앱 authorization 을 취소한 경우) 이므로 error alert 를 유발할 필요가 없다. 근거: memory 의 "RCA Quality Patterns — Assess error severity during RCA" 항목.Cupix::Cron::Integration#renew_before_expiration의 catch-allrescue StandardError(lib/cupix/cron/integration.rb:10) 도 blanket error 로 남기지 말고Cupix::Errors::Parameter는warn으로, 그 외StandardError는error로 분리. - 로그 메시지 명확화:
bim360_operation.rb:66의 로그가developerMessageblank 일 때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:10의rescue StandardError를Cupix::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 (분당):
sum:trace.rack.request.hits{service:cupixworks-migration-worker,resource_name:*bim360*,http.status_code:400}.as_count()
Cron 실행 시 실패 로그 수 (분당) — 로그 기반 count:
logs("service:cupixworks-migration-worker status:error \"BIM360 Authentication failed\"").index("*").rollup("count").by("host").last("1d")
성공 대비 실패 비율 (분당):
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!전이 + cronnext로 회복), 서비스 전반에 영향 없음. 실질적 fix 는 (1) 로그 레벨 downgrade (2) 로그 메시지 정보 보강 (3) 사용자 재인증 유도 UX 뿐. 모든 항목은 backward-compatible 이며 breaking change 없음.