Blog

Concurrency and the AASM Gem

By | July 19th, 2010 at 9:07AM

Hello all,

The Engine Yard blog is back in action after taking a break following JRuby 1.5, Rubinius 1.0, the introduction of xCloud, RailsConf and (very soon) Rails 3.

Our latest post is from a special guest and Engine Yard partner Xavier Shay. He’ll be running a pair of training sessions on ‘using your database to make your Ruby on Rails applications rock solid’ at Engine Yard’s San Francisco office on the 24th and 31st of July. Visit www.dbisyourfriend.com for course and registration details.

Code Concurrency

Your Ruby on Rails code is run concurrently, whether you like it or not.

Concurrency is a staple term when talking about hosting infrastructure, but it is too often brushed aside when discussing actual code bases. This attitude is especially prevalent in the Ruby on Rails community: I can’t name one popular plugin that gets it right. In this post I will address problems with the typical state machine pattern used by Rails applications, and show you how to address them and make your code bullet-proof.

The Problem

Consider the following controller action, backing a big green “ship button” next to a purchase order:

def ship
  @order = PurchaseOrder.find(params[:id])
  @order.ship!
  redirect_to order_path(@order)
end

Imagine two users both press the “ship” button at the same time. (Or as often happen, one user double clicks the button.) The two requests will hit the load balancer and be distributed out to run on different processes. What happens when the above code—typical of many rails applications—is run in two different places at the same time?

Both processes will load the order from the database at line 2. At line 3 when the ship! method is run, both processes will check the attributes of the order and see that it is currently unshipped. As a result, both execute shipping code, which may include sending emails, updating caches, and transferring funds. As a result, the customer will receive duplicate emails, or worse, be charged twice. All versions of acts_as_state_machine (AASM) exhibit this behavior.

The Fix

Any time you read data from the database with the intention of making changes based on that data (“ship the order if it isn’t already shipped”) you *must* obtain an exclusive database lock on the row. The database will block any processes trying to access that row until the session that obtained the lock concludes its transaction (COMMIT or ROLLBACK). ActiveRecord allows us to do this using the :lock flag:

def ship
  PurchaseOrder.transaction do
    @order = PurchaseOrder.find(params[:id], :lock => true)
    @order.ship!
  end
  redirect_to order_path(@order)
end

Working through the above example again, the first process to execute the find will issue the following SQL:

SELECT * FROM purchase_orders WHERE id = 1 FOR UPDATE

Notice the “FOR UPDATE” on the end; this instructs the database to place an exclusive lock on the row. When the second process executes the find and submits the above SQL to the database, the database will wait for the first transaction to complete (after calling ship! and updating the state of the order) before reading and returning the row. The returned row will now have a state of “shipped”, and as such the ship! method will effectively be a noop (no operation). The customer will only receive one email.

It is also possible using ActiveRecord to lock an object that has been already loaded from the database:

def ship
  @order = PurchaseOrder.find(params[:id])
  PurchaseOrder.transaction do
    @order.lock!
    @order.ship!
  end
  redirect_to order_path(@order)
end

This is equivalent to a reload, but adds the “FOR UPDATE” suffix necessary for a database lock. It is an extra SQL statement (the order is selected twice), but is an easier pattern to abstract away.

class Order < ActiveRecord::Base
  # This method is usually provided by AASM
  def ship!
    return if shipped?
  # Important emails and computations
  end
  def ship_with_lock!
    transaction do
      lock!
      ship_without_lock!
    end
  end
  alias_method_chain :ship!, :lock
end

With alias_method_chain, we can continue to use exactly the same controller code we started with (just a plain call to ship!), and locking is handled for us in the background.

Lost updates or duplicate execution won’t be a problem for every website, but if you are starting to worry about the concurrency of your hosting infrastructure, it’s worth having a look over your code too.

If you’d like to join me for some hands-on work with this, I’ll be running two classes at Engine Yard’s San Francisco office on the 24th and 31st of July. Visit www.dbisyourfriend.com for course and registration details.

  • http://blog.costan.us Victor Costan

    Thank you for bringing this issue to light! I'm definitely guilty as charged on most of my apps.

    For completeness' sake, it'd be nice to know when the lock is released in the first example. Is it when I save! the model? When the controller finishes executing? Or when the model is finalized by the Ruby GC?

    In the second example, I'm assuming the lock gets dropped at the end of the transaction. If that's not the case, please do correct my misunderstanding.

    Once again, thanks for the post!

  • Rodrigo Dellacqua

    What about code design to avoid concurrency on environments with no transaction?

  • http://metaduck.com Pedro

    The lock should be released when the transaction finishes (when the "transaction do" block ends).

  • mike in africa

    awesome… concurrency/transaction/locking discussion is long overdue in this community…

  • Xavier Shay

    The lock is released when the transaction completes (or is aborted, if an exception is thrown). If you explicitly declare the transaction (PurchaseOrder.transaction) it's at the 'end' of that block, otherwise each statement is executed in it's own transaction (simplification, but roughly correct)

  • Xavier Shay

    As long as the environment as atomicity, you can do a similar thing with optimistic locking (a form of locking you do in your application code, rather than the database). If you were using MongoDB for instance, you could update state to 'ship' where state is 'unshipped' in the one statement.

  • http://intensedebate.com/profiles/marshally marshally

    Xavier,

    Thanks for a great post on locking. I agree with other commenters that the disregard for locking in the Rails community is troublesome at times.

    I've experienced the locking mechanism you describe here to yield poor performance in high throughput MySql instances. InnoDB table type supports row level locking, which can be a benefit (but not a cure) for this performance problem.

  • http://developer-in-test.blogspot.com Sai Venkat

    Transaction design is a complicated subject. Lock based blocking transactions are easy to design for simple cases when you don't need to work with too many entities or high throughput applications. When working with larger throughputs or entities, these can lead to at the minimum reduced throughput or in worst case dead locks. Also you need to decide lock granularity and all this depend on the type of data and the kind of throughput you are looking for (The same problems you would encounter in normal concurrency).

    I have experienced that spreading transaction across code would lead to problems more often than not. But it is good that rails community is taking a note of this.

  • http://www.schuerig.de/michael Michael Schuerig

    Xavier, I'm not convinced you are right about locking. Keep in mind that ActiveRecord already does optimistic locking automatically if the lock_version column is present. That may not be enough if you have multiple unrelated objects whose consistency you need to ensure, but it works well for a single object. (In the case of multiple related objects, view them as an aggregate and use the aggregate's root for locking.)

    What you get with optimistic locking is that the first update, in your example the #save triggered by #ship!, succeeds and increments the lock_version. For further calls to @order#ship! there are two cases:

    @order may have been loaded before the first call to #ship!. In this case, @order.lock_version contains a by now superseded version number, the object is "stale" and an attempt to save it will result in a StaleObjectError exception.

    If @order has been loaded after the first call to #ship!, it refers to a fresh object, i.e., one with the current version number. This object can be updated and saved. But it ought not to be possible to call #ship! on it! It is the job of the state machine to recognize that the object is already shipped and that a second #ship! is an error.

    Now what about non-transactional actions such as sending mail? Well, you better put them in a place where it is certain that the state transition has already been successfully executed. It's the job of the state machine implementation to ensure this. I don't know AASM well enough to say whether it indeed works this way.

  • http://rowtheboat.com George Palmer

    Bit late to the party but surely Rails optimistic locking solves this. I use this on all my projects but of all the freelance projects I've inherited I've not seen it on one other. In a very unscientific manner, suggests it's not very widely known, widely used feature of rails

  • http://www.webvanta.com/ Christopher Haupt

    Xavier,

    I played with one of my app's that use AASM and ran in to one issue to watch out for using the override technique at the end of the article. Unless I missed something reading through the code, if you override the AASM event methods (e.g. the dynamically provided "ship!" method in this example), you will run in to the situation where the rest of the AASM internal machinery won't be run, and you won't see a state change…a simple change works around this issue using your example:

    def ship_with_lock!
    transaction do
    lock!
    return if shipped?
    # Important emails and computations
    ship_without_lock!
    end
    end
    alias_method_chain :ship!, :lock

    Simply use the existing ship! method, but chain your transaction wrapper and other code before calling the AASM supplied method. With more recent versions of AASM, there are a number of callbacks available, so you could factor out your "Important emails and computations" and put them in their own method to tidy things up a bit.

    The SF class was great. Thanks again as it was a great opportunity to dig in to some of the darker corners of MySQL in the context of Rails.