Search code examples
ruby-on-railsrubyactiverecordherokuheroku-postgres

Can model instances in a Rails app be modified by other DB connections while they're being inspected?


I'm diving into Rails 4 and I'm trying to understand how to safely access model data while it's being accessed by multiple DB connections. I have some match-making logic that finds the oldest user in a queue, removes that user from the queue, and returns the user...

# UserQueue.rb
class UserQueue < ActiveRecord::Base
  has_many :users

  def match_user(user)
    match = nil
    if self.users.count > 0
      oldest = self.users.oldest_in_queue 
      if oldest.id != user.id
        self.users.delete(oldest)
        match = oldest
      end
    end
  end
end

If two different threads executed this match_user method around the same time, is it possible for them to both find the same oldest user and try to delete it from the queue, and return it to the caller? If so, how do I prevent that?

I looked into transactions, but they don't seem to be a solution since there's only one model being modified in this case (the queue).

Thanks in advance for your wisdom!


Solution

  • ActiveRecord has support for row locking.

    This is taken from the Rails guide, locking records for update:

    11.1 Optimistic Locking

    Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened. An ActiveRecord::StaleObjectError exception is thrown if that has occurred and the update is ignored.

    Optimistic locking column

    In order to use optimistic locking, the table needs to have a column called lock_version of type integer. Each time the record is updated, Active Record increments the lock_version column. If an update request is made with a lower value in the lock_version field than is currently in the lock_version column in the database, the update request will fail with an ActiveRecord::StaleObjectError. Example:

    c1 = Client.find(1)
    c2 = Client.find(1)
    
    c1.first_name = "Michael"
    c1.save
    
    c2.name = "should fail"
    c2.save # Raises an ActiveRecord::StaleObjectError
    

    You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict.

    This behavior can be turned off by setting ActiveRecord::Base.lock_optimistically = false.

    To override the name of the lock_version column, ActiveRecord::Base provides a class attribute called locking_column:

    class Client < ActiveRecord::Base
      self.locking_column = :lock_client_column
    end
    

    I suggest reading this section in the Rails guide.