I've got a rather easy Rails application that enables customers to join up their attendance on some courses. The ActiveRecord models are the following:

class Course < ActiveRecord::Base
  has_many :scheduled_runs
  ...
end

class ScheduledRun < ActiveRecord::Base
  belongs_to :course
  has_many :attendances
  has_many :attendees, :through => :attendances
  ...
end

class Attendance < ActiveRecord::Base
  belongs_to :user
  belongs_to :scheduled_run, :counter_cache => true
  ...
end

class User < ActiveRecord::Base
  has_many :attendances
  has_many :registered_courses, :through => :attendances, :source => :scheduled_run
end

A ScheduledRun instance includes a finite quantity of places available, and when the limit is arrived at, forget about attendances could be recognized.

def full?
  attendances_count == capacity
end

attendances_count is really a counter cache column holding the amount of attendance associations produced for the ScheduledRun record.

My issue is which i don't fully know the right way to make sure that a race condition does not occur when 1 or even more people make an effort to register going back available put on a training course simultaneously.

My Attendance controller appears like this:

class AttendancesController < ApplicationController
  before_filter :load_scheduled_run
  before_filter :load_user, :only => :create

  def new
    @user = User.new
  end

  def create
    unless @user.valid?
      render :action => 'new'
    end

    @attendance = @user.attendances.build(:scheduled_run_id => params[:scheduled_run_id])

    if @attendance.save
      flash[:notice] = "Successfully created attendance."
      redirect_to root_url
    else
      render :action => '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

As you can tell, it does not consider in which the ScheduledRun instance has arrived at capacity.

Any help on this is greatly appreciated.

Update

I am not sure if this sounds like the proper way to perform positive securing within this situation, but some tips about what Used to do:

I added two posts towards the ScheduledRuns table -

t.integer :attendances_count, :default => 0
t.integer :lock_version, :default => 0

I additionally added a means to ScheduledRun model:

  def attend(user)
    attendance = self.attendances.build(:user_id => user.id)
    attendance.save
  rescue ActiveRecord::StaleObjectError
    self.reload!
    retry unless full? 
  end

Once the Attendance model is saved, ActiveRecord goes ahead and updates the counter cache column around the ScheduledRun model. Here's the log output showing where this occurs -

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)

If your subsequent update happens towards the ScheduledRun model prior to the new Attendance model is saved, this will trigger the StaleObjectError exception. After which, the entire factor is retried again, if capacity has not recently been arrived at.

Update #2

Following on from @kenn's response this is actually the up-to-date attend method around the SheduledRun object:

# creates a new attendee on a course
def attend(user)
  ScheduledRun.transaction do
    begin
      attendance = self.attendances.build(:user_id => 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