복붙노트

[RUBY-ON-RAILS] 어떻게 레일 응용 프로그램에서 경쟁 조건을 피할 수 있습니까?

RUBY-ON-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 레코드 생성 출석 연결 수를 들고 카운터 캐시 열입니다.

내 문제는 내가 완전히 하나 또는 더 많은 사람들이 동시에 코스에서 사용 가능한 마지막 장소를 등록 할 때 경쟁 조건이 발생하지 않도록하는 올바른 방법을 모르는 것입니다.

내 출석 컨트롤러는 다음과 같다 :

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

출석 모델이 저장되면, 액티브는 계속 작동하고 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

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

해결법

  1. ==============================

    1.낙관적 잠금 길을 가야하는 것입니다,하지만 당신은 이미 눈치 챘을 수도로 has_many 협회에서 자식 객체 생성이 잠금 장치를 건너 뛰고 있기 때문에, 당신의 코드는, 액티브 :: StaleObjectError를 제기하지 않습니다. 다음 SQL를 살펴 보자 :

    낙관적 잠금 길을 가야하는 것입니다,하지만 당신은 이미 눈치 챘을 수도로 has_many 협회에서 자식 객체 생성이 잠금 장치를 건너 뛰고 있기 때문에, 당신의 코드는, 액티브 :: StaleObjectError를 제기하지 않습니다. 다음 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
    

    구현 방법 낙관적 잠금 위의 문 쇼 : 공지 사항 WHERE 절에 lock_version = 1. 이후, 경쟁 조건이 발생하면 동시 프로세스는이 정확한 쿼리를 실행하려고하지만, 첫 번째는 원자 2에 lock_version를 업데이트하기 때문에 단지 첫 번째, 성공, 이후 프로세스는 기록과 인상 액티브 :: 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
    

    엄격하게 경쟁 조건을 방지하는 것은 아닙니다,하지만 실제로는 대부분의 경우에 작동합니다.

  2. ==============================

    2.@ run.full 경우 당신은 테스트하지 않아도 ??

    @ 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
    

    관련 scheduled_run가 가득 찬 경우는 @attendance를 저장하지 않습니다.

    이 코드를 테스트하지 않았습니다 ...하지만 난 괜찮아 생각합니다.

  3. from https://stackoverflow.com/questions/3037029/how-do-i-avoid-a-race-condition-in-my-rails-app by cc-by-sa and MIT license