state_machines-activerecord
state_machines-activerecord copied to clipboard
Off-by-one after_transition bug, when using two state machines at the same time.
Let me preface this by saying that this is probably pretty non-standard, but I discovered a bug that is pretty reproducible and it occurs when we're trying to operate two state machines at the same time.
Here is an example app created with this command
rails new state_machine_bug --database=sqlite3 --skip-keeps --skip-action-text --skip-active-storage --skip-action-cable --skip-spring --skip-turbolinks --skip-test --skip-action-mailer --skip-action-text --skip-sprockets --skip-javascript
Gemfile looks like this
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.7.1'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem "state_machines-activerecord", "~> 0.6.0"
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'listen', '~> 3.2'
end
class Vehicle < ActiveRecord::Base
state_machine initial: :parked do
state :parked, :driving
event :start do
transition any => :driving
end
event :park do
transition any => :parked
end
before_transition any => :parked do |vehicle, transition|
p :before_transition
p transition
p transition.object_id
p "-"*50
vehicle.location_state_event = "garage"
end
after_transition any => any do |vehicle, transition|
p :after_transition
p transition
p transition.object_id
p "-"*50
end
end
state_machine :location_state, initial: :garaged do
state :garaged, :outside
event :garage do
transition any => :garaged
end
event :drive do
transition any => :outside
end
before_transition any => any do |vehicle, transition|
p :before_transition_location
p transition
p transition.object_id
p "-"*50
end
after_transition any => any do |vehicle, transition|
p :after_transition_location
p transition
p transition.object_id
p "-"*50
end
end
end
Now run the following commands in the rails console in this order
v = Vehicle.create!
v.drive!
v.start!
v.park
here's what I get. Note the following before you jump in
- the transition objects (before and after) for a given state machine are usually the same, you can tell by their matching object_ids
- the first time we park the vehicle, you'd expect to see an after_transition object output for the location_state machine. there isn't one
- when we park the vehicle a second time, we then see an after_transition object output, but the transition object in the before_transition and the after_transition are not the same, and if you look at the data in them, the transition object in the after_transition, is containing the data for the PREVIOUS transition
- if you continue to park the vehicle the after_transition object, will consistently have the transition object for the PREVIOUS transition.
v = Vehicle.create!
=> #<Vehicle id: 1, state: "driving", location_state: "garaged", created_at: "2020-11-19 07:51:02", updated_at: "2020-11-19 08:01:08">
2.7.1 :034 > v.start!
:after_transition
#<StateMachines::Transition attribute=:state event=:start from="driving" from_name=:driving to="driving" to_name=:driving>
10260
"--------------------------------------------------"
=> true
2.7.1 :035 > v.drive!
:before_transition_location
#<StateMachines::Transition attribute=:location_state event=:drive from="garaged" from_name=:garaged to="outside" to_name=:outside>
10280
"--------------------------------------------------"
(0.1ms) begin transaction
Vehicle Update (0.4ms) UPDATE "vehicles" SET "location_state" = ?, "updated_at" = ? WHERE "vehicles"."id" = ? [["location_state", "outside"], ["updated_at", "2020-11-19 08:16:32.458179"], ["id", 1]]
:after_transition_location
#<StateMachines::Transition attribute=:location_state event=:drive from="garaged" from_name=:garaged to="outside" to_name=:outside>
10280
"--------------------------------------------------"
(1.0ms) commit transaction
=> true
2.7.1 :036 > v
=> #<Vehicle id: 1, state: "driving", location_state: "outside", created_at: "2020-11-19 07:51:02", updated_at: "2020-11-19 08:16:32">
2.7.1 :037 > v.park!
:before_transition
#<StateMachines::Transition attribute=:state event=:park from="driving" from_name=:driving to="parked" to_name=:parked>
10300
"--------------------------------------------------"
:before_transition_location
#<StateMachines::Transition attribute=:location_state event=:garage from="outside" from_name=:outside to="garaged" to_name=:garaged>
10320
"--------------------------------------------------"
(0.1ms) begin transaction
Vehicle Update (0.4ms) UPDATE "vehicles" SET "state" = ?, "location_state" = ?, "updated_at" = ? WHERE "vehicles"."id" = ? [["state", "parked"], ["location_state", "garaged"], ["updated_at", "2020-11-19 08:16:40.926659"], ["id", 1]]
:after_transition
#<StateMachines::Transition attribute=:state event=:park from="driving" from_name=:driving to="parked" to_name=:parked>
10300
"--------------------------------------------------"
(0.8ms) commit transaction
=> true
2.7.1 :038 > v.park!
:before_transition
#<StateMachines::Transition attribute=:state event=:park from="parked" from_name=:parked to="parked" to_name=:parked>
10340
"--------------------------------------------------"
:before_transition_location
#<StateMachines::Transition attribute=:location_state event=:garage from="garaged" from_name=:garaged to="garaged" to_name=:garaged>
10360
"--------------------------------------------------"
:after_transition_location
#<StateMachines::Transition attribute=:location_state event=:garage from="outside" from_name=:outside to="garaged" to_name=:garaged>
10320
"--------------------------------------------------"
:after_transition
#<StateMachines::Transition attribute=:state event=:park from="parked" from_name=:parked to="parked" to_name=:parked>
10340
"--------------------------------------------------"
=> true
I know this is non-standard, so if you could just give me some pointers for where to look to dig a little deeper on this I'd really be grateful. I started to look at the code here, but I wasn't immediately able to identify the StateMachine::Transition code to delve a bit deeper into this.
Thanks!