2010-06-14 2 views
20

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

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 

attendances_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 уже достиг мощности.

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

Update

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

Я добавил два столбца таблицы 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. В этот момент все это снова повторяется, если пропускная способность еще не достигнута.

Update # 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 
+0

Исправлено в последних рельсах. –

+0

Вам нужно использовать оптимистичную блокировку. Этот скринкаст покажет вам, как это сделать: [link text] (http://railscasts.com/episodes/59-optimistic-locking) – rtacconi

+0

Что вы имеете в виду, dmitry? – Edward

ответ

13

Оптимистическая блокировка - это путь, но, как вы уже заметили, ваш код никогда не будет поднимать 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) 

При обновлении атрибутов в родительских объекта, вы обычно видите следующий 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

Спасибо, Кенн. Я не понимал, что создание дочерних объектов пропустило механизм блокировки. Я тоже обернул все это в транзакцию, так что родительский объект не будет излишне обновляться, если создание дочернего объекта завершится неудачно. – Cathal

0

Не нужно просто проверить, если @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 полон.

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

+0

Это не сработает. Проблема в том, что запись @run может уже быть обновлена ​​другим запросом, оставив @run несовместимым с тем, что представлено в базе данных. Насколько я знаю, оптимистическая блокировка - это способ решить эту проблему. Однако, как вы собираетесь применять это к ассоциациям? – Cathal

+0

Правильно ... Я отредактировал свой ответ:] –

Смежные вопросы