Solving Ruby on Rails’ Optimistic Locking Problem with conflict_warnings(Part 2: The Solution)
Jan 6This is the second part of a 2 part post about Optimistic Locking in Ruby on Rails. Part 1, which describes the problem in detail can be found here.
Update: In the process of writing this blog post, I realized that the conflict_warnings plugin is not exactly easy to use with optimistic locking. I’ve started to rework conflict_warnings into a new plugin I call ‘better_optimistic_locking’ that will be better suited for handing stale optimistic locks. But it’s one of those things I work on when I have time to do so. (read: the odd evening or weekend)
To recap: Optimistic locking in Ruby on Rails prevents multiple changes to the same record from clobbering each other. It fails in practice because a lock cannot persist across HTTP requests. When I encountered this problem, I realized that nothing short of adding a state preserving parameter to the request and custom before filters could prevent the race conditions from causing inconsistencies in the database.
About the third time I needed this functionality I decided it was about time to abstract the code into a plugin. And thus conflict_warnings was born. Named after the HTTP 409 Conflict status, which RFC 2616 Section 10 describes as:
The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request.
conflict_warnings provides a coherent set of filters, controller instance methods, and view helpers. When used together, they will block hazardous HTTP requests in much the same way that Active Record’s optimistic locking blocks saves. The helpers provide a state preserving parameter to links and forms. While the filters use the parameters to identify and block conflicting requests.
Installation
To install conflict_warnings, point script/plugin at the github repostiory
$ script/plugin install git://github.com/EmFi/conflict_warnings.git
Usage
Assuming you are following standard Rails naming schemes, basic usage is pretty straight forward. Just add one of the filter methods to your controllers.
class ExamplesController < ApplicationController filter_conflicts, :only => :update ... end
Now use the appropriate helper to redefine your forms and links that could produce hazardous actions.
<% form_for_with_timestamp @example do |f|%> ... <% end %>
That’s it! If the updated_at/updated_on attribute of the record in question is after than the timestamp parameter the filter kicks in. The default action is to render a special template if it exists, otherwise redirect_to :back. However, all the controller methods of conflict_warnings accept a block that will be executed instead of the default action.
conflict_warnings also provides controller instance methods so that you can create custom filters based around these methods. All that advanced usage is explained in the help files distributed in the doc directory of the plugin.
Here are a few more advanced examples.
Custom block
class ExamplesController < ApplicationController filter_conflicts :only => :confirm do respond_to do |format| format.html {render :action => "show"} format.js { render :update do |page| page.replace_html :notificaiton_area, :text => "Your request could not be processed because the example has been modified recently. Please try again" page.replace_html :status, :text => @example.status page.visual_effect :highlight, :status flash.discard end } end end end
If the a user loads the the show page for an example, and that same
example is modified by another user before that first user confirms,
that first users’ attempt to confirm is blocked.
Limited Resources
A common subset of problems that could benefit from conflict_warnings are those that model a system with resources that have limited availability, such as collecting reservations for an event. Validations do the job, but require some extra work to turn into reasonable responses to a shortage of the resource in question. conflict_warnings handles it with a single line of code.
app/controllers/attendees_controller.rb
class AttendeesController < ApplicationController filter_resource_unavailble :only => :create, :model => "Event" end
app/views/attendees/create_resource_unavaible.html.erb
<h1>Sold Out!</h1> We regret to inform you that <%=@event.name%> on <%=@event.date%> has sold out before your transaction could be completed.
LockingResource/custom filter example:
conflict_warnings can be used to enforce mutex locks on resources at the controller level.
class LockingResourcesController < ApplicationController before_filter :login_required, :acquire_lock protected def acquire_lock catch_resources_unavailable current_user, :accessor => :acquire_lock_for_user, :message => "Could not acquire lock" end end
If user cannot acquire a lock they are redirected back to the
referring page with the message
“Could not acquire lock” contained in flash[:warnings]
With the right options it can have some very creative uses, here are just a few I’ve used in the past.
- Only update portions of a record that have changed and highlight
them with Prototype or jQuery (requires some kind of model version
tracking, maybe acts_as_audited) - Render custom forms displaying side by side comparisons of conflicting information.
- Simplify actions upon failing to acquire a lock.
- Enabling/Disabling some actions by when they occur.
Incomplete Features
- Custom Form Builder: Displaying multiple versions of conflicting records for comparison in a form will be a common task, easily streamlined by a custom Form Builder
- link/form helper for models already using optimistic locks: I see the default timestamp solution used by conflict_warnings as a good enough solution, but it’s still not flawless. The controller side of the plugin provides a filters that uses optimistic locking, but they still require more complicated helpers. To supply the I haven’t decided on the best syntax for them yet.
Caveats
Passing the extra parameters is not the perfect solution either, because the parameter is not tamper resistant.
Like most other plugins, contributions and constructive feedback are always welcome.
Comments