Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Race conditions in Rails first_or_create

I'm trying to enforce uniqueness of values in one of my table fields. Changing the table isn't an option. I need to use ActiveRecord to conditionally insert a row into the table but I'm concerned about synchronization.

Does first_or_create in Rails ActiveRecord prevent race conditions?

This is the source code for first_or_create from GitHub:

def first_or_create(attributes = nil, options = {}, &block)
  first || create(attributes, options, &block)
end

Is it possible that a duplicate entry will result in the database due to synchronization issues with multiple processes?

like image 960
Maros Avatar asked May 17 '12 18:05

Maros


3 Answers

The Rails 4 documentation for find_or_create_by provides a tip that may be useful for this situation:

Please note this method is not atomic, it runs first a SELECT, and if there are no results an INSERT is attempted. If there are other threads or processes there is a race condition between both calls and it could be the case that you end up with two similar records.

Whether that is a problem or not depends on the logic of the application, but in the particular case in which rows have a UNIQUE constraint an exception may be raised, just retry:

begin
  CreditAccount.find_or_create_by(user_id: user.id)
rescue ActiveRecord::RecordNotUnique
  retry
end

Similar error catching may be useful for Rails 3. (Not sure if the same ActiveRecord::RecordNotUnique error is thrown in Rails 3, so your implementation may need to be different.)

like image 194
Chris Peters Avatar answered Nov 14 '22 20:11

Chris Peters


Yes, it's possible.

You can significantly reduce the chance of conflict with either optimistic or pessimistic locking. Of course optimistic locking requires adding a field to the table, and pessimistic locking doesn't scale as well--plus, it depends on your data store's capabilities.

I'm not sure whether you need the extra protection, but it's available.

like image 29
David Avatar answered Nov 14 '22 19:11

David


Rails 6 (released in 2019) introduced a new method, create_or_find_by, specifically to deal with the potential race condition / synchronization issue that exists with first_or_create and find_or_create_by, where two different threads could simultaneously SELECT to see if the target record exists, both see that it doesn't, and then both try to INSERT it.

Example usage:

company = Company.create_or_find_by(name: 'Stack Overflow')

(Where Company is an existing ActiveRecord model, and name is a unique-constrained column on the company table.)

Also available: create_or_find_by!, which raises an exception if a validation error occurs.

like image 3
Jon Schneider Avatar answered Nov 14 '22 18:11

Jon Schneider