ES /docs

StateMachines::InvalidTransition: Cannot transition cycle_state via :deleting from :deleted (Reason(s): Cycle state cann

RCA: StateMachines::InvalidTransition on cycle_state

Overview#

What Happened#

cupixworks-api(tesla) 의 리소스 삭제/보관 API(PUT/DELETE /api/v1/levels|facilities|floorplans/...) 에서 StateMachines::InvalidTransition 이 발생한다. Cyclable state machine 의 cycle_state 전이가 (1) 이미 종료된 상태(예: 이미 archived 인 리소스를 다시 archive)이거나 (2) before_transition 단계의 모델 validation(레벨 이름 중복, elevation 겹침)에 걸려 거부될 때 예외가 raise 되며, 이는 client_error_controller.rb 에서 HTTP 400 으로 정상 매핑된다. Error Tracking 상 대표 메시지(:deleting from :deleted)는 STALE 이며, 14일 retention 창의 실제 발생은 :archiving from :archived / :trashing from :created / :purging from :trashed 등 다른 변형이다.

Quick Facts#

Field Value
exception.class StateMachines::InvalidTransition
exception.message (representative, STALE) Cannot transition cycle_state via :deleting from :deleted (Reason(s): Cycle state cannot transition via "deleting")
exception.message (current) Cannot transition cycle_state via :archiving from :archived / :trashing from :created / :purging from :trashed (Reason(s) 다양)
top_frame app/models/concerns/cyclable.rb (state_machine cycle_state)
HTTP status 400 (mapped STAT10001 / Cupix::Errors::InvalidState)
env production (cupixworks-api)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (tesla) — Level/Facility/Floorplan 삭제·보관 50 (retention 창 샘플) 사용자에게 400 반환. 재시도·중복 클릭 또는 유효성 위반 안내. 서버 오류 아님

Timeline#

  1. 2025-02-05 16:18 KST — 최초 발생(first_seen). 대표 메시지 :deleting from :deleted 로 고정됨.
  2. 2026-07-30 04:26 KST 전후DELETE /api/v1/levels/.../purge 에서 :purging from :trashed (elevation overlap) 다수 발생.
  3. 2026-07-31 16:46 KST — 마지막 발생(last_seen). PUT /api/v1/levels/23297/trash :trashing from :created (name 중복).
  4. 2026-08-03 16:50 KST — retention 창 내 최신, PUT /api/v1/floorplans/22346/archive :archiving from :archived.

Error Log#

Datadog Logs

text
Cannot transition cycle_state via :deleting from :deleted (Reason(s): Cycle state cannot transition via "deleting")

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 37 (Error Tracking 집계)
  • 최초 발생: 2025-02-05 16:18 KST
  • 최근 발생: 2026-07-31 16:46 KST

Root Cause Summary#

Cyclable concern 의 state_machine :cycle_state (app/models/concerns/cyclable.rb:48-169) 는 각 이벤트(trashing/purging/deleting/archiving/created)마다 특정 from 상태에서만 전이를 허용한다. 클라이언트가 (1) 이미 목적 상태에 도달한 리소스에 같은 액션을 다시 요청하거나(예: 이미 archived 인 facility 를 PUT .../archive), (2) before_transition / before_validation 단계의 모델 validation(Level 이름 중복, elevation 겹침 등)에 걸리는 전이를 요청하면, state_machine 의 bang 이벤트 메서드(archiving_cycle_state!, deleting_cycle_state! 등)가 StateMachines::InvalidTransition 을 raise 한다. 이 예외는 client_error_controller.rb:19,37-39rescue_from StateMachines::InvalidTransition → invalid_transition_400_error 에 의해 STAT10001(Cupix::Errors::InvalidState) / HTTP 400 으로 이미 올바르게 매핑되어 클라이언트에게 반환된다. 서버 코드 결함이 아니라 클라이언트 입력(중복 요청 / 유효성 위반)에 대한 정상적인 거부이며, APM 이 예외를 error-tag 하기 때문에 Error Tracking 에 집계될 뿐이다.

Technical Analysis#

Code Path#

  • Entry point: PUT/DELETE /api/v1/levels|facilities|floorplans/{id}/{trash|purge|archive} (retention 로그에서 확인된 controller: Api::V1::LevelsController#trash|purge|untrash, Api::V1::FacilitiesController#archive, Api::V1::FloorplansController#archive)
  • Repository: CyclableRepository#trash|purge|archive → 모델의 bang 메서드 호출
app/repositories/concerns/cyclable_repository.rb:6-32ruby
def trash
  check_deletable_permission
  @model.cycle_state_updated_by_id = current_user.id if @model.has_attribute?(:cycle_state_updated_by_id)
  @model.trash!
end
# ...
def archive
  check_archive_permission
  @model.cycle_state_updated_by_id = current_user.id if @model.has_attribute?(:cycle_state_updated_by_id)
  @model.archive!
end
  • 전이 정의: 각 이벤트는 특정 from 상태만 허용한다. archivingcreated 에서만, deleting/purgingcreated 에서만 시작할 수 있고, 이미 종료 상태(archived, deleted, trashed)에는 재전이 경로가 없다.
app/models/concerns/cyclable.rb:74-99ruby
event :deleting do
  transition created: :purging, if: ->(model) { model.class.untrashable? }
  transition created: :deleting
end

event :archiving do
  transition from: :created, to: :archiving
end

event :archived do
  transition %i[archiving] => :archived
  transition created: :archived
end
  • Failure point: bang 이벤트 메서드(archiving_cycle_state! / deleting_cycle_state! 등)가 유효한 전이가 없거나 before_transition/validation 이 halt 되면 StateMachines::InvalidTransition 을 raise. Cyclable#trash!(cyclable.rb:293-310) 은 deleting_cycle_state! 를 bang 으로 호출한다.
app/models/concerns/cyclable.rb:293-306ruby
def trash!
  set_cycle_state
  if skip_trash?
    purge!
  else
    run_callbacks :trash do
      if self.class.untrashable?
        trashing_cycle_state!
      else
        deleting_cycle_state!   # ← 유효 전이 없거나 validation halt 시 InvalidTransition raise
      end
    end
  end
  • 매핑: 예외는 controller 에서 400 client error 로 변환된다.
app/controllers/concerns/client_error_controller.rb:19,37-39ruby
rescue_from StateMachines::InvalidTransition, with: :invalid_transition_400_error
# ...
def invalid_transition_400_error(exception)
  raise_error(400, exception, code: 'STAT10001', type: Cupix::Errors::InvalidState, reason: 'Cannot complete state transition', message: exception.message)
end
  • 기대 동작: 사용자가 유효한 상태의 리소스에 삭제/보관을 요청하면 전이 성공.
  • 실제 동작: 이미 전이 완료된 리소스에 중복 요청하거나 validation 을 위반하면 400 으로 거부. 코드 관점에서 의도된 방어 동작이다.

Log Evidence#

Datadog query (모두 status:info, [400] 응답):

text
service:cupixworks-api "Cannot transition cycle_state"

retention 창(now-14d)의 실제 발생은 대표 메시지와 다르며 두 부류로 나뉜다.

부류 1 — 이미 종료 상태(멱등성 위반, 중복 클릭):

json
{
  "timestamp": "2026-08-03 16:50:06",
  "status": "info",
  "message": "[400] PUT /api/v1/floorplans/22346/archive (Api::V1::FloorplansController#archive)",
  "error": {
    "message": "Cannot transition cycle_state via :archiving from :archived (Reason(s): Cycle state cannot transition via \"archiving\")",
    "class": "StateMachines::InvalidTransition"
  }
}
json
{
  "timestamp": "2026-07-31 10:36:44",
  "status": "info",
  "message": "[400] PUT /api/v1/facilities/10utd8/archive (Api::V1::FacilitiesController#archive)",
  "error": {
    "message": "Cannot transition cycle_state via :archiving from :archived (Reason(s): Cycle state cannot transition via \"archiving\")",
    "class": "StateMachines::InvalidTransition"
  }
}

부류 2 — 모델 validation halt (레벨 이름 중복 / elevation 겹침):

json
{
  "timestamp": "2026-07-31 16:46:51",
  "status": "info",
  "message": "[400] PUT /api/v1/levels/23297/trash (Api::V1::LevelsController#trash)",
  "error": {
    "message": "Cannot transition cycle_state via :trashing from :created (Reason(s): Name Level '06 - Sixth Floor' already exists in this Building.)",
    "class": "StateMachines::InvalidTransition"
  }
}
json
{
  "timestamp": "2026-07-30 19:26:30",
  "status": "info",
  "message": "[400] DELETE /api/v1/levels/65012/purge (Api::V1::LevelsController#purge)",
  "error": {
    "message": "Cannot transition cycle_state via :purging from :trashed (Reason(s): Elevation Level 'L2' overlaps with Level 'LEVEL1' (z=3.0).)",
    "class": "StateMachines::InvalidTransition"
  }
}

endpoint 분포 (now-14d 샘플 50건):

text
30  DELETE /api/v1/levels   (purge)
12  PUT    /api/v1/levels   (trash/untrash)
 7  PUT    /api/v1/facilities (archive)
 1  PUT    /api/v1/floorplans (archive)
  • 특정 리소스(level 65012, facility po00en 등)에 대해 짧은 시간 내 반복 요청 패턴이 관찰됨 → 사용자 중복 클릭/재시도. 특정 서버 상태 재진입이 아니라 다수 distinct 리소스에 산발적으로 발생.
  • 대표 메시지 :deleting from :deleted 는 retention 창에서 0건 — STALE. Error Tracking 이 모든 Cannot transition cycle_state via X 변형을 한 이슈로 묶고 first_seen 샘플을 고정했기 때문.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 대표 메시지 :deleting from :deleted 가 현재 발생하는 실제 root cause first_seen 대표 샘플 retention 창(now-14d) "Cannot transition cycle_state" 검색 결과에 :deleting from :deleted 0건; 실제는 :archiving from :archived / :trashing from :created / :purging from :trashed Rejected (STALE)
H2 이미 종료 상태 리소스에 대한 중복 액션(멱등성 위반) — PUT .../archive on archived 다수 :archiving from :archived 로그, Cyclable state_machine archivingcreated 에서만 시작 가능 (cyclable.rb:92-94); facility po00en 반복 요청 Confirmed (부류 1)
H3 before_transition/validation halt (Level 이름 중복, elevation 겹침) 로 인한 전이 거부 Reason(s): ... already exists in this Building / Elevation ... overlaps ... (z=...) 메시지; floorplan_level.rb:5 uniqueness validation 존재 Confirmed (부류 2)
H4 서버 코드 결함(잘못된 상태 매핑 → 500 오분류) client_error_controller.rb:19,37-39 에서 StateMachines::InvalidTransition → 400 STAT10001 로 명시 매핑; 모든 로그 status:info [400], 500 아님 Rejected
H5 외부 의존성/인프라 장애 status-board scope svc:cupixworks-api::unknown, active 없음; 메시지가 애플리케이션 로직 예외 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 없음. StateMachines::InvalidTransition 은 이미 client_error_controller.rb:37-39 에서 HTTP 400 (STAT10001 Cupix::Errors::InvalidState) 으로 올바르게 분류되어 클라이언트에게 반환된다. 서버 코드 결함이 아니므로 코드 수정 불필요.

단기 개선 (1주 이내)#

  • Error Tracking noise 감소를 위해 이 이슈(77270758-e391-11ef-8f04-da7ad0900002)를 IGNORE 처리 권장. 400 client error 는 서버 알람 대상이 아니다.
  • 프런트엔드 협의 대상(코드 자동 수정 제외): 부류 1(멱등성 위반)은 이미 archived/deleted/trashed 상태인 리소스에 액션 버튼이 노출/재클릭 가능하다는 UX 문제일 수 있음. 완료 상태에서 버튼 비활성화 또는 액션 후 목록 갱신으로 재요청을 막으면 발생 자체가 감소한다.

장기 개선 (재발 방지)#

  • 필요 시 Cyclable state machine 에 멱등 이벤트(이미 목적 상태면 no-op 반환) 옵션을 도입해 중복 요청이 예외 대신 200/204 로 처리되도록 고려. 단, 이는 API 계약 변경이므로 프런트엔드/클라이언트와 조율 필요. 현재로서는 400 반환이 계약상 정상 동작이므로 필수 아님.

Monitoring#

StateMachines::InvalidTransition (STAT10001) 400 발생 추이 — 급증 시에만 UX/클라이언트 이슈로 확인:

text
service:cupixworks-api "Cannot transition cycle_state"

archive 멱등성 위반(부류 1) 추이:

text
service:cupixworks-api "Cannot transition cycle_state via :archiving from :archived"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 불필요 — Error Tracking IGNORE + 선택적 프런트엔드 UX 개선)

Noise Verdict#

noise — StateMachines::InvalidTransition 은 이미 완료된 상태 리소스에 대한 중복 요청 또는 모델 validation 위반을 서버가 정상적으로 HTTP 400 으로 거부한 것으로, 코드 결함이 아닌 클라이언트 입력 오류이다.