Note that there are some explanatory texts on larger screens.

plurals
  1. POHow do I avoid a race condition in my Rails app?
    text
    copied!<p>I have a really simple Rails application that allows users to register their attendance on a set of courses. The ActiveRecord models are as follows:</p> <pre><code>class Course &lt; ActiveRecord::Base has_many :scheduled_runs ... end class ScheduledRun &lt; ActiveRecord::Base belongs_to :course has_many :attendances has_many :attendees, :through =&gt; :attendances ... end class Attendance &lt; ActiveRecord::Base belongs_to :user belongs_to :scheduled_run, :counter_cache =&gt; true ... end class User &lt; ActiveRecord::Base has_many :attendances has_many :registered_courses, :through =&gt; :attendances, :source =&gt; :scheduled_run end </code></pre> <p>A ScheduledRun instance has a finite number of places available, and once the limit is reached, no more attendances can be accepted.</p> <pre><code>def full? attendances_count == capacity end </code></pre> <p>attendances_count is a counter cache column holding the number of attendance associations created for a particular ScheduledRun record.</p> <p>My problem is that I don't fully know the correct way to ensure that a race condition doesn't occur when 1 or more people attempt to register for the last available place on a course at the same time.</p> <p>My Attendance controller looks like this:</p> <pre><code>class AttendancesController &lt; ApplicationController before_filter :load_scheduled_run before_filter :load_user, :only =&gt; :create def new @user = User.new end def create unless @user.valid? render :action =&gt; 'new' end @attendance = @user.attendances.build(:scheduled_run_id =&gt; params[:scheduled_run_id]) if @attendance.save flash[:notice] = "Successfully created attendance." redirect_to root_url else render :action =&gt; 'new' end end protected def load_scheduled_run @run = ScheduledRun.find(params[:scheduled_run_id]) end def load_user @user = User.create_new_or_load_existing(params[:user]) end end </code></pre> <p>As you can see, it doesn't take into account where the ScheduledRun instance has already reached capacity. </p> <p>Any help on this would be greatly appreciated.</p> <p><strong><em>Update</em></strong></p> <p>I'm not certain if this is the right way to perform optimistic locking in this case, but here's what I did:</p> <p>I added two columns to the ScheduledRuns table - </p> <pre><code>t.integer :attendances_count, :default =&gt; 0 t.integer :lock_version, :default =&gt; 0 </code></pre> <p>I also added a method to ScheduledRun model:</p> <pre><code> def attend(user) attendance = self.attendances.build(:user_id =&gt; user.id) attendance.save rescue ActiveRecord::StaleObjectError self.reload! retry unless full? end </code></pre> <p>When the Attendance model is saved, ActiveRecord goes ahead and updates the counter cache column on the ScheduledRun model. Here's the log output showing where this happens -</p> <pre><code>ScheduledRun Load (0.2ms) SELECT * FROM `scheduled_runs` WHERE (`scheduled_runs`.`id` = 113338481) ORDER BY date DESC Attendance Create (0.2ms) INSERT INTO `attendances` (`created_at`, `scheduled_run_id`, `updated_at`, `user_id`) VALUES('2010-06-15 10:16:43', 113338481, '2010-06-15 10:16:43', 350162832) ScheduledRun Update (0.2ms) UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481) </code></pre> <p>If a subsequent update occurs to the ScheduledRun model before the new Attendance model is saved, this should trigger the StaleObjectError exception. At which point, the whole thing is retried again, if capacity hasn't already been reached.</p> <p><strong><em>Update #2</em></strong></p> <p>Following on from @kenn's response here is the updated attend method on the SheduledRun object:</p> <pre><code># creates a new attendee on a course def attend(user) ScheduledRun.transaction do begin attendance = self.attendances.build(:user_id =&gt; user.id) self.touch # force parent object to update its lock version attendance.save # as child object creation in hm association skips locking mechanism rescue ActiveRecord::StaleObjectError self.reload! retry unless full? end end end </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload