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#
- 2025-02-05 16:18 KST — 최초 발생(first_seen). 대표 메시지
:deleting from :deleted로 고정됨. - 2026-07-30 04:26 KST 전후 —
DELETE /api/v1/levels/.../purge에서:purging from :trashed(elevation overlap) 다수 발생. - 2026-07-31 16:46 KST — 마지막 발생(last_seen).
PUT /api/v1/levels/23297/trash:trashing from :created(name 중복). - 2026-08-03 16:50 KST — retention 창 내 최신,
PUT /api/v1/floorplans/22346/archive:archiving from :archived.
Error Log#
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-39 의 rescue_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 메서드 호출
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상태만 허용한다.archiving은created에서만,deleting/purging은created에서만 시작할 수 있고, 이미 종료 상태(archived,deleted,trashed)에는 재전이 경로가 없다.
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 으로 호출한다.
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 로 변환된다.
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] 응답):
service:cupixworks-api "Cannot transition cycle_state"
retention 창(now-14d)의 실제 발생은 대표 메시지와 다르며 두 부류로 나뉜다.
부류 1 — 이미 종료 상태(멱등성 위반, 중복 클릭):
{
"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"
}
}
{
"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 겹침):
{
"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"
}
}
{
"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건):
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 archiving 은 created 에서만 시작 가능 (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 (STAT10001Cupix::Errors::InvalidState) 으로 올바르게 분류되어 클라이언트에게 반환된다. 서버 코드 결함이 아니므로 코드 수정 불필요.
단기 개선 (1주 이내)#
- Error Tracking noise 감소를 위해 이 이슈(
77270758-e391-11ef-8f04-da7ad0900002)를 IGNORE 처리 권장. 400 client error 는 서버 알람 대상이 아니다. - 프런트엔드 협의 대상(코드 자동 수정 제외): 부류 1(멱등성 위반)은 이미
archived/deleted/trashed상태인 리소스에 액션 버튼이 노출/재클릭 가능하다는 UX 문제일 수 있음. 완료 상태에서 버튼 비활성화 또는 액션 후 목록 갱신으로 재요청을 막으면 발생 자체가 감소한다.
장기 개선 (재발 방지)#
- 필요 시
Cyclablestate machine 에 멱등 이벤트(이미 목적 상태면 no-op 반환) 옵션을 도입해 중복 요청이 예외 대신 200/204 로 처리되도록 고려. 단, 이는 API 계약 변경이므로 프런트엔드/클라이언트와 조율 필요. 현재로서는 400 반환이 계약상 정상 동작이므로 필수 아님.
Monitoring#
StateMachines::InvalidTransition (STAT10001) 400 발생 추이 — 급증 시에만 UX/클라이언트 이슈로 확인:
service:cupixworks-api "Cannot transition cycle_state"
archive 멱등성 위반(부류 1) 추이:
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 으로 거부한 것으로, 코드 결함이 아닌 클라이언트 입력 오류이다.