ES /docs

Exception occurred at set_upload_state. from: upload_done, to: upload_done, state_updated_at: 2026-0

RCA: Exception occurred at set_upload_state (upload_done to upload_done)

Error Log#

Datadog Logs

text
Exception occurred at set_upload_state. from: upload_done, to: upload_done, state_updated_at: 2026-04-13 07:23:59 UTC

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 2
  • 최초 발생: 2026-04-13T07:24:02.174Z
  • 최근 발생: 2026-04-13T07:51:22.078Z

Root Cause Summary#

클라이언트가 이미 upload_done 상태인 Capture에 대해 upload_state: 'upload_done'으로 PUT 요청을 보내면서 발생한 중복 상태 전이 오류입니다. set_upload_state 메서드(parameter/capture.rb:119)에서 동일 상태 전이를 명시적으로 차단하는 guard 조건(upload_state_upload_done?)에 의해 Cupix::Errors::InvalidState (code: STAT40000)가 발생하며, rescue 블록에서 에러 로그가 기록됩니다. 이는 클라이언트 측에서 upload 완료 API를 중복 호출(retry 또는 race condition)하는 것이 원인입니다.

Technical Analysis#

Code Path#

  • Entry point (API): app/controllers/api/v1/captures_controller.rb:46update 액션이 repository_instance.update(params)를 호출합니다.
ruby
# app/controllers/api/v1/captures_controller.rb:46-49
def update
  @model = repository_instance.update(params)
  super
end
  • Repository: app/repositories/capture_repository.rb:182-194update 메서드가 set_parameters(params)를 호출합니다.
ruby
# app/repositories/capture_repository.rb:182-194
def update(params = {})
  super
  set_parameters(params)
  begin
    @model.save!
  rescue StandardError => e
    raise Cupix::Errors::Parameter.new(code: 'ARG10001', reason: 'Invalid argument', message: e.message)
  end
  @model
end
  • Parameter dispatch: app/concerns/parameter/capture.rb:17-19set_parametersset_upload_state(params)를 호출합니다.
ruby
# app/concerns/parameter/capture.rb:17-19
def set_parameters(params = {})
  set_error_code(params)
  set_upload_state(params)
  • Failure point: app/concerns/parameter/capture.rb:119 — 이미 upload_done 상태인 모델에 대해 guard 조건이 true를 반환하여 InvalidState 예외를 발생시킵니다.
ruby
# app/concerns/parameter/capture.rb:116-131
when 'upload_done'
  raise Cupix::Errors::InvalidState.new(code: 'STAT10000', reason: 'At least 1 pano or video is required to make capture as upload_done state') unless @model.processible_materials_exist?

  raise Cupix::Errors::InvalidState.new(code: 'STAT40000', reason: 'State not changed') if @model.upload_state_upload_done?

  @model.upload_done_upload_state!
when 'pano_uploading'
  # ...
end
rescue Cupix::Errors::InvalidState => e
  Cupix::Logger.error("Exception occurred at set_upload_state. from: #{@model.upload_state}, to: #{params[:upload_state]}, state_updated_at: #{@model.state_updated_at}")
  raise e
  • State machine 정의: app/models/concerns/statable/capture.rb:197-270upload_state state machine은 transition all => :upload_done을 허용하지만, application layer에서 동일 상태 전이를 차단합니다.
ruby
# app/models/concerns/statable/capture.rb:224-226
event :upload_done do
  transition all => :upload_done, if: :processible_materials_exist?
end

기대 동작: 클라이언트가 upload_state: 'upload_done'을 요청할 때, Capture가 upload_ready 또는 다른 이전 상태에서 upload_done으로 전이됩니다.

실제 동작: Capture가 이미 upload_done 상태인데 동일한 전이 요청이 들어와서, line 119의 guard 조건(upload_state_upload_done?)이 true를 반환하고 STAT40000 에러가 발생합니다.

Log Evidence#

두 건의 에러 로그를 Datadog에서 확인했습니다.

검색 쿼리:

text
service:cupixworks-api status:error "set_upload_state"

에러 로그 1 (07:24:02.174Z):

text
Exception occurred at set_upload_state. from: upload_done, to: upload_done, state_updated_at: 2026-04-13 07:23:59 UTC
  • Host: ip-10-1-144-228.us-west-2.compute.internal
  • Request ID: 412ed36e-300b-4f0c-8443-38ad13bb0044
  • Region: us-west-2, Tenant: cupix

에러 로그 2 (07:51:22.078Z):

text
Exception occurred at set_upload_state. from: upload_done, to: upload_done, state_updated_at: 2026-04-13 07:51:20 UTC
  • Host: ip-10-1-80-134.us-west-2.compute.internal
  • Request ID: f0d86735-1d55-4e20-b297-d1f92e3e743a
  • Region: us-west-2, Tenant: cupix

정상 전이 로그 (동일 시간대, 50건 확인):

검색 쿼리:

text
service:cupixworks-api status:info "upload_state changed"

정상적인 전이는 모두 upload_ready → upload_done 패턴을 따릅니다:

text
[Capture] upload_state changed from upload_ready to upload_done
  • class_name: "Capture", function: "log_event", remark: "uploading_finished"
  • 에러 로그에는 이러한 structured attributes(class_name, function, cupix_trace_id)가 없음 — 정상 전이 콜백이 아닌 rescue 블록에서 로그가 발생했기 때문

주요 관찰 사항:

  • 두 번째 에러 발생 14초 후(07:51:36Z) 동일 호스트(ip-10-1-80-134)에서 정상 upload_ready → upload_done 전이가 성공 (model_id: 679510). 이는 시스템이 정상 동작 중에 일부 중복 요청만 실패한 것을 보여줍니다.
  • 두 에러 모두 us-west-2 리전, cupix tenant에서만 발생했습니다.
  • state_updated_at 값이 에러 발생 시각보다 2~3초 이전이며, 이는 이전 성공 전이 시점의 timestamp입니다.

Fix Recommendation#

즉시 조치 (Critical)#

해당 없음 — 이 에러는 의도된 방어 로직입니다. STAT40000 에러 코드로 동일 상태 전이를 차단하는 것은 올바른 동작이며, 데이터 무결성에 영향을 주지 않습니다.

단기 개선 (1주 이내)#

  • 로그 레벨 변경: app/concerns/parameter/capture.rb:130에서 Cupix::Logger.errorCupix::Logger.warn으로 변경하는 것을 고려합니다. 이 상황은 클라이언트의 중복 요청이며, 시스템 에러가 아닌 경고 수준의 이벤트입니다. error 레벨로 로깅하면 불필요한 에러 알림이 발생합니다.
  • Idempotent 처리 검토: 클라이언트가 동일 상태 전이를 요청할 때 에러 대신 현재 상태를 성공으로 반환하는 idempotent 패턴 적용을 검토합니다. 이미 upload_done 상태라면 추가 작업 없이 200 응답을 반환하면 됩니다.

장기 개선 (재발 방지)#

  • 클라이언트 중복 호출 원인 파악: 클라이언트(SDK 또는 프론트엔드)가 upload 완료 API를 중복 호출하는 이유를 조사합니다. 네트워크 retry, 사용자 더블 클릭, 또는 비동기 callback 중복이 원인일 수 있습니다.
  • 구조화된 로깅: 에러 로그에 class_name, model_id, cupix_trace_id 등의 structured attributes를 추가하여 디버깅을 용이하게 합니다.

Monitoring#

  • 이 에러의 빈도를 모니터링하여 특정 클라이언트나 Capture에서 반복적으로 발생하는지 추적:
text
service:cupixworks-api status:error "set_upload_state" "upload_done, to: upload_done"
  • 로그 레벨 변경 후에는 warn 레벨로 모니터링:
text
service:cupixworks-api status:warn "set_upload_state" "State not changed"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — 로그 레벨 변경은 1줄 수정, idempotent 처리는 guard 조건의 동작 변경(에러 대신 early return)으로 구현 가능