Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Off-by-one after_transition bug, when using two state machines at the same time. #91

Open
concept47 opened this issue Nov 19, 2020 · 0 comments

Comments

@concept47
Copy link

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!

@concept47 concept47 changed the title off by one transition bug, when using two state machines at the same time. Off-by-one after_transition bug, when using two state machines at the same time. Nov 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant