Как избежать состояния гонки в моем приложении Rails? - PullRequest
20 голосов
/ 14 июня 2010

У меня очень простое приложение на Rails, которое позволяет пользователям регистрировать свою посещаемость на нескольких курсах. Модели ActiveRecord следующие:

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

Экземпляр ScheduledRun имеет ограниченное количество доступных мест, и как только предел достигнут, больше посещений не может быть принято.

def full?
  attendances_count == capacity
end

serveances_count - это столбец кэша счетчика, содержащий количество ассоциаций посещаемости, созданных для конкретной записи ScheduledRun.

Моя проблема в том, что я не полностью знаю правильный способ гарантировать, что условия гонки не возникают, когда 1 или более человек пытаются зарегистрироваться на последнее доступное место на курсе одновременно.

Мой контроллер посещаемости выглядит так:

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

Как вы можете видеть, он не учитывает, где экземпляр ScheduledRun уже достиг емкости.

Любая помощь по этому вопросу будет принята с благодарностью.

Обновление

Я не уверен, что это правильный способ выполнить оптимистическую блокировку в этом случае, но вот что я сделал:

Я добавил два столбца в таблицу ScheduledRuns -

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

Я также добавил метод к модели ScheduledRun:

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

При сохранении модели посещаемости ActiveRecord выполняет обновление столбца кэша счетчика в модели ScheduledRun. Вот вывод журнала, показывающий, где это происходит -

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)

Если последующее обновление модели ScheduledRun происходит до сохранения новой модели посещаемости, это должно вызвать исключение StaleObjectError. В этот момент все снова повторяется, если емкость еще не достигнута.

Обновление № 2

Следуя ответу @ kenn, вот обновленный метод обслуживания объекта 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

Ответы [ 2 ]

13 голосов
/ 23 июля 2010

Оптимистическая блокировка - это путь, но, как вы уже могли заметить, ваш код никогда не вызовет ActiveRecord :: StaleObjectError, поскольку создание дочернего объекта в ассоциации has_many пропускает механизм блокировки.Взгляните на следующий SQL:

UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)

Когда вы обновляете атрибуты в объекте parent , вы обычно видите вместо этого следующий SQL:

UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1

Вышеприведенное утверждение показывает, как реализована оптимистическая блокировка: обратите внимание на предложение lock_version = 1 в WHERE.Когда возникает состояние гонки, параллельные процессы пытаются выполнить этот точный запрос, но только первый выполняется успешно, потому что первый атомарно обновляет lock_version до 2, а последующим процессам не удастся найти запись и вызвать ActiveRecord:: StaleObjectError, поскольку в той же записи больше нет lock_version = 1.

Итак, в вашем случае возможный обходной путь - прикоснуться к родительскому элементу непосредственно перед созданием / уничтожением дочернего объекта, например:

def attend(user)
  self.touch # Assuming you have updated_at column
  attendance = self.attendances.create(:user_id => user.id)
rescue ActiveRecord::StaleObjectError
  #...do something...
end

Это не означает, что нужно строго избегать условий гонки, но практически это должно работать в большинстве случаев.

0 голосов
/ 14 июня 2010

Разве вам не нужно проверять, если @run.full??

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

   # ...
end

Редактировать

Что если вы добавите проверку вроде:

class Attendance < ActiveRecord::Base
   validate :validates_scheduled_run

   def scheduled_run
      errors.add_to_base("Error message") if self.scheduled_run.full?
   end
end

@attendance не сохранится, если соответствующий scheduled_run заполнен.

Я не проверял этот код ... но я считаю, что все в порядке.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...