Rails uniqueness validations are not thread safe! This is a problem when even single threaded Rails servers are susceptible to race conditions. Luckily, this is an easy problem to avoid.
If you have a multi-threaded Rails server like Puma or Passenger Enterprise, use asynchronous background processing with tools like Sidekiq, or have a load balanced environment with multiple web servers then the race condition is real! Just defining a uniqueness validation will not keep your data unique and can cause a lot of headaches.
First, let’s look at why they are not thread safe with a simple example. Take the following model.
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :email t.string :name t.timestamps end end end
class User < ActiveRecord::Base validates :email, :name, presence: true validates :email, uniqueness: true end
Now for a simple race condition, two users attempt to make an account using the same dummy email address ‘john@doe.com’. To validate the uniqueness, Rails will query the database for the existence of the email; SELECT COUNT(id) FROM users WHERE email = 'john@doe.com'
. Since both requests come in around the same time, the validation returns a count of 0. Now, Rails will insert the new users, with non-unique email addresses!
Luckily the solution is simple. Just add a unique index to the table:
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :email t.string :name t.timestamps t.index :email, unique: true end end end
Or, if you are creating your model/migration with a generator:
rails generate migration create_users email:string:uniq:index name:string
If the uniqueness validation has a scope, then you will need to create the uniqueness validation on all attributes of the scope. Take the following example, unique usernames for each company.
class User < ActiveRecord::Base validates :username, uniqueness: { scope: :company_id } end
The migration would look like this.
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.integer :company_id t.string :username t.index [:username, :company_id], unique: true end end end
When there is a unique index on the table and this race condition occurs, the second model to be created will throw the ActiveRecord::RecordNotUnique
error. This is an error from the DBMS and not the rails validation, so you must catch it. There are a few ways to handle it, this will depend on how the code is executed.
If the user is being created because of a controller action, then we can re-validate the model. This will allow Rails to create an error with the correct validator.
class UsersController < ApplicationController .... def create @user = User.new(user_params) if @user.save redirect_to @user else render 'new' end rescue ActiveRecord::RecordNotUnique => e @user.validate render 'new' end ... end
Another way to handle this is with a retry. Retries can be useful if this user is being created by some background process. Such as a Sidekiq job that imports users from a third party. Just be sure to not cause an infinite loop of retries.
class UserWorker ... def perform(attributes) retry_count ||= 0 User.create!(attributes) rescue ActiveRecord::RecordNotUnique => e if (retry_count += 1) <= 1 retry else raise end end ... end
That’s it! You now have thread safe uniqueness validations that can handle race conditions.