acts_as_state_machine is not concurrent
Here is a short 4 minute screencast in which I show you how the acts as state machine (AASM) gem fails in a concurrent environment, and also how to fix it.
(If embedding doesn’t work or the text is too small to read, you can grab a high resolution version direct from Vimeo)
It’s a pretty safe bet that you want to obtain a lock before all state transitions, so you can use a bit of method aliasing to do just that. This gives you much neater code than the quick fix I show in the screencast, just make sure you understand what it is doing!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class ActiveRecord::Base def self.obtain_lock_before_transitions AASM::StateMachine[self].events.keys.each do |t| define_method("#{t}_with_lock!") do transaction do lock! send("#{t}_without_lock!") end end alias_method_chain "#{t}!", :lock end end end class Tractor # ... aasm_event :buy do transitions :to => :bought, :from => [:for_sale] end obtain_lock_before_transitions end |
This is a small taste of my DB is your friend training course, that helps you build solid rails applications by finding the sweet spot between stored procedures and treating your database as a hash. July through September I am running full day sessions in the US and UK. Chances are I’m coming to your city. Check it out at http://www.dbisyourfriend.com
Acts_as_state_machine locking
consider the following!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Door < ActiveRecord::Base acts_as_state_machine :initial => :closed state :closed state :open, :enter => :say_hello event :open do transitions :from => :closed, :to => :open end def say_hello puts "hello" end end door = Door.create! fork do transaction do door.open! end end door.open! # >> hello # >> hello |
It’s broken, you can only open a door once. This is a classic double-update problem. One way to solve is with pessimistic locking. I made some codes that automatically lock any object when you call an event on it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class ActiveRecord::Base # Forces all state transition events to obtain a DB lock def self.obtain_lock_before_all_state_transitions event_table.keys.each do |transition| define_method("#{transition}_with_lock!") do self.class.transaction do lock! send("#{transition}_without_lock!") end end alias_method_chain "#{transition}!", :lock end end end class Door < ActiveRecord::Base # ... as before obtain_lock_before_all_state_transitions end |
beware! Your state transitions can now throw ActiveRecord::RecordNotFound errors (from lock!), since the object may have been deleted before you got a chance to play with it.
If you’re not using any locking in your web app, you’re probably doing it wrong. Just sayin’.