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
July 06, 2010 at 7:32 AM
Enjoying these videos, Xavier, thanks! Jared (not your brother) is back here with me at Amani now, and we're doing a bit of gnarly refactoring on the website code.
Just a quick comment, if I understand properly, I think you'll want to s/Tractor/self/ on line 3 of the code example.
Cheers! -- M
July 06, 2010 at 9:42 PM
Correct, cheers!
July 08, 2010 at 7:16 AM
This is very timely. I just started using AASM and have been worried about this very issue. Thanks, Xavier, for the demo and elegant solution!
--Matt
July 09, 2010 at 8:37 AM
What about optimistic locking? Have you tried to your example with this approach in active record?
July 10, 2010 at 11:47 AM
Nice!
I'd probably @alias_method_chain@ the @aasm_event@ method itself as well, so that as soon as you define the event it creates the @alias_method_chain@ around the event method automatically without having to call @ obtain_lock_before_transitions@ in each model.
BTW, you should totally update lesstile to also support inline-code blocks.
July 11, 2010 at 4:20 AM
Phil, optimistic locking would probably work fine too (though I haven't confirmed AASM respects the rails lock_version, I assume it would). I feel pessimistic is a cleaner solution in this instance, since I don't need to change my existing models. (Patching AASM to by default use optimistic locking without lock_version would be neat though, hmmm)
Bo, great idea for aasm_event.
October 04, 2010 at 4:02 PM
One problem with the pessimistic approach using lock! is that lock! will also reload the model. That means you lose all unsaved changes that would otherwise be persisted on the state change since aasm just sets the state and saves the record (it doesn't only update the state attribute).
Maybe it's not a good idea to rely on aasm to save the model on state changes but it definitely makes migrating legacy code that does so a little painful.
October 10, 2010 at 4:21 AM
If you're state changing a dirty model, I'd argue that's probably Doing It Wrong.