A Guide to Optimistic Locking
This guest post is from community contributor and Engine Yard partner Brian Landau, of Viget Labs. Brian is a Developer at Viget Labs, where he works on web applications small and large. He works mostly in Ruby and JavaScript but enjoys dabbling in other languages too (Io is his current favorite).
Although optimistic locking is a feature that has been in Rails for a long time, I find that I and those around me rarely take advantage of it. While you can easily get started with optimistic locking just by looking at the Rails API Docs, you’ll quickly find that you need to do more then add a lock_version
column to take full advantage of this feature.
Getting Started
The use case for optimistic locking is preventing users from overwriting changes made by other users. Let’s say Billy comes to your awesome travel web app to make a change to a location, but just before he submits, Jenny comes and submits a change. Optimistic locking will prevent Billy’s changes from going through. Getting started with optimistic locking is really as simple as adding a lock_version
column to every table on which you want locking enabled.
class AddLockingColumns < ActiveRecord::Migration
def self.up
add_column :destinations, :lock_version, :integer
end
def self.down
remove_column :destinations, :lock_version
end
end
After adding this column, every update to a model will result in this lock version being incremented. If for some reason you can’t use the column name lock_version
, no problem: use some other name and just set that in the class like so:
class Destination
self.locking_column = "my_custom_locking"
end
Once you’ve done this, if two people try to submit an update to a model at the same time, one of them will cause an ActiveRecord::StaleObjectError
error to be raised.
Fixing the edit form_for
While this behavior is helpful, it doesn’t solve a more worrisome problem. Let’s say Billy comes to change that same vacation destination. He opens up the edit form, bangs away at the content and walks away to get some coffee. He gets distracted and doesn’t come back to work on it for a few hours. While he’s away, Jenny makes a quick change to the destination. He comes back and finishes the content changes and submits the edit form. What happens? Well, with optimistic locking out of the box, his changes succeed and go through to the database. This is problematic for me, and is not what I would expect. This happens because the lock_version
is set from the database when you instantiate the model object inside the update action. What we need is the model to be locked for Billy to the version he has when he accesses the edit form. The best way to accomplish this is by adding a hidden input for the lock_version
field. Then, when someone submits the form, if the lock version has been incremented since they accessed it, the update fails with an ActiveRecord::StaleObjectError
error. You can do this by hand, by adding this hidden field to every form you need locking on, like so:
<%= form_for @destination do |form| %>
<%= form.hidden_field :lock_version %>
<%# ... other inputs %>
<% end %>
Alternatively, you can make your life easier and just add this code to your application:
module ActionView
module Helpers
module OptimisticLockingFormFor
def self.included(base)
base.alias_method_chain :form_for, :optimistic_locking
end
def form_for_with_optimistic_locking(record_or_name_or_array, *args, &block)
form_for_without_optimistic_locking(record_or_name_or_array, *args) do |form_with_locking|
lock_form = form_with_locking.object &&
form_with_locking.object.respond_to?(:locking_enabled?) &&
form_with_locking.object.locking_enabled? &&
!form_with_locking.object.new_record?
if lock_form
concat(content_tag(:div,
form_with_locking.hidden_field(form_with_locking.object.class.locking_column),
:style => 'margin:0;padding:0;display:inline').html_safe)
end
yield form_with_locking
end
end
end
end
end
ActionView::Base.send :include, ActionView::Helpers::OptimisticLockingFormFor
This code modifies the form_for
helper to automatically add a lock_version
hidden input for every object that has locking enabled and isn’t new (new records aren’t versioned yet, and submitting a lock version causes problems). With this in place you don’t need to remember to add the lock version to every form you need it on. Great! Now Billy can get coffee for as long as he wants and not have to worry about overwriting Jenny’s changes. I have seen some other suggestions out there to tie the form to the lock version when the user accesses the edit form, like putting the model object in session, or the lock version in session. All of these solutions have their flaws, though. Storing a model in session is a bad idea for a number of reason that have been explained well elsewhere. Overall, I believe storing the lock version on the form is the best way to handle this type of locking, as it ensures that the lock version is tied to the form the user has in front of them.
Out with the Stale in with the New
A remaining problem is that, if Billy has made this change after Jenny already made a change, an ActiveRecord::StaleObjectError
error is raised which by default results in a blank page. The simplest solution to this is to add a static HTML error page at public/409.html
. What I prefer to do is catch the error and render the edit page with a flash error message telling the user what happened. We also want to ensure the record: 1) is the latest version, 2) isn’t using the lock version from the original edit, and 3) is using the attribute values the user entered on the form. The reason for all of this is to ensure the form has the correct lock version and has the values the user supplied to make it easy for them to resubmit the form. You can do that right in the controller action like this:
class DestinationsController < ApplicationController
def update
# ... update code
rescue ActiveRecord::StaleObjectError
@destination.reload.attributes = params[:destination].reject do |attrb, value|
attrb.to_sym == :lock_version
end
flash.now[:error] = "Another user has made a change to that record "+
"since you accessed the edit form."
render :edit, :status => :conflict
end
end
As your application grows, you’ll want to DRY up multiple rescue ActiveRecord::StaleObjectError
blocks with something like this:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::StaleObjectError do |exception|
respond_to do |format|
format.html {
correct_stale_record_version
stale_record_recovery_action
}
format.xml { head :conflict }
format.json { head :conflict }
end
end
protected
def stale_record_recovery_action
flash.now[:error] = "Another user has made a change to that record "+
"since you accessed the edit form."
render :edit, :status => :conflict
end
end
class DestinationsController < ApplicationController
protected
def correct_stale_record_version
@destination.reload.attributes = params[:destination].reject do |attrb, value|
attrb.to_sym == :lock_version
end
end
end
You’ll also notice in the rescue_from
block both XML and JSON API requests are handled by just returning a 409 “Conflict” status code with no body.
Final Thoughts
Optimistic locking isn’t right for every situation; some applications won’t need any type of locking, and others may need more strict forms of locking. You may also find that you need to recover from conflicts differently. For instance, you might want to show the current version in the database next to the version the user is trying to submit. However, if you feel it covers most of your applications’ use cases, optimistic locking is so easy to implement there’s no reason you shouldn’t try it.
About Viget Labs In 1999, Viget Labs started building web products for startups. Since 2005, they’ve been building them in Ruby on Rails. Now, Viget Labs’ team of nearly 50 works with both startups and big brands from their offices near Washington, DC, in Durham, NC, and Boulder, CO.
Share your thoughts with @engineyard on Twitter