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!
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 thelock_version
column. If an update request is made with a lower value in thelock_version
field than is currently in thelock_version
column in the database, the update request will fail with anActiveRecord::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 calledlocking_column
:
class Client < ActiveRecord::Base
self.locking_column = :lock_client_column
end
I suggest reading this section in the Rails guide.