ES /docs

CPBim::downloadOriginal | end - undefined forgeUrn or path

RCA: CPBim::downloadOriginal | end - undefined forgeUrn or path

Overview#

What Happened#

2026-04-23 15:05:22 UTC에 cupixworks-any-thumbnail-agent 서비스에서 BIM ID 19214의 썸네일 생성 중 forge_urnundefined인 상태로 downloadOriginal이 호출되어 에러가 발생했다. 동일 시간대 같은 팀(aec-on)의 다른 BIM 모델(19209, 19211, 19213)은 정상적으로 썸네일이 생성되었다.

Quick Facts#

Field Value
exception.message CPBim::downloadOriginal | end - undefined forgeUrn or path
top_frame cpbim.ts:52
env production, us-west-2

Affected Teams#

Team / Domain Error Count Impact
aec-on (team 657) 1 BIM 19214의 썸네일이 생성되지 않음. 사용자 뷰어에서 BIM 미리보기 이미지 누락

Timeline#

  1. 2026-04-23T15:03:14Z — BIM 19214 레코드 생성 (Kibana created_at)
  2. 2026-04-23T15:05:22Z — Thumbnail agent가 SQS 메시지 수신, CPBim::downloadOriginal 에러 발생
  3. 2026-04-23T15:05:26Z — BIM 19214 레코드 업데이트 (updated_at) — forge_state가 translated로 변경되고 forge_urn 설정 완료
  4. 2026-04-24 — Error sweeper에서 감지, RCA 수행

Error Log#

Datadog Logs

text
CPBim::downloadOriginal | end - undefined forgeUrn or path

Impact#

  • Service: cupixworks-any-thumbnail-agent
  • Team: aec-on (id: 657)
  • 발생 횟수: 1
  • 최초 발생: 2026-04-23T15:05:22.440Z
  • 최근 발생: 2026-04-23T15:05:22.440Z

Root Cause Summary#

BIM의 forge_statetranslated 전환 시 update_thumbnail (SQS 메시지 발송)이 update_last_bim_revsion (forge_urn 설정)보다 먼저 실행되는 race condition이 근본 원인이다. Tesla(Rails) 측 BIM 상태 머신에서 forge_statetranslatedupdate_thumbnail을 호출하지만, forge_urn은 BIM의 statedone 전환 시 update_last_bim_revsion에서 BimRevision에서 복사된다. forge_state 전환의 after_transition callback이 main state 머신에 forge_translated_state 이벤트를 발생시키지만, 해당 이벤트가 main state machine에 정의되어 있지 않아 :done 전환이 이 경로에서는 발생하지 않는다. :done 전환은 이후 BimRevision state → :done 에서 별도로 트리거되므로, 썸네일 SQS 메시지가 forge_urn이 아직 null인 상태에서 전송된다.

Technical Analysis#

Code Path#

1. BimRevision forge_state → translated (트리거)

BimRevision의 forge_state가 translated로 전환되면, BIM의 forge_state에 동일한 이벤트를 전파한다:

app/models/concerns/statable/bim_revision.rb:137-138ruby
after_transition from: any, to: %i[queued translating translated translation_skipped] do |bim_revision, transition|
  bim_revision.bim.fire_events((transition.to + '_forge_state').to_sym)
end

2. BIM forge_state → translated (after_transition callbacks)

BIM의 forge_state:translated로 전환되면 두 개의 after_transition callback이 정의 순서대로 실행된다:

app/models/concerns/statable/bim.rb:168-178ruby
after_transition from: any, to: any - [:created] do |bim, transition|
  if bim.bim_revisions.size == 1
    bim.fire_events(('forge_' + transition.to + '_state').to_sym)
  else
    bim.fire_events(:in_revision_state) unless bim.state_in_revision?
  end
end

after_transition from: any, to: %i[translated translation_skipped] do |bim, transition|
  bim.update_thumbnail
end

첫 번째 callback (line 168-174)이 먼저 실행되어 fire_events(:forge_translated_state)를 호출하지만, BIM의 main state 머신에 forge_translated라는 event가 정의되어 있지 않다:

app/models/concerns/statable/bim.rb:14-27ruby
state_machine :state, initial: :initializing, namespace: :state do
  state :initializing,
        :created,
        :resource_uploading,
        :resource_missing,
        :resource_uploaded,
        :forge_queued,
        :forge_started,
        :forge_translating,   # forge_translating은 있지만
        :in_revision,
        :revision_error,
        :error,
        :done do              # forge_translated 이벤트는 없음
  end

따라서 fire_events(:forge_translated_state)no-op이 되고, BIM state는 :done으로 전환되지 않는다.

두 번째 callback (line 176-178)이 이어서 실행되어 update_thumbnail을 호출한다.

3. SQS 메시지 발송 (forge_urn이 null인 상태)

app/models/concerns/thumbnailable/agent.rb:30-37ruby
def update_thumbnail!(validate: true)
  return if self.respond_to?(:soft_copied?) && self.soft_copied?

  check_thumbnail_updatable if validate
  session = self.user.agent_team_session(self.team)
  client = Cws::ThumbnailAgent::Client.new(model: self, session: session)
  update(thumbnail_update_queued_at: DateTime.now)
  client.invoke!
end
lib/cws/thumbnail_agent.rb:35-40ruby
else
  body[:model] = {
    id: @model.id,       # BIM ID만 전달, forge_urn은 포함되지 않음
    type: @model.class.name
  }
end

4. Thumbnail Agent에서 API 조회 (forge_urn이 아직 null)

packages/cupix-tesla-thumbnail-agent/src/model/cpbim.ts:20-28typescript
setModel = async (): Promise<boolean> => {
    logger.debug('CPBim::setModel | begin');
    logger.debug('CPBim::setModel | model id: %d, type: %s', this.modelId, this.modelType);
    if (this.modelId == undefined || this.modelType == undefined || this.cupixApi == undefined) {
        logger.error('CPBim::setModel | end - undefined id or type or cupixApi');
        return false;
    }
    this._serverModel = await this.getBimById(this.modelId);
    if (this._serverModel == undefined) return false;

API에서 BIM을 조회하지만, 이 시점에 forge_urn은 아직 BIM 레코드에 설정되지 않은 상태이다.

5. Failure point — forge_urn undefined 검증 실패

packages/cupix-tesla-thumbnail-agent/src/model/cpbim.ts:45-54typescript
downloadOriginal = (): Promise<void> => new Promise((resolve, reject) => {
    const serverModel = this._serverModel;
    const forgeUrn = serverModel && (<any>serverModel).forge_urn;  // null — 아직 설정 안됨
    const forgeRegion = serverModel && (<any>serverModel).forge_region ? serverModel.forge_region : Environment.ADF_REGION;
    logger.debug('CPBim::downloadOriginal | begin');
    logger.debug('CPBim::downloadOriginal | forge urn: %s, path: %s', forgeUrn, this._originalFilePath);
    if (forgeUrn == undefined || this._originalFilePath == undefined) {
        logger.error('CPBim::downloadOriginal | end - undefined forgeUrn or path');
        return reject();   // ← 에러 발생 지점
    }

6. forge_urn이 설정되는 시점 (에러 이후)

BIM state → :done은 BimRevision state → :done에서 트리거된다:

app/models/concerns/statable/bim_revision.rb:88-95ruby
after_transition from: any, to: :done do |bim_revision, transition|
  if bim_revision.bim.bim_revisions.size == 1
    bim_revision.send_upload_web_to_case
  else
    bim_revision.send_reupload_web_to_case
  end

  bim_revision.bim.fire_events(:done_state) unless bim_revision.bim.state_done?
end

BIM state → :done 전환 시 forge_urn이 최종 설정된다:

app/models/concerns/statable/bim.rb:88-89,264-298ruby
after_transition from: any, to: :done do |bim, transition|
  bim.update_last_bim_revsion     # 여기서 forge_urn 복사
  # ...
end

def update_last_bim_revsion
  # ...
  self.forge_urn = _last_bim_revision&.forge_urn           # line 290
  self.forge_region = _last_bim_revision&.forge_region       # line 291
  self.forge_format_type = _last_bim_revision&.forge_format_type # line 292
  self.last_bim_revision = _last_bim_revision
  # ...
end

Log Evidence#

에러 로그 조회에 사용한 Datadog 쿼리:

text
service:cupixworks-any-thumbnail-agent status:error "CPBim::downloadOriginal"
Time: 2026-04-23T14:00:00Z to 2026-04-23T16:00:00Z

BIM 19214 관련 전체 로그 (시간순):

text
2026-04-24 00:05:22 KST [info]  BaseService::runByMessage | id: 19214, key: undefined
2026-04-24 00:05:22 KST [info]  ThumbnailService::newModel | model id: 19214, type: Bim
2026-04-24 00:05:22 KST [error] CPBim::downloadOriginal | end - undefined forgeUrn or path
2026-04-24 00:05:22 KST [error] BaseService::handlingMessageErrors | Error and message object - {"error":"undefined error","sqsMessage":{"MessageId":"5bc8eba4-a0b7-4ade-9c53-154cdeca2f10","Attributes":{"ApproximateReceiveCount":"1"}}}

모든 로그가 동일 초(15:05:22Z)에 발생 — setModel에서 API 조회 → downloadOriginal에서 forge_urn null 확인 → 즉시 reject.

같은 시간대 성공한 BIM (비교):

text
2026-04-24 00:05:31 KST [info]  BaseService::runByMessage | id: 19209, key: undefined
2026-04-24 00:05:31 KST [info]  ThumbnailService::newModel | model id: 19209, type: Bim
2026-04-24 00:05:32 KST [info]  ImageCPObject::generateThumbnail | begin
2026-04-24 00:05:32 KST [info]  ImageCPObject::generateThumbnail | end - output: /tmp/workspace/19209/19209_thumbnail.jpg, size: 4451

2026-04-24 00:05:33 KST [info]  BaseService::runByMessage | id: 19213, key: undefined
2026-04-24 00:05:34 KST [info]  ImageCPObject::generateThumbnail | end - output: /tmp/workspace/19213/19213_thumbnail.jpg, size: 5155

2026-04-24 00:05:36 KST [info]  BaseService::runByMessage | id: 19211, key: undefined
2026-04-24 00:05:36 KST [info]  ImageCPObject::generateThumbnail | end - output: /tmp/workspace/19211/19211_thumbnail.jpg, size: 3508

BIM 19209, 19213, 19211은 모두 generateThumbnail까지 정상 진행됨. BIM 19214만 downloadOriginal에서 실패.

Kibana에서 BIM 19214의 타임라인:

json
{
  "id": 19214,
  "state": "done",
  "forge_state": "translated",
  "created_at": "2026-04-23T15:03:14.745Z",
  "updated_at": "2026-04-23T15:05:26.342Z"
}

created_at(15:03:14) → 에러(15:05:22) → updated_at(15:05:26): 에러 발생 4초 후 BIM 레코드가 업데이트됨. 이는 update_last_bim_revsionforge_urn을 설정하며 save한 시점과 일치한다.

동일 시간대 Bim not found 에러도 발생 (BIM 19197-19200):

text
2026-04-23 23:50:53~55 KST [error] BaseService::handlingMessageErrors | "statusCode":403, "code":"ENT4000", "reason":"Bim not found"

이들은 별도 이슈(삭제된 BIM에 대한 썸네일 요청)로, 본 건과 무관하다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Race condition: SQS 메시지가 forge_urn 설정 전에 발송됨 BIM updated_at(15:05:26)이 에러(15:05:22)보다 4초 후. forge_state after_transitionupdate_thumbnailforge_urn 설정 전에 호출. BIM main state에 forge_translated event 미정의 → :done 전환 불가 → update_last_bim_revsion 미실행 Confirmed
H2 BIM 레코드 자체가 존재하지 않거나 API 조회 실패 setModelfalse를 반환했다면 downloadOriginal이 호출되지 않음 (thumbnail-service.ts:75-77). 에러가 downloadOriginal에서 발생했으므로 setModel은 성공 Rejected
H3 Autodesk Forge 서비스 장애로 forge_urn이 영구적으로 미설정 Kibana에서 BIM 19214의 현재 forge_state: "translated", state: "done" 확인. 최종적으로 정상 완료됨 Rejected
H4 _originalFilePath가 undefined (path 문제) 에러 메시지가 "undefined forgeUrn or path"로 둘 중 하나일 수 있음 setModel 성공 시 line 36에서 _originalFilePath가 반드시 설정됨. forgeUrn이 null인 것이 주요 원인 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: app/models/concerns/statable/bim.rb:176-178
  • update_thumbnail 호출을 forge_statetranslated 시점이 아닌 BIM statedone 전환 시점으로 이동해야 한다. statedone 전환 시 update_last_bim_revsion이 먼저 실행되어 forge_urn이 설정된 후 썸네일 에이전트가 호출되므로 race condition이 해소된다.
  • 대안: after_transition from: any, to: :done callback (line 88-94)에 bim.update_thumbnail을 추가하고, forge_statetranslated 에서의 update_thumbnail 호출(line 176-178)을 제거한다.

단기 개선 (1주 이내)#

  • Thumbnail agent 측 retry 로직 추가: cpbim.ts:51-53에서 forge_urn이 undefined일 때 즉시 reject하는 대신, 짧은 delay 후 API를 재조회하는 retry 메커니즘을 추가한다. 이는 SQS 메시지 순서가 보장되지 않는 환경에서 방어적 코딩이 된다.
  • 에러 레벨 조정: 현재 logger.error로 기록하지만, 이는 일시적 timing 이슈이므로 logger.warn으로 변경하고 retry 후에도 실패할 때만 error로 기록하는 것이 적절하다.

장기 개선 (재발 방지)#

  • BIM state machine에서 forge_translated event를 main state machine에 추가하여, forge 번역 완료 → :done 전환이 forge_state callback 체인 내에서 동기적으로 실행되도록 한다. 현재는 이 이벤트가 no-op이어서 별도의 BimRevision :done 전환에 의존하는 구조적 문제가 있다.
  • SQS 메시지에 forge_urn을 포함하여 전달하는 방안도 검토할 수 있다. 현재 메시지에는 model.idmodel.type만 포함되어 있어 agent가 별도로 API를 조회해야 한다.

Monitoring#

  • 기존 에러 모니터링 유지:
text
service:cupixworks-any-thumbnail-agent status:error "CPBim::downloadOriginal"
  • 수정 후 해당 에러 발생 빈도가 0으로 감소하는지 확인
  • 추가 모니터링:
text
service:cupixworks-any-thumbnail-agent status:error "undefined forgeUrn"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 썸네일 생성 실패는 사용자 경험에 영향을 주지만, BIM 데이터 자체의 무결성이나 핵심 기능에는 영향 없음. 재시도 메커니즘이 없어 수동으로 썸네일 업데이트를 트리거해야 복구 가능.