How do I avoid a race condition in my Rails app?

前端 未结 2 638
感动是毒
感动是毒 2020-12-02 17:37

I have a really simple Rails application that allows users to register their attendance on a set of courses. The ActiveRecord models are as follows:

class Co         


        
相关标签:
2条回答
  • 2020-12-02 18:06

    Optimistic locking is the way to go, but as you might have noticed already, your code will never raise ActiveRecord::StaleObjectError, since child object creation in has_many association skips the locking mechanism. Take a look at the following SQL:

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

    When you update attributes in the parent object, you usually see the following SQL instead:

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

    The above statement shows how optimistic locking is implemented: Notice the lock_version = 1 in WHERE clause. When race condition happens, concurrent processes try to run this exact query, but only the first one succeeds, because the first one atomically updates the lock_version to 2, and subsequent processes will fail to find the record and raise ActiveRecord::StaleObjectError, since the same record doesn't have lock_version = 1 any longer.

    So, in your case, a possible workaround is to touch the parent right before you create/destroy a child object, like so:

    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
    

    It's not meant to strictly avoid race conditions, but practically it should work in most cases.

    0 讨论(0)
  • 2020-12-02 18:16

    Don't you just have to test if @run.full??

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

    Edit

    What if you add a validation like:

    class Attendance < ActiveRecord::Base
       validate :validates_scheduled_run
    
       def scheduled_run
          errors.add_to_base("Error message") if self.scheduled_run.full?
       end
    end
    

    It won't save the @attendance if the associated scheduled_run is full.

    I haven't tested this code... but I believe it's ok.

    0 讨论(0)
提交回复
热议问题