Como evito uma condição de corrida no meu aplicativo Rails?

Eu tenho um aplicativo Rails realmente simples que permite aos usuários registrar sua presença em um conjunto de cursos. Os modelos do ActiveRecord são os seguintes:

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

Uma instância ScheduledRun tem um número finito de vagas disponíveis e, uma vez atingido o limite, não há mais presença de presenças.

def full?
  attendances_count == capacity
end

frequentances_count é uma coluna de contador de cache que contém o número de associações de atendimento criadas para um registro ScheduledRun específico.

Meu problema é que não conheço completamente a maneira correta de garantir que uma condição de corrida não ocorra quando 1 ou mais pessoas tentam se registrar no último lugar disponível em um percurso ao mesmo tempo.

Meu controlador de presença fica assim:

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

Como você pode ver, não leva em consideração onde a instância ScheduledRun já atingiu a capacidade.

Qualquer ajuda neste assunto seria altamente apreciada.

Atualizar

Não tenho certeza se este é o caminho certo para executar o bloqueio otimista neste caso, mas aqui está o que eu fiz:

Adicionei duas colunas à tabela ScheduledRuns -

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

Também adicionei um método ao modelo ScheduledRun:

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

Quando o modelo de atendimento é salvo, o ActiveRecord segue em frente e atualiza a coluna de cache do contador no modelo ScheduledRun. Aqui está a saída do log mostrando onde isso acontece -

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)

Se uma atualização subsequente ocorrer no modelo ScheduledRun antes que o novo modelo de Atendimento seja salvo, isso deve acionar a exceção StaleObjectError. Nesse ponto, a coisa toda é repetida novamente, se a capacidade ainda não tiver sido atingida.

Atualização # 2

A seguir à resposta de @ kenn, aqui está o método de participação atualizado no objeto SheduledRun:

# 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

questionAnswers(2)

yourAnswerToTheQuestion